55 KiB
Strict AI-Native Repository Profile
SSOT: This document is the canonical architecture and repository profile.
runtime-research.mdis supporting research, not an authority. When the two conflict, this document wins; adopted decisions must be recorded here.
0. Stack
This repository describes a Python backend with a separate SvelteKit frontend.
Backend
- Language and runtime: Python 3.13
- Package and project manager:
uv - HTTP API: FastAPI
- ASGI server: Uvicorn
- API schemas: Pydantic 2 and OpenAPI
- Configuration:
pydantic-settings(BaseSettings)
- Configuration:
- Password hashing:
pwdlib[argon2]using Argon2id - Database: PostgreSQL
- Data access: SQLAlchemy 2.x async engine and ORM
- Database migrations: Alembic
- PostgreSQL driver:
psycopg3 async implementation - Database URL scheme:
postgresql+psycopg://
Frontend
- Framework: SvelteKit
- Production adapter:
@sveltejs/adapter-node - UI runtime: Svelte 5
- Language: TypeScript
- Styling: Tailwind CSS v4
- UI components: shadcn-svelte
- Accessible primitives: Bits UI
- Icons: Lucide Svelte
Verification
- Backend tests: pytest
- Frontend tests: Vitest
- Browser tests: Playwright
- Python quality: Ruff, Pyright, Import Linter
- Frontend quality: Prettier, ESLint,
svelte-check
Redis, message brokers, distributed tracing, Sentry, Loki, Datadog, and Kubernetes are not part of the default stack. Add them only when an explicit operational requirement justifies them.
1. Objective
Repository được tối ưu cho AI coding agents theo nguyên tắc:
minimize decision entropy
maximize structural signal
maximize mechanical enforcement
Agent được quyền quyết định cách giải bài toán.
Agent không được tự quyết lại:
naming grammar
test taxonomy
verification workflow
Repository phải làm cho:
valid decisions → obvious
invalid decisions → mechanically difficult
2. Repository Structure
project/
├── AGENTS.md
├── CLAUDE.md
├── README.md
├── ARCHITECTURE.md
├── CONTEXT.md
│
├── pyproject.toml
├── uv.lock
├── .python-version
├── .importlinter
├── .pre-commit-config.yaml
├── .env.example
├── alembic.ini
├── Makefile
├── Dockerfile.backend
├── .dockerignore
├── compose.yaml
│
├── docs/
│ ├── specs/
│ ├── adr/
│ └── agents/
│ ├── domain.md
│ ├── issue-tracker.md
│ └── triage-labels.md
│
├── migrations/
│ ├── env.py
│ ├── script.py.mako
│ └── versions/
│
├── apps/
│ └── web/
│ ├── src/
│ │ ├── routes/
│ │ ├── lib/
│ │ │ ├── components/
│ │ │ │ ├── ui/
│ │ │ │ └── shared/
│ │ │ ├── features/
│ │ │ ├── generated/ # API types/client when Web consumes the API
│ │ │ ├── server/
│ │ │ ├── styles/
│ │ │ └── utils/
│ │ ├── hooks.server.ts
│ │ └── app.html
│ ├── static/
│ ├── tests/
│ │ └── unit/
│ ├── Dockerfile
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── svelte.config.js
│ └── vite.config.ts
│
├── src/
│ └── <service>/
│ ├── domain/
│ │ ├── entities/
│ │ ├── value_objects/
│ │ ├── policies/
│ │ ├── events/
│ │ └── errors/
│ │
│ ├── application/
│ │ ├── use_cases/
│ │ └── ports/
│ │ ├── inbound/
│ │ └── outbound/
│ │
│ ├── infrastructure/
│ │ ├── adapters/
│ │ │ ├── persistence/
│ │ │ ├── clients/
│ │ │ └── messaging/
│ │ └── config/
│ │
│ ├── presentation/
│ │ ├── http/
│ │ ├── cli/
│ │ └── consumers/
│ │
│ └── bootstrap/
│ ├── app.py
│ ├── entrypoint.py
│ └── wiring.py
│
├── tests/
│ ├── unit/
│ │ ├── domain/
│ │ └── application/
│ ├── integration/
│ │ └── infrastructure/
│ ├── functional/
│ │ └── presentation/
│ └── architecture/
│
├── e2e/
│ ├── playwright.config.ts
│ └── tests/
│
├── .artifacts/
│ └── verification/
│ ├── screenshots/
│ ├── traces/
│ └── videos/
│
└── .github/
└── workflows/
└── ci.yml
Container ownership:
Dockerfile.backend
→ builds the Python/FastAPI backend image
apps/web/Dockerfile
→ builds the SvelteKit/Node web image
compose.yaml
→ orchestrates backend, web, and PostgreSQL locally
→ does not define image build logic itself
Each deployable runtime has its own image and Dockerfile. Multi-stage builds remain inside each Dockerfile. Do not combine backend and web into one production image.
Makefile ownership:
Makefile
→ canonical repository command interface for developers, agents, and CI
→ owns recipes such as db-up, db-down, backend-dev, web-dev, check, test, migrate, admin-create, and build
→ calls existing tools such as uv, pnpm, Docker Compose, Alembic, and pytest
→ contains no business logic or duplicated implementation logic
Makefile conventions:
→ command recipes are `.PHONY`
→ `make help` lists supported commands
→ recipes remain simple and POSIX-shell compatible
→ CI calls the same recipes documented for local use
→ do not add a second task runner or duplicate command catalog
Ownership:
.env.example
→ the single canonical environment variable list with dummy values.
→ Checked in. It is the only env template file.
→ Secrets have no real defaults. App fails fast at startup if missing.
→ `PUBLIC_*` is exposed to the browser. Never put secrets in `PUBLIC_*`.
Env files:
```text
.env.example
→ checked in, documents every variable dev and test need
→ dev and test share the same shape, only values differ
.env
→ local-only dev values, never committed
→ copy from the example: `cp .env.example .env`
.env.test
→ local-only test values, never committed
→ points at an isolated test database and credentials
→ test runner loads `.env.test` instead of `.env`
staging / prod
→ no env file shipped with the artifact
→ values injected by the deployment environment or secrets manager
Variable categories:
runtime
→ APP_ENV, LOG_LEVEL
database
→ DATABASE_URL
auth / external services
→ API_KEY_ENCRYPTION_KEY for re-displaying existing API keys; keep it outside the database
→ credentials required by integrations actually used
→ server-only, never PUBLIC_*
backend integrations
→ AI provider keys, base URLs
→ messaging brokers, webhook secrets
→ only the integrations the service actually uses
web public
→ PUBLIC_APP_NAME and other explicitly browser-safe runtime values
→ browser calls SvelteKit by default; `PUBLIC_API_URL` is added only if browser-to-FastAPI calls are explicitly adopted
web server-only
→ `BACKEND_INTERNAL_URL` and backend credentials used by `$lib/server/`
→ never `PUBLIC_*`
Environment separation:
dev
→ local `.env`, dummy values allowed except real-looking secrets
test
→ isolated test database and credentials
→ never points at dev or prod data
staging / prod
→ injected by the deployment environment or secrets manager
→ no `.env` file shipped with the artifact
→ production secrets never appear in repo, logs, or screenshots
Why dev and test are separated:
dev
→ where a human works, dirty data is acceptable
test
→ where machines verify, data must be clean and predictable
Sharing one database lets manual work break tests
and lets test fixtures wipe manual work.
Backend-Web HTTP contract → FastAPI/Pydantic schema owns the definition. → Do not commit generated OpenAPI or create a contract pack by default. → Once Web consumes the API, one Makefile recipe exports transient OpenAPI and generates TypeScript types/client into apps/web/src/lib/generated/ for local setup, CI, and builds. → Generated outputs are ignored by Git, never manually edited, and produced before Web checks and image builds.
migrations/
→ canonical database migration history.
→ env.py uses the SQLAlchemy async engine configuration.
→ Alembic revisions live in versions/ and are reviewed before merge.
→ alembic check runs in CI to detect model/migration drift.
→ alembic upgrade head runs once as a release step before application startup.
→ FastAPI lifespan must not run database migrations.
apps/web/tests/ → isolated web unit/component tests. → No backend, no network, no browser. → Uses Vitest.
e2e/ → single full-system browser regression suite. → backend + web + database running together. → Uses Playwright Test. → The only browser E2E entry point.
apps/web/src/lib/utils/ → generic presentation utilities only. → Examples: class merging, date formatting, numeric clamping. → Must be pure, reused in at least two places, and must not import domain or feature concepts.
---
# 3. Documentation Ownership
Every type of knowledge has exactly one canonical owner.
```text
AGENTS.md / CLAUDE.md
→ HOW TO WORK
README.md
→ WHAT THIS PROJECT IS
ARCHITECTURE.md
→ WHERE CODE GOES
CONTEXT.md
→ WHAT WORDS MEAN
docs/specs/
→ WHAT THE SYSTEM SHOULD DO
docs/adr/
→ WHY WE CHOSE THIS
docs/agents/
→ HOW EXTERNAL AGENT SKILLS INTEGRATE
AGENTS.md
Contains only repository-wide agent workflow and hard operational rules.
It should route agents toward the appropriate source of truth instead of duplicating those sources.
Example responsibility:
Before coding:
1. Read relevant CONTEXT.md terms.
2. Read ARCHITECTURE.md.
3. Read relevant approved spec.
4. Read relevant ADR when architectural decisions are involved.
5. Inspect an existing exemplar in the target layer.
Before handoff:
1. Ruff
2. Pyright
3. Import Linter
4. Relevant tests
5. Browser verification when user-facing behavior changes
CLAUDE.md
Claude-specific entry point.
Prefer:
Follow AGENTS.md for repository-wide instructions.
Do not duplicate architecture or domain rules.
README.md
Contains:
project purpose
scope
basic architecture overview
installation
development setup
run commands
deployment entry points
It does not define architectural rules.
CONTEXT.md
Canonical ubiquitous language.
Defines:
term
precise meaning
relationships
allowed usage
forbidden synonyms
Example:
ScoringRun
One execution of one ScoringPolicy against one canonical input snapshot.
Avoid:
- ScoringJob
- ScoreExecution
- RunJob
Rule:
One concept → one canonical term.
ARCHITECTURE.md
Defines:
layers
placement rules
dependency rules
naming grammar
test taxonomy
transaction ownership
configuration boundaries
error boundaries
composition rules
docs/specs/
Approved behavioral requirements.
Specs describe:
inputs
behavior
outputs
constraints
acceptance criteria
Specs must not redefine architecture.
docs/adr/
Only architectural decisions with meaningful trade-offs.
Use ADR when a choice is:
cross-cutting
non-obvious
expensive to reverse
architecturally significant
Do not create ADRs for routine implementation details.
docs/agents/
Configuration/integration for external agent skills.
It is not a project knowledge source of truth.
4. Architecture
Domain
domain/
Owns business truth.
Allowed:
entities
value objects
domain policies
domain events
domain errors
invariants
pure domain calculations
Forbidden dependencies:
FastAPI
SQLAlchemy
HTTPX
Postgres
Redis
environment variables
presentation
infrastructure
application orchestration
Application
application/
Owns system use cases.
use_cases/
application/use_cases/
Represents:
what the application does
Examples:
CreateScoringRun
ExecuteScoringRun
GetScoringRun
ReviewScoringRun
ports/inbound/
Stable contracts through which external actors may invoke application capabilities.
Conceptually:
outside
→ inbound port
→ use case
Python note:
An explicit inbound Protocol is optional when the use case itself already forms a clear callable application boundary.
Do not create an inbound abstraction purely to mirror an existing function without adding an invariant.
ports/outbound/
Capabilities that the application requires from the outside world.
Examples:
ScoringRunRepository
AIProvider
EventPublisher
Clock
IdGenerator
Conceptually:
use case
→ outbound port
→ adapter
Application does not know concrete technologies.
5. Infrastructure
infrastructure/
Owns technical implementations.
adapters/persistence/
Examples:
PostgresScoringRunRepository
SQLAlchemy models
database sessions
transaction implementation
adapters/clients/
Examples:
AIHubClient
OpenAIClient
CRMClient
adapters/messaging/
Examples:
OutboxPublisher
KafkaPublisher
WebhookDispatcher
config/
Examples:
environment settings
runtime configuration loading
secret references
Configuration contract:
pydantic-settings BaseSettings
→ the only backend configuration loader and validator
canonical variables
→ APP_ENV
→ LOG_LEVEL
→ DATABASE_URL
→ API_KEY_ENCRYPTION_KEY
Session tokens are stored only as digests. API keys retain a verifier for authentication and an encrypted copy for authorized re-display; the encryption key is a server-only secret kept outside the database. Do not add a session signing secret unless the selected implementation requires one.
DATABASE_URL
→ uses postgresql+psycopg://
infrastructure/config/
→ defines Settings
bootstrap/
→ loads validated Settings and constructs concrete dependencies
domain/ and application/
→ do not read environment variables directly
Required secrets and production connection settings have no insecure defaults. Missing or invalid configuration fails application startup. PostgreSQL container variables such as POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB configure the database image; DATABASE_URL is the canonical application connection setting.
6. Presentation
presentation/
Owns inbound transport.
http/
FastAPI routes
request schemas
response schemas
HTTP dependency wiring
HTTP error mapping
middleware
HTTP layer translates:
HTTP
→ application input
→ use case
→ application result
→ HTTP response
Operational endpoints:
/healthz
→ liveness probe
→ checks that the process responds
→ must not query the database
→ returns 200 when the app is alive
/readyz
→ readiness probe
→ checks critical dependencies such as PostgreSQL
→ returns 200 when the app can accept traffic
→ returns 503 when the app is not ready
Health responses must not expose secrets or infrastructure details. Health endpoints contain no business logic.
No business rules.
cli/
Command-line entry points.
consumers/
Message/event consumers.
7. Bootstrap
bootstrap/
Composition root.
Owns:
dependency construction
concrete adapter selection
application wiring
application startup
Rule:
Construction belongs at the edge.
Usage depends on abstractions.
Bootstrap may know every layer.
Inner layers may not know bootstrap.
Dependency Injection (DI)
Use manual dependency injection by default. Do not add a DI container without a demonstrated requirement.
bootstrap/wiring.py
→ constructs concrete adapters and application services
→ is the only composition root
use case
→ receives outbound ports through explicit parameters
presentation/http/
→ may use FastAPI Depends for transport-bound wiring only
→ must pass already-wired application services into the use case
domain/ and application/
→ must not import FastAPI or Depends
Application and domain code must not construct infrastructure dependencies. Tests replace concrete adapters at the composition root with fakes or test implementations. App-lifetime resources such as engines and clients are created once; transaction semantics remain owned by the application use case.
Application Factory and Lifespan
bootstrap/app.py
→ defines create_app(settings, dependencies)
→ creates and configures the FastAPI instance
→ registers routers, middleware, handlers, and lifespan
bootstrap/entrypoint.py
→ loads runtime Settings
→ calls create_app()
→ exposes app for the ASGI server
bootstrap/wiring.py
→ builds the concrete dependency graph
The application factory is the project application-construction pattern, not a DI container. It is used to create isolated development, test, and production application instances. FastAPI Depends remains limited to presentation/http transport wiring; application and domain code do not depend on it.
The lifespan manages application-lifetime resources such as database engines and external clients. It must not run database migrations or contain business rules. The production ASGI entrypoint is:
uvicorn src.<service>.bootstrap.entrypoint:app --host 0.0.0.0 --port 8000
8. Dependency Direction
Domain
▲
│
Application
▲ ▲
│ │
Infrastructure Presentation
\ /
Bootstrap
Hard rules:
Domain
X Application
X Infrastructure
X Presentation
Application
✓ Domain
X Infrastructure
X Presentation
Infrastructure
✓ Application
✓ Domain
X Presentation
Presentation
✓ Application
✓ Domain when mapping requires it
X Infrastructure concrete implementation
Bootstrap
✓ all layers
These rules should be machine-enforced.
9. Naming Model
Naming has three owners.
CONTEXT.md
→ vocabulary
ARCHITECTURE.md
→ grammar
PEP 8 + Ruff
→ spelling/style
Vocabulary
Domain decides canonical nouns.
Example:
ScoringRun
ScoringPolicy
EngineProfile
Rule
ScoreBand
Grammar
Domain type
ScoringRun
ScoringPolicy
Rule
Score
Avoid:
ScoringRunEntity
ScoringPolicyModel
RuleData
unless technical distinction genuinely requires it.
Use case
Verb + noun:
CreateScoringRun
ExecuteScoringRun
ReviewScoringRun
Outbound port
Capability/role:
ScoringRunRepository
AIProvider
EventPublisher
Adapter
Technology + role when useful:
PostgresScoringRunRepository
AIHubProvider
KafkaEventPublisher
Domain event
Past tense:
ScoringRunCreated
ScoringRunCompleted
PolicyRejected
Functions
Controlled verb grammar:
calculate_
compute_
derive_
validate_
normalize_
select_
build_
create_
load_
save_
find_
list_
run_
execute_
Meaning:
calculate / compute
→ deterministic calculation
derive
→ obtain information from known facts
validate
→ check validity
build
→ construct value/object
create
→ create resource/entity
load
→ external read
save
→ persistence
find/list
→ query
run/execute
→ workflow
Avoid generic names by default:
Manager
Helper
Utils
Common
Processor
Misc
BaseService
10. Test Taxonomy
tests/
├── unit/
├── integration/
├── functional/
└── architecture/
Unit
Tests pure domain/application behavior.
no network
no database
no filesystem unless explicitly part of unit
Examples:
rule evaluation
score calculation
policy validation
state transitions
Integration
Tests real technical boundaries.
Examples:
repository + PostgreSQL
Alembic + PostgreSQL
AI client + mocked HTTP transport
outbox + database
Prefer real production technology where practical.
Do not substitute SQLite for PostgreSQL when PostgreSQL semantics matter.
local integration tests
→ PostgreSQL provided by compose.yaml
CI integration tests
→ isolated PostgreSQL service container
Testcontainers
→ not a default dependency; add only when test isolation requirements justify it
Functional
Tests system behavior from an external boundary.
Examples:
HTTP request
→ FastAPI
→ application
→ persistence
→ HTTP response
Use HTTPX for FastAPI.
Architecture
Tests structural invariants.
Examples:
domain cannot import infrastructure
application cannot import presentation
FastAPI cannot appear in domain/application
SQLAlchemy cannot appear in domain
bootstrap is the only composition root
Use Import Linter plus custom AST/path checks where necessary.
11. Browser E2E
e2e/
Uses Playwright Test.
Purpose:
deterministic user-facing regression tests
Examples:
login
create lead
score lead
review result
E2E tests must be repeatable locally and in CI.
12. Agent Browser Verification
After automated checks pass, an agent may perform exploratory browser verification.
Preferred tooling:
Playwright CLI
or
agent-browser
Chrome DevTools
for deep diagnostics
Agent verification flow:
open application
→ execute affected user flow
→ inspect visible state
→ inspect console
→ inspect network failures
→ screenshot final state
→ collect trace when useful
→ report evidence
Artifacts:
.artifacts/verification/
├── screenshots/
├── traces/
└── videos/
Rule:
Browser agent = discover
Automated test = preserve
If exploratory verification discovers a reproducible bug:
reproduce
→ add deterministic regression test
→ fix
→ rerun verification
13. Static Quality Toolchain
Ruff
Owns:
formatting
linting
imports
dead code
common Python correctness checks
complexity guardrails
Ruff is not a test framework.
Pyright
Owns static type correctness.
Recommended:
strict or near-strict project configuration
Hard target:
Pyright errors = 0
Import Linter
Owns dependency topology.
Hard target:
architecture violations = 0
14. Quantitative Guardrails
Hard invariants:
Ruff errors = 0
Pyright errors = 0
Architecture violations = 0
Forbidden dependency edges = 0
Failed tests = 0
Initial complexity guardrails:
McCabe complexity <= 10
function arguments <= 5
branches <= 12
nested blocks <= 5
returns <= 6
statements/function <= 50
These values are guardrails, not design objectives.
Rule:
Do not refactor solely to game a metric.
A justified exception should remain explicit.
15. Coverage
Measure:
line coverage
branch coverage
Prefer branch coverage.
Coverage is evidence of exercised code, not proof of correctness.
Rule:
100% coverage is not a project objective by default.
Critical domain/application behavior matters more than raw percentage.
16. Hooks
Use:
.pre-commit-config.yaml
Compatible runners:
pre-commit
or
prek
Fast local feedback
On save / agent iteration:
ruff format
ruff check
pyright on affected code
Pre-commit
Run cheap deterministic checks:
ruff format --check
ruff check
syntax/config validation
secret checks
merge-conflict checks
cheap structure checks
Pre-push
Run:
pyright
lint-imports
unit tests
Hooks improve feedback latency.
Hooks are not authoritative because they can be bypassed.
17. CI
CI is the final enforcement layer.
CI provider
→ GitHub Actions
→ workflow lives in .github/workflows/ci.yml
→ workflow invokes canonical Makefile recipes
→ migration to Gitea Actions is a repository/deployment handoff; keep shared checks in Makefile
Ruff
↓
Pyright
↓
Import Linter
↓
Unit tests
↓
Integration tests
↓
Functional tests
↓
Architecture tests
↓
Coverage
↓
E2E when relevant
Required invariant:
local hook rules ⊆ CI rules
Anything important enough to block development locally must also be enforced in CI.
Canonical Delivery Workflow
Development
→ native backend and web processes
→ PostgreSQL from Docker Compose
→ Makefile provides the canonical commands
→ fast local feedback
Pull request CI
→ invokes the same Makefile recipes
→ backend quality checks
→ frontend quality checks
→ isolated PostgreSQL integration tests
→ `alembic check`
→ generate the Web API client from the current FastAPI schema and type-check Web once it consumes the API
→ relevant functional and architecture tests
Build
→ build `Dockerfile.backend`
→ build `apps/web/Dockerfile`
→ tag images with the Git commit SHA
→ test the built artifacts
Release
→ publish the immutable backend and web images
→ run `alembic upgrade head` once as a release step
→ deploy the same images
→ check `/healthz` and `/readyz`
→ run smoke tests and relevant E2E tests
Failure
→ stop the release or roll back to the previous image SHA
→ do not automatically downgrade production schema
build once
→ test that artifact
→ deploy that artifact
The deployment platform, registry, reverse proxy, and provider-specific manifests remain deferred until the target infrastructure is selected.
18. Rule Classification
Every architectural/project rule should be classified.
Hard invariant
Binary and mechanically checkable.
Examples:
domain must not import infrastructure
ruff errors = 0
pyright errors = 0
tests pass
These should block CI.
Guardrail
Quantitative smell indicator.
Examples:
complexity > 10
arguments > 5
function > 50 statements
May block or warn depending on project maturity.
Semantic guidance
Cannot reliably be reduced to a static rule.
Examples:
Does this policy belong in Domain?
Is this name faithful to the ubiquitous language?
Is this abstraction genuinely useful?
Enforce through:
CONTEXT.md
ARCHITECTURE.md
existing exemplars
review
agent reasoning
19. Rule Escalation
For every documented rule:
Can it be machine checked?
│
yes
↓
lint / test / CI
│
no
↓
documentation + exemplar
Do not leave mechanically enforceable architecture invariants only in Markdown.
20. Agent Workflow
Canonical implementation loop:
1. Understand task
2. Read:
- AGENTS.md
- relevant CONTEXT terms
- relevant ARCHITECTURE rules
- relevant spec
- relevant ADR
3. Locate canonical destination
4. Inspect an existing exemplar
5. Implement
6. Run:
- Ruff
- Pyright
- Import Linter
7. Run relevant tests
8. Run broader affected test suite
9. Run browser/E2E verification when applicable
10. Inspect:
- console
- network
- screenshots/traces when useful
11. Report:
- what changed
- tests/checks performed
- verification evidence
21. Placement Decision Grammar
Agent should classify, not design architecture.
Business truth?
→ domain/
Application workflow?
→ application/use_cases/
Stable inbound contract?
→ application/ports/inbound/
Capability application requires externally?
→ application/ports/outbound/
Database implementation?
→ infrastructure/adapters/persistence/
External API/service implementation?
→ infrastructure/adapters/clients/
Messaging implementation?
→ infrastructure/adapters/messaging/
Environment/runtime configuration?
→ infrastructure/config/
HTTP transport?
→ presentation/http/
CLI transport?
→ presentation/cli/
Message/event consumer?
→ presentation/consumers/
Dependency construction?
→ bootstrap/
There should be exactly one obvious answer.
22. Abstraction Rule
Code generation cost is cheap.
Understanding and verification cost are not.
Therefore:
Extra structure that buys semantic information
→ good
Extra abstraction that only buys indirection
→ reject
Good:
application/ports/outbound/scoring_run_repository.py
Bad:
core/services/base_manager.py
For every new abstraction ask:
From PATH + NAME can an agent determine:
1. what this code does?
2. which dependency directions are legal?
3. where another thing of the same kind belongs?
If not, reconsider the abstraction.
23. Documentation Duplication Rule
Canonical information must not be duplicated.
Domain definitions
→ CONTEXT.md only
Dependency topology
→ ARCHITECTURE.md only
Project instructions
→ AGENTS.md only
Historical decision rationale
→ ADR only
Behavioral requirement
→ spec only
Other documents may link to the canonical owner but should not restate it in detail.
24. Programming Model
The repository uses:
Functional Core
+
Object-Oriented Domain where identity/state matters
+
Imperative Shell at the edges
This is not a choice between functional programming and object-oriented programming.
Use this decision grammar:
stateless deterministic transformation
→ function
identity + invariant + lifecycle
→ object/entity
immutable domain concept
→ value object
rule spanning multiple concepts
→ pure domain policy/function
application workflow
→ use case
external state/effect
→ adapter
Functions are the default in Python.
Use classes only when they provide at least one meaningful property:
identity
encapsulated invariant
state/lifecycle
resource ownership
dependency grouping
polymorphic boundary
Example:
# Deterministic transformation
def calculate_score(evidence: Evidence, policy: ScoringPolicy) -> Score:
...
# Identity, lifecycle, and invariant
class ScoringRun:
def complete(self, score: Score) -> None:
if self.status is not RunStatus.RUNNING:
raise InvalidRunTransition(...)
...
# Rule spanning multiple concepts
def requires_review(
score: Score,
confidence: Confidence,
policy: ReviewPolicy,
) -> bool:
...
Do not introduce classes solely as namespaces.
Avoid:
class ScoreCalculator:
def calculate(self, evidence: Evidence, policy: ScoringPolicy) -> Score:
...
Prefer:
def calculate_score(evidence: Evidence, policy: ScoringPolicy) -> Score:
...
Functional Core / Imperative Shell
PURE CORE
domain/
deterministic
explicit input/output
no I/O
no clock access
no global randomness
no environment access
ORCHESTRATION
application/
coordinates domain logic
requests effects through ports
EFFECTFUL SHELL
infrastructure/
presentation/
bootstrap/
database
HTTP
filesystem
queues
environment
clock implementation
randomness implementation
Rules:
Prefer functions for stateless deterministic behavior.
Use objects for identity, invariants, lifecycle, state,
resource ownership, or meaningful polymorphism.
Prefer immutable data.
Keep side effects at explicit boundaries.
Prefer composition over inheritance.
Do not introduce classes solely as namespaces.
25. Model Boundaries
Models belong to their architectural boundary.
HTTP schema
≠ Application command/result
≠ Domain model
≠ ORM model
Canonical flow:
RequestSchema
↓ mapping
Command
↓
UseCase
↓
Domain
↓
Repository Port
↓
Persistence Mapper
↓
ORM Model
Rules:
Presentation maps transport data to/from application data.
Application maps workflow inputs/results to/from domain concepts when needed.
Infrastructure maps domain data to/from persistence or external-service models.
FastAPI, Pydantic, SQLAlchemy, and provider-specific models must not leak across their owning boundaries.
A model may be reused across a boundary only when it is already owned by the inner layer and introduces no framework or transport coupling.
26. State and Mutation
Prefer immutable values.
Mutation is allowed only where state or lifecycle is part of the concept.
Example immutable value object:
@dataclass(frozen=True)
class Score:
value: int
A lifecycle-bearing entity such as ScoringRun may be stateful, but state changes must:
occur through explicit operations
preserve invariants
reject invalid transitions
remain observable in tests
Never mutate shared state implicitly.
Do not use module-level mutable state for domain behavior or application workflow state.
27. Composition and Inheritance
Default:
composition > inheritance
Avoid generic inheritance structures such as:
BaseService
BaseManager
AbstractRepositoryBase
BaseUseCase
Inheritance is justified only when there is a genuine substitutable abstraction with stable shared semantics.
For outbound ports, prefer Python Protocol over an inheritance hierarchy created only for code reuse.
Use composition to share implementation behavior.
28. Effects and Reliability Semantics
Effects must be explicit and owned by the correct layer.
Transaction ownership
Use case owns transaction semantics.
Infrastructure provides the transaction mechanism.
A transaction boundary should normally align with one application use case.
Domain code must not open, commit, or roll back transactions.
Time and randomness
Domain and application behavior must not read global time or randomness directly when those values affect behavior.
Use explicit inputs or outbound ports such as:
Clock
IdGenerator
RandomSource
Infrastructure provides their runtime implementations.
External calls
External calls belong behind outbound ports and infrastructure adapters.
Every external operation must define the applicable semantics:
timeout
retry
idempotency
cancellation
error mapping
Rules:
Timeouts are explicit.
Retries are bounded and live at the technical boundary.
Business logic does not contain transport retries.
Retryable operations must be idempotent or carry an idempotency strategy.
Cancellation must propagate where the runtime supports it.
Provider errors are translated before crossing into application/domain code.
Do not spread retry or transaction logic across domain behavior.
29. Exceptions and Waivers
A rule suppression is an explicit, local exception—not an unnamed escape hatch.
Every suppression must:
target a specific rule
use the narrowest possible scope
include rationale when the reason is not obvious
remain visible to review and tooling
Avoid anonymous suppressions such as:
# noqa
Prefer a targeted suppression:
result = legacy_call() # noqa: S307 -- trusted migration input; remove after ADR-012
Use the tool-specific rule code where supported.
An architectural exception that changes dependency direction, layer ownership, or another cross-cutting invariant requires an ADR. It must not be hidden with # noqa, type: ignore, excluded paths, or disabled checks.
30. Web Architecture
The web frontend is a separate SvelteKit application:
apps/web/
It is not placed inside the backend presentation/ layer.
The backend owns business capabilities and HTTP APIs. The web application owns user-facing composition, interaction, navigation, and presentation state.
Production runtime:
@sveltejs/adapter-node
→ builds the SvelteKit application as a Node.js server
pnpm build
→ creates the production output in `build/`
node build
→ runs the production web server
adapter-node is the default production adapter for the Docker web image. adapter-auto and adapter-static are not production defaults for this repository.
Runtime binding:
containerized backend and web
→ bind to 0.0.0.0
native local process
→ bind to 127.0.0.1 by default
→ use 0.0.0.0 when another container or host needs access
backend port
→ 8000
web port
→ 3000
PostgreSQL port
→ 5432
HOST controls the network interfaces on which a process listens; it is not a public domain name. Port values use these defaults unless a deployment target requires an explicit mapping.
Web runtime configuration:
ORIGIN
→ public origin used by SvelteKit for request/origin validation
→ local default: http://localhost:3000
→ production value is injected by the deployment environment
$env/dynamic/private
→ runtime server-only values such as BACKEND_INTERNAL_URL and secrets
→ may be imported only by server-side modules
$env/dynamic/public
→ runtime values safe to expose to the browser
→ only variables with the PUBLIC_ prefix
$env/static/*
→ reserved for values genuinely invariant across deployments
The web server calls the backend through BACKEND_INTERNAL_URL from server-only code by default. Do not expose backend credentials or internal service addresses through PUBLIC_*. Build images without environment-specific secrets or URLs; inject runtime configuration when each container starts.
Use official SvelteKit conventions as architectural signals:
src/routes/
→ routes, layouts, navigation, and page boundaries
src/lib/
→ reusable web application code
src/lib/server/
→ secrets, privileged I/O, sessions, and server-only backend communication
hooks.server.ts
→ request lifecycle concerns
Do not replace these conventions with framework vocabulary such as:
pages/
views/
controllers/
Do not mirror the backend's Clean Architecture directory structure inside the web application.
The web structure is:
apps/web/src/
├── routes/
├── lib/
│ ├── components/
│ │ ├── ui/
│ │ └── shared/
│ ├── features/
│ ├── server/
│ ├── styles/
│ └── utils/
├── hooks.server.ts
└── app.html
Rules:
route
→ composition, navigation, and data boundary
feature
→ user capability and product-facing behavior
component
→ rendering and interaction
server module
→ secrets, privileged I/O, session handling,
and backend communication
Route-specific components may remain colocated with the route. Move code into $lib only when it is reused or belongs to a stable feature/component category.
Avoid large route components that combine:
API calls
business rules
state synchronization
validation
markup
styling
Business truth remains in the backend Domain/Application layers. The web may implement presentation rules and immediate input feedback, but it must not independently redefine authoritative business rules.
31. Web Programming and State Model
Use Svelte 5 runes for new reactive code.
State ownership follows this order:
URL state
→ shareable, bookmarkable, or navigation state
server/page data
→ authoritative remote state
local $state
→ component-local mutable UI state
$derived
→ deterministic computed state
context
→ state shared within one bounded component subtree
global state
→ last resort
Rules:
derived value
→ $derived
external side effect
→ $effect
ordinary application-state synchronization
→ not $effect
Keep state as close as possible to its owner.
Do not copy server data into local state unless the user is editing a draft or the local copy has another explicit lifecycle.
Do not introduce a global state library until URL state, page data, local state, and context have been shown insufficient.
Every user-facing flow must define:
loading
empty
error
success
Interactive state must remain keyboard-accessible and observable in tests.
32. Web Data, Forms, and Server Boundaries
Use SvelteKit's built-in mechanisms before adding data or form libraries.
Default decision grammar:
route/page data
→ SvelteKit load
mutating form
→ SvelteKit form action
progressive enhancement
→ enhance the native form
privileged operation
→ server-only module
experimental remote function
→ explicit project decision, not a per-task choice
Form actions are the stable default for mutations.
Remote functions may be adopted when the project explicitly accepts their current stability and standardizes their use. Agents must not mix remote functions, direct fetch, custom wrappers, and third-party query libraries without a documented reason.
Rules:
Secrets never enter client code.
Privileged backend calls run in server-only modules.
Native forms remain the baseline where practical.
Validation at the trust boundary remains authoritative.
Client validation improves feedback but does not replace server validation.
Do not introduce TanStack Query, a custom API abstraction, or a form framework without demonstrated requirements that SvelteKit does not satisfy.
33. Backend-Web Contracts
Cross-application contracts must have one source of truth.
Canonical HTTP contract flow:
FastAPI schema
↓
OpenAPI
↓
generated TypeScript types/client
↓
SvelteKit web application
Hard invariant:
The web application must not manually redefine backend DTOs
when the backend publishes those DTOs through OpenAPI.
FastAPI/Pydantic remains the only source of HTTP contract definitions. Once Web consumes an API, a Makefile recipe exports OpenAPI from the current backend and generates TypeScript types/client into apps/web/src/lib/generated/; neither output is committed or manually edited. Local setup, CI, and the Web image build run that recipe before Web type-check/build. A generated client transports the backend contract into Web; Web feature models own only presentation-specific state. CI verifies compatibility by generating from the current backend and type-checking/building Web, not by comparing checked-in generated files.
Do not create parallel contract packs, standalone copies of the same schemas, or fixture/manifest catalogs without an actual consumer requirement. Event contracts require explicit ownership and compatibility rules only when events are consumed across applications.
HTTP errors
Use conventional HTTP statuses (401 unauthenticated, 403 forbidden, 404 not found, 409 conflict, 422 invalid input, 500 unexpected failure) and FastAPI's default error responses in v1. Domain and application errors remain HTTP-independent; presentation/http/ maps expected errors to safe HTTP responses. Unexpected failures return a generic 500 without stack traces or secrets; details go to operational logs. Add a custom error envelope or stable business error codes only when a real client needs to distinguish cases that status alone cannot express.
Authentication and machine-to-machine access
The v1 application has two caller types with separate credentials:
Human administrators
→ email/password login
→ FastAPI is the authentication and authorization authority
→ one role: admin; multiple admin accounts are allowed
→ no public self-registration; initial accounts are provisioned through `make admin-create`
→ password hashes use `pwdlib[argon2]` with Argon2id
→ server-side opaque session in PostgreSQL; generate a high-entropy token with Python `secrets`, store only its SHA-256 digest
→ session lifetime: 8 hours absolute; logout revokes the session; no sliding refresh in v1
→ cookie attributes: HttpOnly, SameSite=Lax, Path=/, host-only (omit Domain), Secure in production
SvelteKit Admin Web
→ acts as the server-side frontend boundary
→ forwards the session to FastAPI for protected operations
→ does not make final authorization decisions
External applications (CRM, workers, automation)
→ call FastAPI directly using X-API-Key
→ each key identifies an application/client
→ keys cannot access admin, account, or key-management endpoints
→ admins can reveal and copy an existing key again, including after returning to the page
→ store a verifier/hash for authentication and an encrypted copy for authorized re-display
→ keys can be revoked or rotated
FastAPI validates the session and authorization on every protected admin operation. API-key authentication is separate from admin sessions; FastAPI validates each key and authorizes access to machine-facing AI endpoints. Do not trust a user ID or role supplied by the browser or an unverified internal header.
V1 uses a minimal FastAPI-owned email/password and database-session flow: pwdlib[argon2] handles password hashing; Python secrets generates opaque session tokens; only session token digests are stored. SQLAlchemy models and Alembic own user, session, and API-key metadata. API-key management in Admin Web supports create, list metadata, reveal/copy an existing key on demand, and revoke; normal key authentication uses the verifier, not decryption. Encrypt the retrievable copy with a vetted library and a server-only API_KEY_ENCRYPTION_KEY kept outside the database; never expose secrets in list responses or logs, and mark reveal responses Cache-Control: no-store. Only an authenticated admin may request a key reveal. Do not add JWT, Redis, an authentication framework, or an external auth service by default. Do not implement cryptographic primitives manually. Add detailed API-key scopes only when distinct client permissions are required.
Tests must cover valid/invalid login, expired/revoked sessions, protected admin access, cookie attributes, CSRF rejection for cross-origin mutations, and API-key creation, admin-only re-display, and revocation.
Company email is a login identifier, not SSO. OIDC/SSO is deferred until the company provides an identity provider or requires centralized sign-in. Email-based verification/reset flows require an email service and are not part of the initial default.
Browser-to-backend topology:
Browser
→ calls the same-origin SvelteKit server
SvelteKit server
→ calls FastAPI server-to-server through BACKEND_INTERNAL_URL
External applications
→ call FastAPI directly with X-API-Key
Session handoff: FastAPI owns credential verification, opaque token issuance, session validation, and revocation. SvelteKit uses server-side form actions and its built-in cookie API to set the browser's host-only session cookie after a server-to-server login and delete it on logout; the raw token must not appear in browser-facing JSON or logs. For protected backend calls, SvelteKit forwards only the session cookie to FastAPI, which validates it independently. Do not assume cross-domain server-side fetch forwards cookies automatically or blindly forward all browser headers. Test login → authenticated request → logout across both servers, including cookie attributes and revocation. Keep cookie-authenticated mutation endpoints behind the SvelteKit boundary; any browser-reachable FastAPI mutation that accepts cookies must enforce its own CSRF protection.
The default browser flow does not configure CORS because the browser does not call FastAPI directly. Server-to-server calls and non-browser API clients are not governed by browser CORS. If a future requirement makes browsers call FastAPI across origins, configure an exact trusted-origin allowlist; never use a wildcard origin with credentials.
Browser-initiated mutations use SvelteKit form actions by default. Keep SvelteKit's built-in Origin-based CSRF check enabled; configure ORIGIN to the public web origin when it cannot be inferred reliably. Do not rely on SameSite=Lax alone. Any custom browser-facing mutation endpoint outside form actions must validate the trusted Origin or use a CSRF token. Tests must verify that cross-origin mutations are rejected.
V1 audit records live in PostgreSQL and cover important admin actions only: login attempts, configuration changes, and API-key creation, reveal, and revocation. Record the actor when known, action, target, timestamp, and outcome; never record passwords or key secrets. Successful state changes and their audit record share a database transaction where practical. Keep technical diagnostics in Python logging to stdout, separate from audit; do not add an audit framework or centralized logging service by default.
AI job history is a future concern, separate from administrator audit events and operational logs. PostgreSQL is a possible starting point, not a committed ai_jobs table or schema; define what to record, how long to retain it, and how to display it when AI job requirements are concrete.
34. Web UI System
The web UI uses one fixed foundation:
SvelteKit
├── Svelte 5
├── Tailwind CSS v4
├── Bits UI
├── shadcn-svelte
├── Lucide Svelte
└── native Svelte transitions, animate, and svelte/motion
Responsibilities:
Tailwind CSS v4
→ styling and executable design tokens
Bits UI
→ accessible interaction primitives and behavior
shadcn-svelte
→ project-owned component implementations
Lucide Svelte
→ canonical icon family
Svelte transition / animate / svelte/motion
→ UI motion
Do not mix multiple component systems or icon families.
Component ownership
components/ui/
→ domain-free UI primitives
components/shared/
→ reusable compositions without one feature owner
features/<feature>/
→ product semantics and user capabilities
Examples:
Button
Input
Dialog
Table
Badge
FormField
→ components/ui/
ScoreBadge
ScoringRunTable
PolicyEditor
ReviewPanel
→ features/scoring/ or features/review/
Hard rule:
ui/
→ knows no business domain
features/
→ may compose ui/ with product semantics
Do not use a generic components/ directory as an unclassified component store.
Basic component foundation
The initial UI foundation should cover these primitives before feature-specific components:
Button, Link, IconButton
Input, Textarea, Select
Checkbox, Radio, Switch
Label, FormField, HelperText, ErrorMessage
Card, Badge, Alert
Dialog, Dropdown, Popover, Tooltip, Tabs
Table, Pagination
Skeleton, EmptyState
This is a foundation, not a requirement to build every component upfront. Add a primitive when a real feature needs it; do not create speculative components.
Each interactive component must define:
semantic HTML and component anatomy
variants and size scale
default, hover, focus, active, disabled, loading, and error states
keyboard behavior and visible focus
accessible name, description, and validation feedback
responsive behavior and long-content handling
Prefer native HTML for simple behavior. Use Bits UI for complex accessible interactions, and keep project-owned styling and composition in components/ui/.
Design tokens
Design tokens are semantic and executable:
color
typography
spacing
radius
shadow
breakpoints
z-index
motion duration
easing
Prefer semantic tokens:
background
foreground
surface
primary
muted
destructive
border
focus-ring
Avoid hard-coded arbitrary colors, radii, spacing, and z-index values when an existing token expresses the intent.
Changing a token or primitive should consistently update every consumer.
Motion
Use the smallest native mechanism that fits:
CSS transition
→ simple hover, focus, color, and transform feedback
Svelte transition
→ enter and exit
animate:flip
→ list reordering
svelte/motion Spring or Tween
→ value-based physical or timed motion
Do not add another animation framework without a demonstrated requirement.
Motion must communicate feedback, hierarchy, or state change. It must respect prefers-reduced-motion.
35. Web Accessibility and UX Invariants
Svelte accessibility warnings are zero by default.
A suppression must follow the repository waiver rules and target a specific warning.
Required invariants:
semantic HTML is preferred
interactive controls support keyboard operation
form controls have accessible labels
focus is visible
focus behavior is correct for dialogs and menus
color contrast meets the project accessibility target
icon-only controls have accessible names
reduced-motion preferences are respected
Do not recreate complex interactive primitives when Bits UI already provides their accessibility behavior.
Every component and feature must account for:
long content
small viewport
keyboard navigation
disabled state
loading state
empty state
error state
Accessibility is part of implementation correctness, not a final visual review step.
36. Web Verification and Dependency Policy
Frontend verification stack:
Prettier
→ formatting
ESLint
→ JavaScript/TypeScript linting
svelte-check
→ Svelte and TypeScript static analysis
Vitest
→ unit and component behavior
Playwright
→ browser regression and complete user flows
Playwright CLI or agent-browser
→ exploratory agent verification
Suggested verification flow:
format
→ lint
→ svelte-check
→ unit/component tests
→ build
→ Playwright when user-facing behavior changes
→ exploratory browser verification
Hard targets:
TypeScript errors = 0
svelte-check errors = 0
ESLint errors = 0
Svelte accessibility warnings = 0
failed web tests = 0
Vitest should cover meaningful primitive states. Playwright should preserve critical assembled flows rather than duplicate every component test.
Dependency rule
The approved stack is the default answer.
A new frontend dependency is allowed only when:
the platform does not already provide the capability
Svelte/SvelteKit does not already provide the capability
an installed dependency does not already provide the capability
the requirement is demonstrated, not speculative
maintenance and compatibility cost are acceptable
Before adding a dependency, record:
capability required
existing options considered
reason the approved stack is insufficient
ownership and removal implications
A routine dependency does not require an ADR. A dependency that changes the programming model, state model, contract model, component system, or build architecture does.
Web boundary concerns
The web architecture must explicitly account for:
secret handling
sensitive logging
responsive behavior
performance budgets
build and deployment target
Request/correlation IDs are deferred; add them only if tracing failures across Web and FastAPI logs becomes difficult. The authentication/session and caller model is defined in # 33. Backend-Web Contracts. Remaining browser/security and operational concerns must be assigned to canonical documents and enforcement mechanisms, not left as per-feature decisions.
37. Central Principle
Canonical vocabulary
×
explicit repository rules
×
filesystem structure
×
mechanical enforcement
×
self-verification
The filesystem acts as semantic metadata.
The documentation acts as explicit project knowledge.
The tooling acts as executable constraints.
The tests verify behavior.
The browser gives the agent runtime visibility.
Together they form an agent-native development environment in which:
the agent spends reasoning on the problem,
not on rediscovering how the repository works.