docs: add C4 architecture + sequence diagrams (Mermaid)
Context and container diagrams, sequence diagrams for the core summarize-new-video use case and the local-first/BYO AI routing fallback, and the Clean Architecture layering. Ports & adapters keep the engine provider- and sink-agnostic, making standalone-vs-homelab a wiring choice (ADR-003), not two codebases.
This commit is contained in:
@@ -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<br/>(maintainer; later, trusted users)"]
|
||||||
|
tapir["Tapir<br/>watches subscriptions,<br/>summarizes new videos"]
|
||||||
|
yt["YouTube<br/>(Data API + captions)"]
|
||||||
|
vimeo["Vimeo<br/>(API + text tracks)"]
|
||||||
|
local["Local AI stack<br/>(LiteLLM / piguard alias)"]
|
||||||
|
byo["User's BYO AI<br/>(Claude / OpenAI / Gemini)"]
|
||||||
|
brain["brain<br/>(brain-mcp)"]
|
||||||
|
|
||||||
|
user -->|connects accounts,<br/>reads summaries| tapir
|
||||||
|
tapir -->|subscriptions,<br/>new-video events,<br/>captions| yt
|
||||||
|
tapir -->|subscriptions,<br/>text tracks| vimeo
|
||||||
|
tapir -->|summarize<br/>PRIMARY| local
|
||||||
|
tapir -.->|summarize<br/>FALLBACK, opt-in| byo
|
||||||
|
tapir -.->|optional sink:<br/>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<br/>OAuth callbacks +<br/>user-facing API"]
|
||||||
|
watcher["Watcher<br/>detects new videos<br/>(WebSub + poll)"]
|
||||||
|
engine["Summarization engine<br/>(use-case core)"]
|
||||||
|
resolver["Transcript resolver<br/>(captions-first)"]
|
||||||
|
router["AI router<br/>(llm.Router:<br/>Primary -> Fallback)"]
|
||||||
|
store[("User store<br/>(Postgres,<br/>per-user isolated)")]
|
||||||
|
|
||||||
|
subgraph sinks["Sink adapters (Sink interface)"]
|
||||||
|
sink_store["Store sink<br/>(primary)"]
|
||||||
|
sink_brain["Brain sink<br/>(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<br/>(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<br/>(local: LiteLLM/piguard)
|
||||||
|
participant F as Fallback<br/>(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,<br/>Transcript, Summary"]
|
||||||
|
end
|
||||||
|
subgraph usecase["Use cases (engine)"]
|
||||||
|
u["SummarizeNewVideo,<br/>ConnectAccount,<br/>RouteAI"]
|
||||||
|
end
|
||||||
|
subgraph ports["Ports (interfaces)"]
|
||||||
|
po["VideoSource, Summarizer,<br/>Sink, SecretStore"]
|
||||||
|
end
|
||||||
|
subgraph adapters["Adapters (infra)"]
|
||||||
|
a["YouTube, Vimeo, llm.Router,<br/>Store sink, Brain sink,<br/>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).
|
||||||
Reference in New Issue
Block a user