dotb_ai-platform/rewrite_architect.md

55 KiB
Raw Blame History

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:

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.