Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# AGENTS.md

`@autonomynexus/accounting` — French accounting logic library (PCG, FEC, VAT, financial statements, liasse fiscale). Built with Effect.ts; uses `monetary` as a peer dependency.

## Commands
- Install: `bun install`
- Test: `bun run test`
- Typecheck: `bunx tsc -p tsconfig.build.json`
- Build: `bun run build` (tsc emit)
- Lint: `bun run lint` (oxlint --fix)
- Format: `bun run format` (oxfmt)

## Repo Map
- Entry point: `src/index.ts`
- Models & schemas: `src/models/`, `src/models.ts`
- Chart of accounts (PCG): `src/pcg/`, `src/chart-of-accounts.ts`
- Journal entries: `src/journal/`
- Financial statements (bilan, compte de résultat): `src/financial-statements/`
- Liasse fiscale (BNC-2035 etc.): `src/liasse-fiscale/`, `src/bnc-2035/`
- FEC export: `src/fec/`
- VAT declarations: `src/vat/`
- Amortization: `src/amortization/`
- URSSAF: `src/urssaf/`
- Exercice (fiscal year): `src/exercice/`
- Engine (computation): `src/engine/`
- Regime: `src/regime/`
- Threshold logic: `src/threshold/`
- Bespoke helpers: `src/bespoke/`
- Ports (interfaces): `src/ports/`
- Solde helpers: `src/is-solde/`
- Tests: `test/`

## Conventions (Always Apply)
- Package manager: bun only; do not add npm/yarn/pnpm lockfiles.
- ESM: `.js` extensions required in all imports (tsc emit, not bundled).
- Effect errors: prefer `Schema.TaggedError`, `Effect.catchTag/catchTags`, `Cause.pretty`.
- Effect guards: use library guards (e.g. `Exit.isSuccess`) vs ad-hoc `_tag` checks.
- Monetary: all money values use `monetary` types — never raw `number` for currency amounts.
- Logging: prefer `Effect.log*` over `console.*` (oxlint enforces `no-console`).
- Linting: oxlint enforced (`no-console`, `no-explicit-any`, `no-barrel-file` except `src/index.ts`, `no-namespace`).
- No barrel files: only `src/index.ts` is allowed as a barrel (re-export) file.

---

## Retrievable Documentation

<!-- PROJECT-AGENTS-MD-START -->
Prefer retrieval-led reasoning over pre-training-led reasoning.

[Project Docs]|root: ./docs/agents|Retrievable docs for project patterns and conventions
|effect.md: Tag/Layer, runtime, Effect.gen, Schema.TaggedError, Cause/Exit, logging, error matching
|effect-patterns.md: Effect.ts service pattern, layers, error handling, dependency injection
|monetary.md: Monetary types, snapshots, arithmetic helpers, Drizzle serialization, tax rates
|logging.md: wide events, 1 log per request, spans, structured logging standards
|accounting-financial-statements.md: PCG income statement, balance sheet, journal aggregation, money rules
<!-- PROJECT-AGENTS-MD-END -->
35 changes: 35 additions & 0 deletions docs/agents/accounting-financial-statements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
updated: 2026-02-09
---

# Accounting: Financial Statements

This repo generates French accounting financial statements from validated journal entries,
following PCG (Plan Comptable General) conventions.

## Where
- Feature folder: `src/server/features/accounting/financial-statements/`
- Services include income statement and balance sheet.

## Income Statement (Compte de resultat)
The income statement is derived by aggregating balances by account code:
- Operating result: class 7 revenue - class 6 expenses
- Financial result: 76x revenue - 66x expenses
- Exceptional result: 77x/78x revenue - 67x/68x expenses
- Net result: operating + financial + exceptional - income tax (69x)

## Inputs
- Validated journal entries only.
- Multi-tenant boundaries are enforced (user-scoped).

## Output Shape
- Use `Monetary` types throughout; no floating-point.
- Support year-over-year comparison when previous-year data is provided.

## Implementation Notes
- Prefer business logic in services; keep queries in repositories.
- For multi-step compute + persistence flows, wrap with `withTransaction(...)`.

## Related Money Rules
- Use `monetary` helpers (see `docs/agents/monetary.md`).
- Avoid serializing rich `Monetary` objects; use snapshots for transport.
218 changes: 218 additions & 0 deletions docs/agents/effect-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
---
updated: 2026-02-11
---

# Effect.ts Patterns

