diff --git a/docs/architecture/architecture.md b/docs/architecture/architecture.md new file mode 100644 index 0000000..022f64e --- /dev/null +++ b/docs/architecture/architecture.md @@ -0,0 +1,196 @@ +# Tapir — Architecture (C4 + sequences) + +Diagrams describe **current assumptions**, not a frozen design. They are diffable Mermaid +so they live in version control and render in Gitea. When a decision here changes, record it +in `DECISIONS.md` and update the relevant diagram in the same commit. + +Altitude: C4 Level 1 (Context) and Level 2 (Container), plus sequence diagrams for the two +behaviours that carry the most design risk. Clean Architecture layering is described after +the diagrams. + +--- + +## C4 L1 — System Context + +Who and what Tapir talks to. (Brain and external AI are dashed — both optional.) + +```mermaid +graph TB + user["User
(maintainer; later, trusted users)"] + tapir["Tapir
watches subscriptions,
summarizes new videos"] + yt["YouTube
(Data API + captions)"] + vimeo["Vimeo
(API + text tracks)"] + local["Local AI stack
(LiteLLM / piguard alias)"] + byo["User's BYO AI
(Claude / OpenAI / Gemini)"] + brain["brain
(brain-mcp)"] + + user -->|connects accounts,
reads summaries| tapir + tapir -->|subscriptions,
new-video events,
captions| yt + tapir -->|subscriptions,
text tracks| vimeo + tapir -->|summarize
PRIMARY| local + tapir -.->|summarize
FALLBACK, opt-in| byo + tapir -.->|optional sink:
brain_ingest| brain + + classDef opt stroke-dasharray: 5 5; + class byo,brain opt; +``` + +--- + +## C4 L2 — Containers + +Inside Tapir. The **engine** is provider- and sink-agnostic; everything external is an +adapter behind an interface (Clean Architecture ports & adapters). + +```mermaid +graph TB + subgraph tapir["Tapir (Go)"] + http["HTTP server
OAuth callbacks +
user-facing API"] + watcher["Watcher
detects new videos
(WebSub + poll)"] + engine["Summarization engine
(use-case core)"] + resolver["Transcript resolver
(captions-first)"] + router["AI router
(llm.Router:
Primary -> Fallback)"] + store[("User store
(Postgres,
per-user isolated)")] + + subgraph sinks["Sink adapters (Sink interface)"] + sink_store["Store sink
(primary)"] + sink_brain["Brain sink
(HTTP brain-mcp)"] + end + + subgraph providers["Provider adapters (VideoSource interface)"] + p_yt["YouTube adapter"] + p_vimeo["Vimeo adapter"] + end + end + + http --> store + watcher --> p_yt + watcher --> p_vimeo + watcher -->|NewVideo event| engine + engine --> resolver + resolver --> p_yt + resolver --> p_vimeo + engine --> router + engine --> sink_store + engine -.-> sink_brain + sink_store --> store + + classDef opt stroke-dasharray: 5 5; + class sink_brain opt; +``` + +**Interfaces that keep the engine pure (the ports):** + +- `VideoSource` — list subscriptions, detect new videos, fetch transcript. Implemented by + YouTube and Vimeo adapters. +- `Summarizer` — turn a transcript + context into highlights/takeaways. Backed by the + `llm.Router` (Primary local, Fallback BYO). +- `Sink` — deliver a summary. Implemented by the store sink (primary) and brain sink + (optional). New sinks add an implementation, nothing else. +- `SecretStore` — fetch/store per-user OAuth tokens and BYO keys. Backed by ESO/1Password. + +The engine depends only on these interfaces, never on YouTube, brain, or a concrete model. +This is what makes "standalone vs homelab" a configuration of which adapters are wired, not +two codebases (ADR-003). + +--- + +## Sequence — core use case: new video summarized + +```mermaid +sequenceDiagram + participant Src as VideoSource
(YouTube/Vimeo) + participant W as Watcher + participant E as Engine + participant R as Transcript resolver + participant AI as AI router + participant S as Sink(s) + + Src->>W: new upload (WebSub push / poll) + W->>E: NewVideo{user, channel, videoID} + E->>R: resolve transcript(videoID) + R->>Src: fetch captions + alt captions available + Src-->>R: captions text + R-->>E: Transcript{source: captions} + E->>AI: summarize(transcript, userContext) + AI-->>E: Summary{highlights, takeaways} + E->>S: deliver(summary) + S-->>E: ok + else no transcript + Src-->>R: none + R-->>E: NoTranscript + E->>S: deliver(skipped: no transcript) + end +``` + +--- + +## Sequence — AI routing (local-first, BYO fallback) + +```mermaid +sequenceDiagram + participant E as Engine + participant R as llm.Router + participant P as Primary
(local: LiteLLM/piguard) + participant F as Fallback
(user BYO key) + + E->>R: summarize(transcript) + R->>P: complete(prompt) + alt local succeeds + P-->>R: summary + R-->>E: summary (fallback_used = false) + else local fails (error/timeout/unavailable) + P-->>R: error + alt user has BYO configured + R->>F: complete(prompt) + F-->>R: summary + R-->>E: summary (fallback_used = true) + else no BYO + R-->>E: error (queued for retry) + end + end +``` + +> "Reliably" (VISION / the BYO trigger) is operationalized as: **Primary returned without +> error within timeout.** Richer quality scoring can layer on later without changing the +> interface. `fallback_used` is recorded per summary so "is the local stack good enough?" +> becomes a query, not a guess. + +--- + +## Clean Architecture layering + +```mermaid +graph LR + subgraph domain["Domain (entities)"] + d["User, Subscription, Video,
Transcript, Summary"] + end + subgraph usecase["Use cases (engine)"] + u["SummarizeNewVideo,
ConnectAccount,
RouteAI"] + end + subgraph ports["Ports (interfaces)"] + po["VideoSource, Summarizer,
Sink, SecretStore"] + end + subgraph adapters["Adapters (infra)"] + a["YouTube, Vimeo, llm.Router,
Store sink, Brain sink,
ESO secret store, HTTP, Postgres"] + end + + a --> po + po --> u + u --> d +``` + +Dependencies point inward only. Domain knows nothing of YouTube, brain, Postgres, or any +model. Adapters are swappable; tests target the use-case core through fake adapters (see the +Gherkin features in `docs/use-cases/`). + +--- + +## Out of scope in these diagrams (deferred per ADRs) + +- Audio-download + speech-to-text resolver (ADR-007) — would be an additional `VideoSource` + fallback path, drawn when built. +- Multi-tenant isolation primitives (per-tenant Postgres role, NetworkPolicy, tenant label) + — activate at Stage 1 (ADR-002); single-user Stage 0 doesn't exercise them. +- Public SaaS surface (sign-up, billing) — Future C, not built (ADR-008).