dotb_ai-platform/rewrite_architect.md

2683 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Strict AI-Native Repository Profile
> **SSOT:** This document is the canonical architecture and repository profile. `runtime-research.md` is 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`)
- Password hashing: `pwdlib[argon2]` using Argon2id
- Database: PostgreSQL
- Data access: SQLAlchemy 2.x async engine and ORM
- Database migrations: Alembic
- PostgreSQL driver: `psycopg` 3 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:
```text
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:
```text
naming grammar
test taxonomy
verification workflow
```
Repository phải làm cho:
```text
valid decisions → obvious
invalid decisions → mechanically difficult
```
---
# 2. Repository Structure
```text
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:
```text
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:
```text
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:
```text
→ 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:
```text
.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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
Follow AGENTS.md for repository-wide instructions.
```
Do not duplicate architecture or domain rules.
## README.md
Contains:
```text
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:
```text
term
precise meaning
relationships
allowed usage
forbidden synonyms
```
Example:
```text
ScoringRun
One execution of one ScoringPolicy against one canonical input snapshot.
Avoid:
- ScoringJob
- ScoreExecution
- RunJob
```
Rule:
```text
One concept → one canonical term.
```
## ARCHITECTURE.md
Defines:
```text
layers
placement rules
dependency rules
naming grammar
test taxonomy
transaction ownership
configuration boundaries
error boundaries
composition rules
```
## docs/specs/
Approved behavioral requirements.
Specs describe:
```text
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:
```text
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
```text
domain/
```
Owns business truth.
Allowed:
```text
entities
value objects
domain policies
domain events
domain errors
invariants
pure domain calculations
```
Forbidden dependencies:
```text
FastAPI
SQLAlchemy
HTTPX
Postgres
Redis
environment variables
presentation
infrastructure
application orchestration
```
---
## Application
```text
application/
```
Owns system use cases.
### use_cases/
```text
application/use_cases/
```
Represents:
```text
what the application does
```
Examples:
```text
CreateScoringRun
ExecuteScoringRun
GetScoringRun
ReviewScoringRun
```
### ports/inbound/
Stable contracts through which external actors may invoke application capabilities.
Conceptually:
```text
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:
```text
ScoringRunRepository
AIProvider
EventPublisher
Clock
IdGenerator
```
Conceptually:
```text
use case
→ outbound port
→ adapter
```
Application does not know concrete technologies.
---
# 5. Infrastructure
```text
infrastructure/
```
Owns technical implementations.
## adapters/persistence/
Examples:
```text
PostgresScoringRunRepository
SQLAlchemy models
database sessions
transaction implementation
```
## adapters/clients/
Examples:
```text
AIHubClient
OpenAIClient
CRMClient
```
## adapters/messaging/
Examples:
```text
OutboxPublisher
KafkaPublisher
WebhookDispatcher
```
## config/
Examples:
```text
environment settings
runtime configuration loading
secret references
```
Configuration contract:
```text
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
```text
presentation/
```
Owns inbound transport.
## http/
```text
FastAPI routes
request schemas
response schemas
HTTP dependency wiring
HTTP error mapping
middleware
```
HTTP layer translates:
```text
HTTP
→ application input
→ use case
→ application result
→ HTTP response
```
Operational endpoints:
```text
/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
```text
bootstrap/
```
Composition root.
Owns:
```text
dependency construction
concrete adapter selection
application wiring
application startup
```
Rule:
```text
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.
```text
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
```text
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:
```bash
uvicorn src.<service>.bootstrap.entrypoint:app --host 0.0.0.0 --port 8000
```
---
# 8. Dependency Direction
```text
Domain
▲
│
Application
▲ ▲
│ │
Infrastructure Presentation
\ /
Bootstrap
```
Hard rules:
```text
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.
```text
CONTEXT.md
→ vocabulary
ARCHITECTURE.md
→ grammar
PEP 8 + Ruff
→ spelling/style
```
## Vocabulary
Domain decides canonical nouns.
Example:
```text
ScoringRun
ScoringPolicy
EngineProfile
Rule
ScoreBand
```
## Grammar
### Domain type
```text
ScoringRun
ScoringPolicy
Rule
Score
```
Avoid:
```text
ScoringRunEntity
ScoringPolicyModel
RuleData
```
unless technical distinction genuinely requires it.
### Use case
Verb + noun:
```text
CreateScoringRun
ExecuteScoringRun
ReviewScoringRun
```
### Outbound port
Capability/role:
```text
ScoringRunRepository
AIProvider
EventPublisher
```
### Adapter
Technology + role when useful:
```text
PostgresScoringRunRepository
AIHubProvider
KafkaEventPublisher
```
### Domain event
Past tense:
```text
ScoringRunCreated
ScoringRunCompleted
PolicyRejected
```
### Functions
Controlled verb grammar:
```text
calculate_
compute_
derive_
validate_
normalize_
select_
build_
create_
load_
save_
find_
list_
run_
execute_
```
Meaning:
```text
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:
```text
Manager
Helper
Utils
Common
Processor
Misc
BaseService
```
---
# 10. Test Taxonomy
```text
tests/
├── unit/
├── integration/
├── functional/
└── architecture/
```
## Unit
Tests pure domain/application behavior.
```text
no network
no database
no filesystem unless explicitly part of unit
```
Examples:
```text
rule evaluation
score calculation
policy validation
state transitions
```
## Integration
Tests real technical boundaries.
Examples:
```text
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.
```text
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:
```text
HTTP request
→ FastAPI
→ application
→ persistence
→ HTTP response
```
Use HTTPX for FastAPI.
## Architecture
Tests structural invariants.
Examples:
```text
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
```text
e2e/
```
Uses Playwright Test.
Purpose:
```text
deterministic user-facing regression tests
```
Examples:
```text
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:
```text
Playwright CLI
or
agent-browser
Chrome DevTools
for deep diagnostics
```
Agent verification flow:
```text
open application
→ execute affected user flow
→ inspect visible state
→ inspect console
→ inspect network failures
→ screenshot final state
→ collect trace when useful
→ report evidence
```
Artifacts:
```text
.artifacts/verification/
├── screenshots/
├── traces/
└── videos/
```
Rule:
```text
Browser agent = discover
Automated test = preserve
```
If exploratory verification discovers a reproducible bug:
```text
reproduce
→ add deterministic regression test
→ fix
→ rerun verification
```
---
# 13. Static Quality Toolchain
## Ruff
Owns:
```text
formatting
linting
imports
dead code
common Python correctness checks
complexity guardrails
```
Ruff is not a test framework.
## Pyright
Owns static type correctness.
Recommended:
```text
strict or near-strict project configuration
```
Hard target:
```text
Pyright errors = 0
```
## Import Linter
Owns dependency topology.
Hard target:
```text
architecture violations = 0
```
---
# 14. Quantitative Guardrails
Hard invariants:
```text
Ruff errors = 0
Pyright errors = 0
Architecture violations = 0
Forbidden dependency edges = 0
Failed tests = 0
```
Initial complexity guardrails:
```text
McCabe complexity <= 10
function arguments <= 5
branches <= 12
nested blocks <= 5
returns <= 6
statements/function <= 50
```
These values are guardrails, not design objectives.
Rule:
```text
Do not refactor solely to game a metric.
```
A justified exception should remain explicit.
---
# 15. Coverage
Measure:
```text
line coverage
branch coverage
```
Prefer branch coverage.
Coverage is evidence of exercised code, not proof of correctness.
Rule:
```text
100% coverage is not a project objective by default.
```
Critical domain/application behavior matters more than raw percentage.
---
# 16. Hooks
Use:
```text
.pre-commit-config.yaml
```
Compatible runners:
```text
pre-commit
or
prek
```
## Fast local feedback
On save / agent iteration:
```text
ruff format
ruff check
pyright on affected code
```
## Pre-commit
Run cheap deterministic checks:
```text
ruff format --check
ruff check
syntax/config validation
secret checks
merge-conflict checks
cheap structure checks
```
## Pre-push
Run:
```text
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.
```text
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
```
```text
Ruff
↓
Pyright
↓
Import Linter
↓
Unit tests
↓
Integration tests
↓
Functional tests
↓
Architecture tests
↓
Coverage
↓
E2E when relevant
```
Required invariant:
```text
local hook rules ⊆ CI rules
```
Anything important enough to block development locally must also be enforced in CI.
## Canonical Delivery Workflow
```text
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
```
```text
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:
```text
domain must not import infrastructure
ruff errors = 0
pyright errors = 0
tests pass
```
These should block CI.
## Guardrail
Quantitative smell indicator.
Examples:
```text
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:
```text
Does this policy belong in Domain?
Is this name faithful to the ubiquitous language?
Is this abstraction genuinely useful?
```
Enforce through:
```text
CONTEXT.md
ARCHITECTURE.md
existing exemplars
review
agent reasoning
```
---
# 19. Rule Escalation
For every documented rule:
```text
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:
```text
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.
```text
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:
```text
Extra structure that buys semantic information
→ good
Extra abstraction that only buys indirection
→ reject
```
Good:
```text
application/ports/outbound/scoring_run_repository.py
```
Bad:
```text
core/services/base_manager.py
```
For every new abstraction ask:
```text
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.
```text
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:
```text
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:
```text
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:
```text
identity
encapsulated invariant
state/lifecycle
resource ownership
dependency grouping
polymorphic boundary
```
Example:
```python
# 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:
```python
class ScoreCalculator:
def calculate(self, evidence: Evidence, policy: ScoringPolicy) -> Score:
...
```
Prefer:
```python
def calculate_score(evidence: Evidence, policy: ScoringPolicy) -> Score:
...
```
## Functional Core / Imperative Shell
```text
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:
```text
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.
```text
HTTP schema
≠ Application command/result
≠ Domain model
≠ ORM model
```
Canonical flow:
```text
RequestSchema
↓ mapping
Command
↓
UseCase
↓
Domain
↓
Repository Port
↓
Persistence Mapper
↓
ORM Model
```
Rules:
```text
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:
```python
@dataclass(frozen=True)
class Score:
value: int
```
A lifecycle-bearing entity such as `ScoringRun` may be stateful, but state changes must:
```text
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:
```text
composition > inheritance
```
Avoid generic inheritance structures such as:
```text
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
```text
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:
```text
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:
```text
timeout
retry
idempotency
cancellation
error mapping
```
Rules:
```text
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:
```text
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:
```python
# noqa
```
Prefer a targeted suppression:
```python
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:
```text
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:
```text
@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:
```text
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:
```text
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:
```text
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:
```text
pages/
views/
controllers/
```
Do not mirror the backend's Clean Architecture directory structure inside the web application.
The web structure is:
```text
apps/web/src/
├── routes/
├── lib/
│ ├── components/
│ │ ├── ui/
│ │ └── shared/
│ ├── features/
│ ├── server/
│ ├── styles/
│ └── utils/
├── hooks.server.ts
└── app.html
```
Rules:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
FastAPI schema
↓
OpenAPI
↓
generated TypeScript types/client
↓
SvelteKit web application
```
Hard invariant:
```text
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:
```text
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:
```text
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:
```text
SvelteKit
├── Svelte 5
├── Tailwind CSS v4
├── Bits UI
├── shadcn-svelte
├── Lucide Svelte
└── native Svelte transitions, animate, and svelte/motion
```
Responsibilities:
```text
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
```text
components/ui/
→ domain-free UI primitives
components/shared/
→ reusable compositions without one feature owner
features/<feature>/
→ product semantics and user capabilities
```
Examples:
```text
Button
Input
Dialog
Table
Badge
FormField
→ components/ui/
ScoreBadge
ScoringRunTable
PolicyEditor
ReviewPanel
→ features/scoring/ or features/review/
```
Hard rule:
```text
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:
```text
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:
```text
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:
```text
color
typography
spacing
radius
shadow
breakpoints
z-index
motion duration
easing
```
Prefer semantic tokens:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
format
→ lint
→ svelte-check
→ unit/component tests
→ build
→ Playwright when user-facing behavior changes
→ exploratory browser verification
```
Hard targets:
```text
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:
```text
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:
```text
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:
```text
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
```text
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:
```text
the agent spends reasoning on the problem,
not on rediscovering how the repository works.
```