## Service Pattern
```typescript
// 1. Interface (R = never - deps resolved in Layer)
export type InvoiceServiceInterface = {
readonly createInvoice: (params) => Effect.Effect<Invoice, Error, never>;
};

// 2. Tag (MUST use Effect.Tag - pre-commit enforced)
// Use @app/ServiceName prefix for uniqueness
export class InvoiceService extends Effect.Tag("@app/InvoiceService")<
InvoiceService,
InvoiceServiceInterface
>() {}

// 3. Layer (NO global imports - use yield*)
// Naming: camelCase + Layer suffix (layer, testLayer, mockLayer)
// Use 'layer' for production, NOT 'liveLayer'
export const InvoiceServiceLayer = Layer.effect(
InvoiceService,
Effect.gen(function* () {
const db = yield* Database;
const revalidator = yield* RevalidationService;

const createInvoice = (params) =>
Effect.fn("createInvoice")(function* () {
const data = yield* Effect.tryPromise({
try: () => db.insert(invoice).values(params),
catch: (e) => new DatabaseError({ operation: "create", details: e }),
});
yield* revalidator.revalidatePaths(["/invoices"]);
return data;
});

return InvoiceService.of({ createInvoice });
})
);

// Test layer
export const InvoiceServiceTestLayer = Layer.succeed(
InvoiceService,
InvoiceService.of({
createInvoice: () => Effect.succeed(mockInvoice),
})
);
```

**Service Design Pattern**: Start by sketching service tags (no implementations) to reason about boundaries and dependencies before writing production code. Services should have `R = never` (resolve deps in Layer, not interface), **except `R = Database`**: repositories require it for tx propagation, services inherit it from repo calls (services never do direct db calls).

## Effect.fn Pattern
```typescript
// Effect.fn for named, traced effects (tracing spans, stack traces)
const processInvoice = Effect.fn("processInvoice")(function* (invoiceId: InvoiceId) {
const invoice = yield* getInvoice(invoiceId)
return yield* processData(invoice)
})

// Effect.gen for simple inline Effects
const simpleEffect = Effect.gen(function* () {
return yield* fetchData
})
```

## Tagged Errors

- **Expected errors** (Schema.TaggedError): Domain failures callers handle (validation, not found, permission)
- **Defects** (Schema.Defect): Unrecoverable (bugs, invariant violations, external crashes)

```typescript
export class ValidationError extends Schema.TaggedError<ValidationError>()(
"ValidationError", { message: Schema.String }
) {}

export class HttpError extends Schema.Defect {} // Unrecoverable

// Recovery: catchAll, catchTag("HttpError", ...), catchTags({...})
```

## NotFoundError Pattern (Page Builder)
```typescript
// app/lib/errors.ts - Domain errors MUST extend this
export abstract class NotFoundError extends Schema.TaggedError<NotFoundError>()(
"NotFoundError", { message: Schema.String }
) {}

export class InvoiceNotFoundError extends NotFoundError {} // Shares _tag
```

Page builder catches ANY `*NotFoundError` via `Predicate.isTagged("NotFoundError")` → `notFound()` → 404.

## Repository NotFoundError Pattern (CRITICAL)

Repository `find*`/`get*` MUST fail with NotFoundError. **Never return undefined/null.**

```typescript
// ✅ CORRECT: Fail with NotFoundError
findById: (id, userId) => Effect.gen(function* () {
const db = yield* Database;
const result = yield* Effect.tryPromise({...});
if (!result) return yield* new InvoiceNotFoundError({ message: "not found" });
return mapDbRowToDomain(result);
}),

// ❌ WRONG: filterOrFail is fragile (undefined !== null is TRUE!)
repo.findById(id, userId).pipe(Effect.filterOrFail((r) => r !== null, ...))
```

**Null check rule**: Use `!result` or `!= null`. **Never** `!== null`.

## Schemas (UI/Server Contract)

**Date handling**: `Schema.DateFromSelf` for native Dates. `Schema.Date` only for JSON deserialization.

**Branded Types**: Brand ALL primitives, not just IDs
```typescript
export class InvoiceId extends Schema.String.pipe(Schema.brand("InvoiceId")) {}
export class Email extends Schema.String.pipe(Schema.brand("Email")) {}

// Schema.Class for composite types (use .make() to instantiate)
export class CreateInvoiceInput extends Schema.Class<CreateInvoiceInput>("CreateInvoiceInput")({
issuerId: Schema.String,
status: Schema.Literal("draft", "final"),
}) {}

const input = CreateInvoiceInput.make({ issuerId: "123", status: "draft" })
```

Validate at boundary (actions), services use inferred types (no validation).

## Config Management

```typescript
class AppConfig extends Context.Tag("@app/AppConfig")<
AppConfig, { apiUrl: string; apiKey: string }
>() {
static readonly layer = Layer.effect(AppConfig, Effect.gen(function* () {
const apiUrl = yield* Config.string("API_URL")
const apiKey = yield* Config.redacted("API_KEY") // Auto-hidden
return { apiUrl, apiKey: Config.value(apiKey) }
}))

static readonly testLayer = Layer.succeed(AppConfig, { apiUrl: "http://localhost:3000", apiKey: "test" })
}

// Defaults: Config.orElse(() => Config.succeed(3000))
// Validation: Config.mapOrFail((p) => p > 0 ? Effect.succeed(p) : Effect.fail(...))
```

