# Contributing to firma

## Architecture

Firma is a Yarn Berry monorepo. The code is organized into five layers; dependencies flow in one direction.

```
Application  ──>  Use-case  ──>  Domain (Port)  <──  Adapter  ──>  External API
                                       ▲
Cross-cutting (db, utils)   ←── used by every layer above
```

### Layers and packages

| Layer | Package | Responsibility |
|---|---|---|
| **Application** | `apps/cli` | Commander CLI. `commands/` render terminal output, `services/` wire providers and bind to local config + repo. Holds no business logic. |
|  | `apps/mcp` | MCP server. `tools/` shape responses with `ok()` / `err()`. Same provider wiring as CLI, separate instance. |
| **Use-case** | `@firma/portfolio` | Portfolio business logic — `syncPrices`, `takeSnapshot`, `ensureTodaySnapshot`, `buildRiskMetrics`, `buildBenchmarkComparison`, and the write operations `addTransaction`, `editTransaction`, `setManualPrice`. Pure functions accepting domain clients or the repository as parameters. |
|  | `@firma/brief` | Daily-brief orchestrator. Gathers context from all three domain clients plus cash-flow data, calls `@firma/advisor`, and combines everything into one cross-referenced view. |
|  | `@firma/advisor` | Deterministic advisory engine. `analyze(context)` computes a weekly stance, ranked recommendations (deploy / contribution / tactical / derisk / runway / rebalance), and a peer-driven watchlist. Pure rule functions; depends only on `@firma/domain`. |
| **Domain / Port** | `@firma/domain` | Provider-agnostic port interfaces, split into three sub-domains under `src/`. `market/` — stock data: `MarketDataProvider` interface + `MarketDataClient` router (capabilities: quote, snapshot, candles, news, insider, financials, earnings, dividends, peers, economic-calendar). `macro/` — macro data (capabilities: series, metadata, search, country-indicator, live-fx) plus macro domain logic (`assembleMacroSnapshot`, `assembleStressIndex`, `assembleRegime`). `event/` — world events (capability: disasters). |
| **Adapter** | `@firma/finnhub` | Implements `MarketDataProvider` against finnhub.io — the US-market price/research provider. |
|  | `@firma/yahoo-finance` | Implements `MarketDataProvider` for non-US markets (KRX, KOSDAQ, JP, HK, LSE, XETRA, NSE, TW) against Yahoo Finance's keyless chart endpoint. No key required. |
|  | `@firma/fred` | Implements `MacroDataProvider` (series capabilities) against fred.stlouisfed.org. |
|  | `@firma/worldbank` | Implements `MacroDataProvider` (country-indicator) against api.worldbank.org. No key required. |
|  | `@firma/open-er-api` | Implements `MacroDataProvider` (live-fx) against open.er-api.com. No key required. 60s in-process cache. |
|  | `@firma/gdacs` | Implements `EventDataProvider` against the GDACS RSS feed. No key required. |
| **Cross-cutting** | `@firma/db` | Drizzle schema, repository interface + SQLite implementation, `aggregateHoldings()`. |
|  | `@firma/utils` | Pure helpers — date, risk math, ledger categories, formatters. |

### Dependency rule

- Application code imports **domain clients** (`MarketDataClient`, `MacroDataClient`, `EventDataClient`), never raw adapter packages.
- The single exception is `apps/cli/src/services/data-clients.ts` and `apps/mcp/src/data-clients.ts`, which exist precisely to import adapters and wire them into clients.
- Use-case packages (`portfolio`, `advisor`, `brief`) depend only on `@firma/domain` — except `brief`, which also composes `@firma/advisor`.
- Adapter packages depend on `@firma/domain` to satisfy the port interfaces they implement.

This means you can grep for `from '@firma/finnhub'` (or any adapter) and the only matches in `apps/` should be the two `data-clients.ts` files.

## Adding a command

Every new feature needs three layers:

1. **Business logic** in `@firma/portfolio` (or another use-case package). Pure function, accepts a domain client as a parameter, returns a typed result.
2. **CLI command** in `apps/cli/src/commands/<name>.ts` — pulls clients from `services/data-clients.ts`, calls the business function, formats terminal output. Register in `apps/cli/src/index.ts`.
3. **MCP tool** in `apps/mcp/src/tools/<area>.ts` — pulls clients from `data-clients.ts`, calls the same business function, formats with `ok()` / `err()`.

MCP tool naming: `add_*` (write), `show_*` (read), `report` (aggregated), `edit_*` / `delete_*` (mutations).

## Adding a new external data provider

End-to-end recipe — say you're adding Naver Finance for KRX coverage.

1. **Create the external-API package** under `packages/external-api/naver/` — raw client only, no firma-domain knowledge:
   ```
   packages/external-api/naver/
     package.json          devDeps: @firma/utils, tsup, typescript
                           (NO @firma/domain — this package is pure)
     tsconfig.json
     tsup.config.ts
     src/
       fetcher.ts          createNaverFetcher() — HTTP calls, raw types
       types.ts            raw Naver response shapes
       index.ts            export { createNaverFetcher, raw types }
   ```

2. **Add the adapter** in `packages/adapter/adapters/src/naver.ts`:
   ```ts
   import { createNaverFetcher } from '@firma/naver';
   import type { MarketCapability, MarketDataProvider } from '@firma/domain';

   const CAPABILITIES = new Set<MarketCapability>(['quote', 'snapshot', 'candles']);

   export const createNaverProvider = (): MarketDataProvider => {
     const fetcher = createNaverFetcher();
     return {
       id: 'naver',
       capabilities: CAPABILITIES,
       supports: (s) => s.market === 'KR',
       getQuote: async (s) => { /* raw → domain Quote */ },
       // ...
     };
   };
   ```
   Add it to `packages/adapter/adapters/package.json` devDeps and re-export from `src/index.ts`.

3. **Register in data-clients** at `apps/cli/src/services/data-clients.ts` and `apps/mcp/src/data-clients.ts`:
   ```ts
   return createMarketDataClient([
     createNaverProvider(),
     createFinnhubProvider(finnhubKey),  // ordering = priority
   ]);
   ```

4. **No other changes required.** All CLI commands and MCP tools route through the client; they pick up the new provider automatically.

If your provider belongs to a different domain (a new macro source, a new event source), the recipe is identical against `MacroDataProvider` or `EventDataProvider`. If you need a brand-new capability (e.g. you're adding a corporate-actions provider), add the capability to the relevant sub-domain in `@firma/domain` — `src/market/capability.ts` + `provider.ts` + `client.ts` (or `src/macro/`, `src/event/`) — first, then implement the adapter.

## Extension points (other)

**Swap the storage layer** — implement `DataRepository` (from `@firma/db`) against your data store and replace the wrapper in `apps/cli/src/db/index.ts` and `apps/mcp/src/db.ts`. The repository contract is the only thing use-case code knows about; SQLite is just one implementation.

## Dev setup

Requires Node.js 22+ and Yarn Berry.

```bash
corepack enable
yarn install

yarn dev:cli show portfolio    # run CLI in dev mode
yarn typecheck                 # full type check across all packages
yarn test                      # unit tests (vitest)
yarn lint                      # biome lint
yarn format                    # biome format --write
```

## Conventions

- `if`/`else`/`for`/`while` bodies always use braces (enforced by biome's `useBlockStatements`).
- Application code imports domain clients, not adapters.
- Adapter packages' public exports are narrow — typically just `createXxxProvider`. Raw clients stay internal.
