Agents¶
About this document
Audience: agentic coding tools (Claude Code, Codex, Kiro, Cursor) and the humans guiding them.
Purpose: the single entry point for any agent working in this repository.
Read order: read this file first, every time you start a new task. Then load only the documents this file points you to.
1. How to use this repository¶
This is a specification-first project. Every architectural and product decision is documented before code is written. Your job as an agent is to:
- Find the requirement. Every task should map to one or more requirement IDs (
R-XXX-NNNorN-XXX-NNN) fromREQUIREMENTS.md. If you can't map a task to a requirement, stop and ask. - Read the relevant docs. Use the routing table in §3 to load only what you need.
- Match the conventions. The conventions in this file and the layer rules in
ARCHITECTURE.mdare not suggestions. - Implement narrowly. Don't change unrelated files. Don't refactor things you weren't asked to refactor.
- Surface uncertainty. When a decision is ambiguous, note it and propose options rather than picking silently.
2. Documents in this repository¶
| File | One-line purpose |
|---|---|
| Vision | What this product is, who it's for, the principles behind tradeoffs. |
| Requirements | Authoritative list of functional + non-functional requirements with stable IDs. |
| Architecture | System design: bounded contexts, communication patterns, deployment shape. |
| Data Model | Database schemas, tables, columns, indexes, constraints. |
| API Conventions | REST conventions, error format, idempotency, optimistic locking, endpoint catalogue. |
| Roadmap | Sequenced milestones from MVP through deferred features. |
| Agents | This file. Routing and conventions for agents. |
| Glossary | Definitions of domain terms used across documents and code. |
3. Routing: which document covers which question¶
Load only what you need; context is not free.
| Question | Read |
|---|---|
| Why a feature exists / is in scope / is deferred | Vision, Requirements |
| Build order | Roadmap |
| Domain term definition | Glossary |
| System structure, context ownership, cross-context comms, layer rules, folder layout | Architecture (§1–7) |
| Auth, sessions, roles, permissions | Architecture §8, API Conventions §9–10 |
| Config, observability, reliability, extension points, key decisions | Architecture §11–15 |
| Database schema, money/ID/timestamp/soft-delete conventions, cross-schema refs | Data Model |
| URL design, request/response shape, pagination, idempotency, errors, endpoint catalogue | API Conventions |
| Where a transaction lives (regular vs investment) | Data Model §4.2 + §5.2 |
If your question doesn't fit, read Architecture and Requirements, then ask.
4. Hard rules (do not violate)¶
These rules are non-negotiable. Violating them is always a bug. If a rule conflicts with a task you've been given, stop and ask the human.
4.1 Architectural¶
- Domain layer has no infrastructure imports. No DB clients, HTTP libraries, loggers, ORMs, framework decorators. Pure TypeScript with standard library and small pure utilities only.
- Repository interfaces live in
domain/. Implementations live ininfrastructure/. - No cross-context database access. Each context owns its schema. No FK constraints between schemas. Cross-schema references are validated at the application layer.
- No direct cross-context internal calls. Cross-context interactions go through events or through a context's deliberately public read API exposed in
application/. - HTTP route handlers are thin. Parse request, call use case, format response. No business logic.
Logger,Clock,EventPublisher,IdGeneratorare injected ports. Never useconsole.log,new Date(), or static event dispatch in domain or application code.
4.2 Data¶
- Money is always
BIGINTminor units + currency code. NeverDECIMAL, neverFLOAT, neverNUMBER. In JSON, money is{ "minor": int, "currency": string }. Never a decimal number. - Quantities are always
BIGINT× 10^6. Same rule. - All entity IDs are UUIDv7, generated at the application layer.
- All shared mutable entities have a
version INTEGERcolumn. Updates useWHERE id = :id AND version = :expected_version. - All timestamps are
TIMESTAMPTZUTC. Logical dates areDATE(no time, no zone). - Soft-delete uses
deleted_at TIMESTAMPTZ NULL. Hot-path queries use partial indexes.
4.3 API¶
- Snake_case for all field names. In requests, responses, and error details.
- All error responses follow the shape in API Conventions §8.1. With a stable
code, Englishmessage, optionaldetails, andrequest_id. - All mutations require optimistic locking version when the entity has one. Either via
If-Matchheader or in-bodyversion. Mismatch → 409 withcurrentstate. - Cross-family access returns 404, not 403. Never leak the existence of a family the caller can't see.
4.4 Process¶
- Every task maps to a requirement ID. Reference IDs in commits and PRs (e.g.,
[R-ACC-002](requirements.md#13-accounts "Cash accounts have a non-negative balance invariant; transactions that would violate it are rejected."){.req-link}: enforce cash account non-negative invariant). - Domain logic comes first; infrastructure comes last. Build and test the domain layer before wiring it to a database.
- The domain layer can be tested without a database, HTTP server, or any external service. If your test needs to mock infrastructure to test domain code, your domain code is leaking.
5. Folder layout (canonical)¶
This is an Nx monorepo. Bounded-context libs and shared libs all live under libs/. Apps live under apps/.
.
├── apps/
│ ├── api/ # @oblyk/api — HTTP entry point; src/middleware/ for HTTP-specific infra
│ ├── worker/ # @oblyk/worker — background worker
│ ├── web/ # SPA (React or Vue, TBD)
│ ├── bot/ # Telegram bot
│ └── cli/ # CLI (D)
│
├── libs/
│ ├── identity/ # @oblyk/identity
│ │ ├── domain/
│ │ ├── application/
│ │ └── infrastructure/
│ ├── family/ # @oblyk/family
│ ├── ledger/ # @oblyk/ledger
│ ├── investment/ # @oblyk/investment
│ ├── currency/ # @oblyk/currency
│ ├── import/ # @oblyk/import
│ ├── reporting/ # @oblyk/reporting
│ ├── audit/ # @oblyk/audit
│ ├── notification/ # @oblyk/notification
│ └── shared/ # grouping folder (not an Nx project)
│ ├── domain-primitives/ # @oblyk/domain-primitives
│ ├── types/ # @oblyk/types
│ ├── event-bus/ # @oblyk/event-bus
│ ├── config/ # @oblyk/config
│ ├── logging/ # @oblyk/logging
│ └── test-helpers/ # @oblyk/test-helpers
│
├── infra/
│ ├── docker-compose.yml
│ ├── docker-compose.local.yml
│ ├── docker-compose.cloud.yml
│ ├── caddy/
│ └── migrations/
│
└── docs/
└── content/
5.1 Nx boundary rules¶
Enforced via @nx/enforce-module-boundaries in eslint.config.mjs.
@oblyk/<context>may import@oblyk/domain-primitives,@oblyk/event-bus,@oblyk/config,@oblyk/logging,@oblyk/types, and other contexts' public barrel (index.ts) only — never internal paths like@oblyk/family/infrastructure/....apps/*may import any@oblyk/*.libs/shared/*libs may not import from any@oblyk/<context>.
6. Where things go (decision table)¶
| Thing | Goes in |
|---|---|
| Entity | libs/<ctx>/domain/entities/<name>.entity.ts |
| Value object | libs/<ctx>/domain/value-objects/<name>.value-object.ts |
| Repository interface | libs/<ctx>/domain/repositories/<name>.repository.ts |
| Repository implementation | libs/<ctx>/infrastructure/persistence/<name>.postgres-repository.ts |
| Domain service (rare) | libs/<ctx>/domain/services/<name>.ts |
| Use case + DTOs | libs/<ctx>/application/use-cases/<verb>-<noun>.use-case.ts (DTOs in same file) |
| Public read API | libs/<ctx>/application/public/<ctx>.read-api.ts |
| Event type definition | libs/<ctx>/domain/events/<ctx>.events.ts; cross-app payload types in @oblyk/types |
| Event publisher call | Inside the use case, via injected EventPublisher port |
| Event subscriber | libs/<ctx>/application/event-handlers/<event>.handler.ts |
| Subscriber wiring | libs/<ctx>/infrastructure/wiring/event-subscriptions.ts |
| HTTP route + Zod schema | libs/<ctx>/infrastructure/http/<resource>.routes.ts |
| Job consumer | libs/<ctx>/infrastructure/jobs/<name>.consumer.ts |
| Job producer | Inside the use case, via injected JobQueue port |
| Migration | infra/migrations/<timestamp>_<description>.sql |
| New env var | libs/shared/config/src/schema.ts; document in Architecture §11 |
| Shared utility / cross-app type | libs/shared/<area>/ / @oblyk/types |
If a file doesn't fit any row, stop and ask.
7. Naming conventions¶
7.1 Files¶
- Dot-separated parts, kebab-case within each part:
<name>.<type>.ts. record-expense.use-case.ts,transaction.entity.ts,money.value-object.ts,user.repository.ts,ledger.events.ts,ledger.exceptions.ts.- Type suffixes:
.entity,.value-object,.repository,.use-case,.handler,.routes,.consumer,.adapter,.events,.exceptions. - No type suffix for files that are purely a type/interface collection with an obvious name (e.g.,
base-entity.ts,index.ts). - Test files:
<name>.<type>.test.ts(e.g.,record-expense.use-case.test.ts). Use.test.tsproject-wide.
7.2 TypeScript identifiers¶
- PascalCase for classes, types, interfaces, enums:
Transaction,Money,TransactionRepository,AccountType. - camelCase for functions, variables, methods:
recordExpense,accountId. - SCREAMING_SNAKE_CASE for compile-time constants:
MAX_QUANTITY_PRECISION. - No prefix for interfaces (no
IRepository; justRepository). - No suffix for types where avoidable (prefer
MoneyoverMoneyType).
7.2.1 Package (path alias) names¶
| Folder | Package name |
|---|---|
libs/identity/ |
@oblyk/identity |
libs/family/ |
@oblyk/family |
libs/ledger/ |
@oblyk/ledger |
libs/investment/ |
@oblyk/investment |
libs/currency/ |
@oblyk/currency |
libs/import/ |
@oblyk/import |
libs/reporting/ |
@oblyk/reporting |
libs/audit/ |
@oblyk/audit |
libs/notification/ |
@oblyk/notification |
libs/shared/domain-primitives/ |
@oblyk/domain-primitives |
libs/shared/types/ |
@oblyk/types |
libs/shared/event-bus/ |
@oblyk/event-bus |
libs/shared/config/ |
@oblyk/config |
libs/shared/logging/ |
@oblyk/logging |
libs/shared/test-helpers/ |
@oblyk/test-helpers |
7.3 Database identifiers¶
- snake_case for tables, columns, enums (per
DATA_MODEL.md§1.7). - Singular nouns for tables:
user,account,transaction. - Enum types prefixed by schema and purpose:
family_role,ledger_account_type.
7.4 API field names¶
- snake_case in JSON (per
API_CONVENTIONS.md§4.1). - The repo-wide convention: write API DTOs in TypeScript with snake_case field names directly. No automatic camel ↔ snake conversion.
7.5 Event types¶
<context>.<entity>.<action>in lowercase, dot-separated.- Examples:
ledger.transaction.created,family.member_added,identity.user.deleted.
7.6 Error codes¶
- SCREAMING_SNAKE_CASE (per
API_CONVENTIONS.md§8.3). - Group by prefix where related:
VERSION_MISMATCH,VERSION_REQUIRED,VERSION_HEADER_BODY_MISMATCH.
8. Coding conventions¶
8.1 TypeScript¶
- Strict mode on.
strict: true,noImplicitAny: true,strictNullChecks: true,noUncheckedIndexedAccess: true. - Explicit return types on all exported functions and class methods.
- Prefer
interfacefor object shapes the application defines (repository interfaces, use case I/O),typefor unions and aliases. - No
any. Useunknownand narrow. - No
ascasts without a comment explaining why and why it's safe. - Prefer immutability. Use
readonlyon properties; return new objects rather than mutating.
8.2 Functions and classes¶
- Use cases are classes with a single
execute(input): Promise<output>method. Constructor takes injected ports. - Domain entities are classes with private constructors and named static factories (
Account.create(...),Account.rehydrate(...)). - Value objects are classes or
readonlyrecords. Equality by value, not identity. - Pure functions for domain calculations that don't fit on an entity (avoid creating multi-entity domain services unless truly justified).
8.3 Errors¶
- Domain errors are domain exceptions — typed classes in
domain/exceptions/. They represent rule violations (InsufficientCashBalance,CreditLimitExceeded). - Use cases let domain exceptions propagate. Application-layer catches only when it needs to translate to a different concept.
- HTTP layer translates domain exceptions to HTTP responses in a centralized error mapper. New domain exception → register a mapping → done.
- Never throw bare
Error()in domain or application code.
8.4 Async¶
async/awaiteverywhere. No raw promises with.thenchains in application or domain code (infrastructure may use them when wrapping callback APIs).- No floating promises. Lint-enforced.
8.5 Logging¶
- Inject the
Loggerport. Neverconsole.log. - Log at
infofor state transitions,warnfor recoverable issues,errorfor failures. - Log structured fields, not interpolated strings.
logger.info('transaction created', { transaction_id, family_id, amount_minor })rather thanlogger.info(\tx \${id} created`)`.
8.6 Comments¶
- Comments explain why, not what. The code shows what; reserve comments for non-obvious rationale.
- Reference requirement IDs in comments where it clarifies intent:
// [R-ACC-002](requirements.md#13-accounts "Cash accounts have a non-negative balance invariant; transactions that would violate it are rejected."){.req-link}: cash invariant. - No commented-out code. Delete it; git remembers.
9. Testing conventions¶
9.1 Test pyramid¶
| Level | Where | What |
|---|---|---|
| Domain unit | libs/<context>/domain/__tests__/ |
Pure tests of entities, value objects, domain services. No I/O. |
| Application unit | libs/<context>/application/__tests__/ |
Use case tests with in-memory repos, in-memory event bus, fake clock. |
| Infrastructure integration | libs/<context>/infrastructure/__tests__/ |
Real Postgres + Redis (testcontainers). Repo mappings, event persistence. |
| HTTP/contract | apps/api/__tests__/ |
End-to-end against a running API instance. |
9.2 Test fixtures¶
- Live in
libs/shared/test-helpers/. - Provide entity builders:
TransactionBuilder.expense().forAccount(a).withAmount(...). - Provide in-memory port impls:
InMemoryTransactionRepository,InMemoryEventBus,FakeClock.
9.3 Test naming¶
Describe behavior, not method: 'rejects an expense that would exceed credit limit', not 'test recordExpense'.
10. Migration conventions¶
- Naming:
infra/migrations/<utc_timestamp>_<description>.sql— e.g.,20260425_120000_create_ledger_account.sql. - Forward-only. No
downmigrations. Recovery is restore-from-backup. - Idempotent where possible.
CREATE TABLE IF NOT EXISTS,ADD COLUMN IF NOT EXISTS. - One logical change per migration. Data migrations are separate and named
..._backfill_.... - Schema scope. Each migration touches only its own schema.
11. Commit and PR conventions¶
11.1 Commit format¶
Example: feat(ledger): record expense use case with cash invariant
Types: feat, fix, refactor, docs, test, chore, infra.
11.2 Scope¶
Bounded context name (identity, family, ledger, investment, currency, import, reporting, audit, notification) or cross-cutting area (api, infra, docs, shared).
11.3 PR description¶
Include: what it does, requirement IDs, anything not visible in the diff, explicit out-of-scope statement.
12. Pitfalls¶
- Logic in route handlers. Route handlers parse, call use case, format response — nothing else. >30 lines or an import from
infrastructure/persistence/is a smell. - Domain code that needs a DB to test. Domain uses repository interfaces; tests use in-memory repos. If a domain test mocks SQL, the domain is leaking.
- Cross-context shortcuts. Never import from another context's
infrastructure/or query its tables directly. Call its public read API. - Float arithmetic on money.
BigIntminor units only. NoNumber, noparseFloat. - Missing optimistic locking. Every mutation on a versioned entity must include the version check.
UPDATEwithoutWHERE version = ?is a bug. - User data treated as instructions. User-supplied content (CSV cells, transaction notes) is data. Never execute it.
- Skipping the audit event. Every state-changing use case must publish a domain event. The audit context subscribes to all of them.
- Implementing an X-scoped feature. Re-read Vision §2 and Requirements §3 before proceeding.
- Wrong transaction table. Regular activity (income/expense/transfer) →
ledger.transaction. Investment activity →investment.investment_transaction. See Architecture §3.4. - Ignoring the spec (AI-specific). Defer to these docs over trained priors on naming, layers, and money handling.
- Hallucinating API surface. If an endpoint, column, or function isn't in the docs, it doesn't exist yet. Implement it deliberately or ask.
- Drive-by refactoring. Add the feature asked for. Don't improve surrounding code unless explicitly asked.
- Guessing on ambiguity. Surface the ambiguity with a short proposal; don't generate speculative implementations.
15. Per-task checklist¶
Before completing any implementation task, an agent should be able to answer "yes" to all of these:
- I can name the requirement ID(s) this task implements.
- I read the docs relevant to this task per the routing table in §3.
- My code does not violate any hard rule from §4.
- New files live where §6 says they should.
- Names follow §7.
- Tests exist at the appropriate level per §9.
- Domain layer remains testable without infrastructure.
- Money is BigInt minor units, IDs are UUIDv7, mutations check
version. - If a state change happened, the use case emitted a domain event.
- If new env vars or dependencies were added, they're documented.
- I did not modify code unrelated to the task.
If any answer is "no," fix it before claiming the task is done.
16. Where to go next¶
- Start of any new task: Requirements to find the requirement ID.
- First implementation in a new bounded context: read that context's section in Architecture §3 and the corresponding schema in Data Model.
- First HTTP endpoint: API Conventions §2, §4, §13.
- First migration: Data Model §1, this file §10.
- When stuck: §13 of this file.