## HTTP Clients

```typescript
class ApiClient extends Context.Tag("@app/ApiClient")<ApiClient, HttpClient.HttpClient>() {
static readonly layer = Layer.effect(ApiClient, Effect.gen(function* () {
return HttpClient.make().pipe(
HttpClient.mapRequest(HttpClientRequest.bearerToken(token)),
HttpClient.mapRequest(HttpClientRequest.acceptJson)
)
}))
}

const getUser = (id: string) => Effect.gen(function* () {
const client = yield* ApiClient
return yield* client.get(`/users/${id}`).pipe(HttpClientResponse.schemaBodyJson(UserSchema))
})
```

## Validation

**Option types** (prefer over T | null):
```typescript
const getOrg = (id: number | null) => Option.fromNullable(id).pipe(
Option.flatMap((id) => Option.fromNullable(orgs.find(o => o.id === id)))
);

// Usage: Option.match({ onNone: () => "—", onSome: (org) => <Link data={org} /> })
```

**NO `any` types. Use tagged errors, not throws.**

**Union Types**: `Schema.Literal` for enums, `Schema.TaggedClass + Schema.Union` for complex variants.
Pattern match with `Match.value(...).pipe(Match.tag("Draft", ...), Match.exhaustive)`.

## Testing

```typescript
import { it, layer } from "@effect/vitest"

layer(InvoiceServiceTestLayer) // Memoize once per file

it.effect("creates invoice", () => Effect.gen(function* () {
const service = yield* InvoiceService
const invoice = yield* service.createInvoice(params)
expect(invoice.status).toBe("draft")
}))

it.scoped("handles resources", () => Effect.gen(function* () {
const resource = yield* acquireResource // auto cleanup
}))
```

## Observability

```typescript
const OtelLayer = NodeSdk.layer(() => ({
resource: { serviceName: "myapp", serviceVersion: "1.0.0" },
spanProcessor: new BatchSpanProcessor(new OTLPTraceExporter({ url: "..." }))
}))

// Named spans with Effect.fn + withSpan
const fetchUser = Effect.fn("fetchUser")(function* (userId: string) {
return yield* Effect.tryPromise(...).pipe(Effect.withSpan("fetchUser", { attributes: { userId } }))
})
```
61 changes: 61 additions & 0 deletions docs/agents/effect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
updated: 2026-02-15
---

# Effect.ts Patterns

Server-side code uses Effect for services, repositories, infrastructure, and runtime wiring.
The goal is: composable business logic, typed failure, structured logging, and testable layers.

## Project Structure
- Runtime wiring: `src/server/runtime.ts`.
- Feature services/repos: `src/server/features/**`.
- Shared infra: `src/server/shared/infrastructure/**`.
- Shared error formatting: `src/server/shared/errors.ts`.

## Tags + Layers
- Use `Effect.Tag` / `Context.Tag` for capabilities (services, repos, infra).
- Provide implementations with `Layer.*`.
- Compose layers into a `ManagedRuntime` once at startup.

Notes from `src/server/runtime.ts`:
- Infrastructure layers depend on config.
- Repository layers depend on `Database`.
- Service layers depend on repos + infra.
- `ConsolaLoggerLive` replaces the default Effect logger.

## Writing Services
- Prefer `Effect.gen(function* () { ... })` for readable orchestration.
- Keep DB access in repos; keep business rules in services.
- For cross-repo write flows, wrap the orchestrator with `withTransaction(...)`.

## Error Modeling
- Define domain errors with `Schema.TaggedError`.
- Always fail with typed/tagged errors for expected failures.
- Avoid silent fallbacks; either return a typed error or throw a deliberate redirect/notFound.

Matching and formatting:
- Catch typed failures with `Effect.catchTag` or `Effect.catchTags`.
- For diagnostics, use `Cause.pretty(cause)`.
- For user-friendly logs, prefer the formatter in `src/server/shared/errors.ts`.

## Guards / Type Checks
- Prefer Effect-provided guards (examples: `Exit.isSuccess`, `Option.isSome`, `Either.isRight`).
- Avoid ad-hoc `_tag` string checks when the library already provides a guard.

## Logging
- Prefer `Effect.logDebug/info/warn/error` over `console.*`.
- Add request correlation via `Effect.annotateLogs({ requestId, userId, ... })`.
- `src/server/shared/lib/consola-logger.ts` routes Effect logs to the app logger.

## Concurrency
- Use `Effect.all([...])` for parallelizable work (independent reads).
- Use sequential composition for dependent steps (write -> read, idempotency locks, etc.).

## Practical Patterns

Error shaping for server boundaries:
- Keep typed failures as typed RPC errors and let the boundary transport them.

Request-local annotation:
- Put `requestId` in annotations once and let the logger attach it to every message.
Loading