Layered Architecture (L0–L7)

February 24, 2026 · View on GitHub

Reference documentation for vidpipe's layered architecture. For the background mockability analysis, see audit/mockability/.


1. Introduction

The vidpipe codebase uses an eight-layer architecture (L0–L7) to establish clear boundaries between pure logic, infrastructure, external clients, business logic, AI agents, assets, orchestration, and entry points.

Why layers?

  1. Mockability boundaries. Each layer has a single, well-defined mock strategy. You never wonder what to stub — the layer tells you.
  2. Import direction enforcement. Layer N may only import from layers 0 through N. Upward imports are prohibited.
  3. Test strategy per layer. Pure functions get zero-mock unit tests. Clients get API stubs. Agents get injected providers. The layer determines the test type.

The full mockability analysis that motivated this design lives in audit/mockability/README.md.


2. Layer Overview

LayerNamePurposeExamples
L0PureZero-dependency pure functions — no I/O, no mocking neededtypes/, pricing.ts, captionGenerator.ts, text.ts, buildFilterComplex(), adjustTranscript(), platformContentStrategy.ts
L1InfrastructureThin wrappers around Node.js built-ins and external packagesconfig/environment.ts, logger.ts, core/fileSystem.ts, core/paths.ts, core/process.ts, core/env.ts, ai/ (OpenAI, Anthropic, Copilot, Gemini SDK wrappers), image/image.ts (sharp), ffmpeg/ffmpeg.ts (fluent-ffmpeg), http/httpClient.ts
L2ClientsExternal API and child-process clientsFFmpeg tools (silenceDetection, clipExtraction, …), whisperClient.ts, geminiClient.ts, LLM providers (CopilotProvider, OpenAIProvider, ClaudeProvider), lateApi.ts
L3ServicesBusiness logic that composes L2 clientstranscription.ts, costTracker.ts, postStore.ts, scheduler.ts, queueBuilder.ts, processingState.ts, gitOperations.ts
L4AgentsLLM-powered agents extending BaseAgentBaseAgent.ts, ShortsAgent.ts, MediumVideoAgent.ts, SummaryAgent.ts, BlogAgent.ts, ChapterAgent.ts, SocialMediaAgent.ts, ProducerAgent.ts
L5AssetsLazy-loaded artifact representations with L4 bridge modulesMainVideoAsset, ShortVideoAsset, MediumClipAsset, loaders.ts, bridge modules (videoServiceBridge, analysisServiceBridge, pipelineServiceBridge)
L6PipelineStage orchestrationpipeline.ts, runStage(), stages/visualEnhancement.ts
L7AppEntry points — CLI, servers, watcherssrc/index.ts (CLI), review server, fileWatcher.ts, commands (init, schedule, doctor)

3. Layer Rules

Import Direction — Strict Single-Dependency + Foundation

L0 and L1 are foundation layers — importable from any layer. All other layers follow strict single-dependency rules: each business layer imports only from the layer directly below it (plus L0/L1).

L7-App  ──imports──▶  L6, L3, L1, L0
L6-Pipeline  ────▶    L5, L1, L0
L5-Assets  ──────▶    L4, L1, L0
L4-Agents  ──────▶    L3, L1, L0
L3-Services  ────▶    L2, L1, L0
L2-Clients  ─────▶    L1, L0
L1-Infra  ───────▶    L0
L0-Pure  ────────▶    (nothing)

Type-only imports are exempt. import type { ... } may cross any boundary since they are erased at compile time and create no runtime coupling.

Per-Layer Policies

LayerMay ImportMust NOT ImportSingletons?I/O?
L0Self onlyL1–L7, fs, child_processNoNo
L1L0L2–L7Yes (logger, config)Yes (Node.js built-ins)
L2L0, L1L3–L7NoYes (external APIs, binaries)
L3L0, L1, L2L4–L7AllowedVia L2 only
L4L0, L1, L3L2, L5–L7NoVia L3 only
L5L0, L1, L4L2, L3, L6–L7NoVia L4 bridge modules
L6L0, L1, L5L2, L3, L4, L7NoVia L5 asset methods
L7L0, L1, L3, L6L2, L4, L5Yes (CLI, server)Yes (entry point)

4. Architecture Diagram

┌─────────────────────────────────────────────────────────┐
│  L7  App                                                │
│  CLI · Review Server · File Watcher · Commands          │
│  imports: L6, L3, L1, L0                                │
├─────────────────────────────────────────────────────────┤
│  L6  Pipeline                                           │
│  pipeline.ts · runStage() · visualEnhancement           │
│  imports: L5, L1, L0                                    │
├─────────────────────────────────────────────────────────┤
│  L5  Assets                                             │
│  MainVideoAsset · ShortVideoAsset · bridge modules      │
│  imports: L4, L1, L0                                    │
├─────────────────────────────────────────────────────────┤
│  L4  Agents                                             │
│  BaseAgent · ShortsAgent · SummaryAgent · BlogAgent · … │
│  imports: L3, L1, L0                                    │
├─────────────────────────────────────────────────────────┤
│  L3  Services                                           │
│  transcription · costTracker · postStore · scheduler     │
│  imports: L2, L1, L0                                    │
├─────────────────────────────────────────────────────────┤
│  L2  Clients                                            │
│  FFmpeg tools · Whisper · Gemini · LLM providers · Late │
│  imports: L1, L0                                        │
├══════════════════════ FOUNDATION ════════════════════════┤
│  L1  Infrastructure                                     │
│  config · logger · fileSystem · process · paths · env   │
│  ai/ (OpenAI, Anthropic, Copilot, Gemini wrappers)      │
│  image/ (sharp) · ffmpeg/ (fluent-ffmpeg) · http/       │
│  imports: L0                                            │
├─────────────────────────────────────────────────────────┤
│  L0  Pure                                               │
│  types · pricing · captions · filters · text · platform │
│  imports: (nothing)                                     │
└─────────────────────────────────────────────────────────┘
       ▲  L0+L1 are foundation — importable from ANY layer
       ▲  Business layers import ONLY from the layer directly below + foundation

5. Test Strategy

Each layer has a defined test type and mock policy.

Unit Tests (per-layer)

LayerWhat's RealWhat's MockedTimeout
L0EverythingNothing5 s
L1L0 + L1 logicfs, process.env, child_process5 s
L2L0–L2 logicExternal APIs, FFmpeg binary, file system10 s
L3L0–L3 logicL2 clients (via vi.mock())10 s
L4L0, L1, L4 logicL3 services (via vi.mock())10 s
L5L0, L1, L5 logicL4 agents/bridges (via vi.mock())10 s
L6L0, L1, L6 logicL5 assets (via vi.mock())10 s
L7L0, L1, L7 logicL6 pipeline (via vi.mock())10 s

Integration Tests (cross-layer, tiered mock boundaries)

WorkspaceLayers Under TestCoverage ScopeMock BoundaryTimeout
integration-L3L2 + L3 (real clients + services)L2, L3L1 mocked30 s
integration-L4-L6L4 + L5 + L6 (agents + assets + pipeline)L4, L5, L6L2 mocked (L3 runs real but uncounted)60 s
integration-L7L7 app layerL7L1 + L3 mocked60 s

E2E Tests

Test TypeWhat's RealWhat's MockedTimeout
E2EEverythingNothing (real FFmpeg, real I/O)120 s

Vitest Workspace Commands

The test suite is split by project so you can run just the layer you're working on:

npx vitest --project unit                # L0–L7 unit — fast, no external deps
npx vitest --project integration-L3      # L3 services — mocks L1/L0
npx vitest --project integration-L4-L6   # L4-L6 layers — mocks L2
npx vitest --project integration-L7      # L7 app — mocks L1-L3
npx vitest --project e2e                 # Real FFmpeg, real I/O

Per-tier scripts with coverage:

npm run test:integration:L3:coverage
npm run test:integration:L4-L6:coverage
npm run test:integration:L7:coverage
npm run test:e2e:coverage

Mock Simplification Example

Before layers — testing transcription.ts required five mocks:

vi.mock('../tools/whisper/whisperClient.js')
vi.mock('../tools/ffmpeg/audioExtraction.js')
vi.mock('../core/fileSystem.js')
vi.mock('../config/logger.js')
vi.mock('../services/costTracker.js')

With layerstranscription.ts is L3, so only L2 clients need mocking:

vi.mock('../../L2-clients/whisper/whisperClient.js')
vi.mock('../../L2-clients/ffmpeg/audioExtraction.js')

L0 and L1 are foundation layers — they run real in unit tests (no mocking needed).

Integration Test Mock Examples

// Integration L3 — mock L1 only, L2 clients run REAL
vi.mock('../../../L1-infra/fileSystem/fileSystem.js', () => ({ /* controlled I/O */ }))
import { markPending } from '../../../L3-services/processingState/processingState.js'

// Integration L4-L6 — mock L2 only, L3+L4+L5+L6 run real
vi.mock('../../../L2-clients/gemini/geminiClient.js', () => ({ /* fake Gemini */ }))
import { MainVideoAsset } from '../../../L5-assets/MainVideoAsset.js'

// Integration L7 — mock L1 + L3, test real L7 app layer
vi.mock('../../../L1-infra/config/environment.js', () => ({ /* controlled config */ }))
vi.mock('../../../L3-services/lateApi/lateApiService.js', () => ({ /* fake Late API */ }))
import { createRouter } from '../../../L7-app/review/routes.js'

6. Enforcement

Layer boundaries are enforced at three levels:

Agent Hooks (.github/hooks/)

HookPurpose
pre-layer-importBlocks upward imports. If a file in L2 tries to import from L3+, the hook rejects the change.
pre-layer-mockBlocks inappropriate mocking. If an L0 test mocks something, or an L3 test mocks L0/L1, the hook flags it.

Instruction File (.github/instructions/layers.instructions.md)

The Copilot instruction file teaches the AI assistant the layer rules proactively. When Copilot generates code, it follows the import direction and mocking constraints automatically.

Hook Rules Reference

For the complete specification of every import rule, mocking constraint, exemption, and known limitation enforced by these hooks, see:

📄 .github/hooks/README.md — the authoritative source for all enforcement rules.

Future: ESLint Plugin

A CI-enforced ESLint rule will scan import paths and fail the build on layer violations:

# Conceptual check — L0 files must not import from L1+
grep -r "from '\.\./L[1-7]" src/L0-pure/ && echo "VIOLATION" || echo "OK"

7. Adding New Files

Use this decision tree to determine which layer a new file belongs in:

Does it have zero dependencies and zero I/O?
  └─ Yes → L0 (Pure)

Does it wrap a Node.js built-in (fs, path, child_process, http)?
  └─ Yes → L1 (Infrastructure)

Does it call an external API or spawn an external process?
  └─ Yes → L2 (Clients)

Does it contain business logic that composes L2 clients?
  └─ Yes → L3 (Services)

Is it an LLM-powered agent extending BaseAgent?
  └─ Yes → L4 (Agents)

Is it a lazy-loaded artifact representation?
  └─ Yes → L5 (Assets)

Does it orchestrate pipeline stages?
  └─ Yes → L6 (Pipeline)

Is it an entry point (CLI, server, watcher)?
  └─ Yes → L7 (App)

Quick Examples

ScenarioLayerReasoning
New cost-calculation helperL0Pure math, no I/O
Redis cache wrapperL1Infrastructure adapter
YouTube Data API clientL2External API client
Video publishing serviceL3Business logic composing L2 clients
ThumbnailAgentL4LLM-powered agent
ThumbnailAssetL5Lazy-loaded representation
New pipeline stageL6Orchestration
vidpipe publish commandL7CLI entry point

8. Bridge Modules

L5 assets need access to L3 services but can only import L4. To maintain strict layer rules, bridge modules in L4 re-export L3 functionality:

Bridge Module (L4)Re-exports From (L3)
videoServiceBridge.tsFFmpeg operations from videoOperations
analysisServiceBridge.tsGemini analysis, transcription, caption generation
pipelineServiceBridge.tscostTracker, processingState, gitOperations, queueBuilder
// L5 imports from L4 bridge (allowed: L5 → L4)
import { singlePassEdit } from '../../L4-agents/videoServiceBridge.js'

// Bridge re-exports from L3 (allowed: L4 → L3)
export { singlePassEdit } from '../../L3-services/videoOperations/videoOperations.js'

Similarly, L7 needs L2 functionality (Late API, FFmpeg paths) through L3 service wrappers:

L3 WrapperWraps (L2)
lateApiService.tsLateApiClient from L2-clients/late/lateApi
diagnostics.tsFFmpeg/FFprobe path resolvers from L2-clients/ffmpeg

9. Migration Status

Status: Complete. All source files have been restructured into L0–L7 folders. Layer enforcement hooks are active.

The physical folder restructure from the legacy layout (src/agents/, src/tools/, src/services/, etc.) to the layered layout (src/L0-pure/ through src/L7-app/) is done. All imports, tests, and CI are aligned to the new structure.