AGENTS.md · diff

git:20260910.45bef1e to git:20261018.e75e011

298 added, 169 removed. Audit A to A.

- # AGENTS.md — using Brain as your memory over MCP
+ # AGENTS.md — contributing to INITE Brain
- This guide is for AI agents (Claude Code, Cursor, openclaw, custom MCP
- clients) consuming **INITE Brain** — a bitemporal, per-tenant knowledge-graph
- memory service on SurrealDB. Humans: start at
- [docs/getting-started.md](docs/getting-started.md) instead.
+ > Using Brain as your agent's memory? See [docs/agents-using-brain.md](docs/agents-using-brain.md).
- ## Connect
+ This file is for AI coding agents (Claude Code, Cursor, Codex, Copilot, Gemini
+ CLI, Windsurf…) and the humans working with them on **this repository**. It is
+ the source of truth for commands, zones and the idea → PR loop.
+ [CONTRIBUTING.md](CONTRIBUTING.md) is the human-facing companion; where the two
+ disagree, this file wins (and please fix the other one).
- Brain speaks MCP over **Streamable HTTP**, one endpoint per tenant:
+ ## What this is
- ```
- POST https://brain.inite.ai/mcp # tenant from the credential
- POST https://brain.inite.ai/mcp/<companyId> # explicit; must match the credential
- Authorization: Bearer brain_<api-key>
- ```
+ INITE Brain is a bitemporal, per-tenant knowledge-graph memory service for AI
+ agents, served over MCP (Streamable HTTP) and REST, on SurrealDB. NestJS 11 +
+ TypeScript, pnpm, Node 22. Also in the repo: the landing/admin app
+ (`brain-landing/`, Next.js), npm clients (`clients/*`), the agent skills
+ (`skills/`) and the Claude Code plugin (`plugins/inite-brain/`).
- Reach for the tenant-less form when all you have is a URL — an
- OAuth-discovered connector learns its tenant from the token's `org`
- claim, not from the path. `GET /mcp/health` answers unauthenticated.
+ **Place in the INITE Triad:** Auth (inite-auth-service) issues tokens;
+ Billing and Brain verify them. The token contract (issuer, audience, claims)
+ is [inite-triad/docs/compatibility.md](https://github.com/inite-ai/inite-triad/blob/main/docs/compatibility.md);
+ Brain's side of it is `src/auth/token-audience.ts`, pinned by
+ `test/triad-token-contract.socket-spec.ts`.
- Harnesses with native remote-MCP support connect by URL + header. Harnesses
- that can only spawn stdio servers use the first-party shim
- [`@inite/brain-mcp`](clients/brain-mcp/README.md):
+ ## Ground rules (non-negotiable)
- ```json
- { "command": "npx", "args": ["-y", "@inite/brain-mcp"],
- "env": { "BRAIN_API_KEY": "brain_…", "BRAIN_COMPANY_ID": "…" } }
+ 1. **Never edit a shipped migration.** Schema changes are a NEW file
+ `src/db/migrations/NNNN_description.surql` (next free number, idempotent,
+ `IF NOT EXISTS`). The migrator records applied ids per tenant and silently
+ skips edited files, so an edit is invisible in production.
+ 2. **Never weaken tenancy or per-user scope.** Every tenant is its own
+ database (`NS=brain`, `DB=co_<companyId>`, reached only through
+ `SurrealService.withCompany`). Omitting `userId` must keep returning
+ tenant-global facts only — personal memory is never mixed in. No new code
+ path may skip the scope fences (`src/auth/user-scope.ts`,
+ `src/auth/tenant-scope.ts`, `src/search/internals/where-builder.ts`).
+ 3. **Never change the token contract** (accepted `iss`/`aud`, claim shapes,
+ scope names `brain:read|write|admin|read_pii|read_media|platform_admin`)
+ without a Triad-level change agreed first.
+ 4. **GDPR forget stays complete and synchronous.** `forget_entity` / user
+ forget must keep cascading every derived row and leaving only tombstones.
+ A new table holding user content must be wired into the cascade.
+ 5. **No eval forks behind flags.** A flag whose purpose is to let one
+ measurement see a different code path is a fork; forks live on git
+ branches. Per-tenant behaviour is configuration (`RetrievalProfile`,
+ extraction profile), not a flag. New engine-behaviour boolean flags fail
+ `test/flag-budget.unit-spec.ts` until a maintainer approves the golden
+ entry. Every catalogued flag must have a reader
+ (`test/config-catalog-truth.unit-spec.ts`).
+ 6. **Never log or echo secrets, API keys, tokens or memory content/PII.**
+ Never commit `.env*` files or keys. Credentials stored at rest go
+ through `src/common/secret-cipher.ts`.
+ 7. **Never hand-edit generated files:** `docs/openapi.json` and
+ `brain-landing/public/openapi.json` (`pnpm openapi:build`),
+ `plugins/inite-brain/skills/` (`pnpm plugin:sync`),
+ `src/data/skill-versions.json` + `src/data/skill-changelog.ts`
+ (`pnpm skills:build`), `src/db/migrations/0135_flexible_reconcile.surql`
+ (`pnpm migrations:flexible`), `CHANGELOG.md` and version numbers
+ (release-please).
+ 8. **Never skip or weaken a gate** — no `.skip`, no lowered coverage
+ threshold, no raised lint warning cap, no deleted assertion to get green.
+ A "truth" spec failing means the code and its claim disagree; fix one.
+ 9. **No new dependency without a reason** in the PR body (cold-start and
+ image size are tight; see CONTRIBUTING.md § 3).
+ 10. **Real-model evals and paid API calls need the maintainer's OK** — they
+ spend real money. Unit tests never call a model.
+
+ ## Zones
+
+ | Path | What lives there | Zone |
+ | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------- |
+ | `docs/` (except generated), `examples/`, `brain-landing/content/` | Prose, guides, blog/docs MDX, runnable examples | 🟢 green |
+ | `test/*.unit-spec.ts` (new tests only) | Unit tests for existing behaviour | 🟢 green |
+ | `src/mcp/` | MCP tool registrations, profiles, annotations | 🟡 yellow |
+ | `src/*/**.controller.ts`, `src/contracts/` | REST routes, zod wire contracts (additive changes) | 🟡 yellow |
+ | `src/search/`, `src/synthesize/`, `src/ai/`, `src/documents/`, `src/episodes/` | Retrieval, answer generation, extraction, document pipeline | 🟡 yellow (+ eval evidence) |
+ | `src/source-plane/`, `src/sources/`, `src/jobs/`, `src/dreams/`, other `src/*` | Connectors, job queue, background passes, everything else | 🟡 yellow |
+ | `clients/*`, `plugins/`, `skills/`, `brain-landing/` (app code) | npm clients, Claude Code plugin, agent skills, landing + admin UI | 🟡 yellow |
+ | `src/db/migrations/`, `src/db/schema.surql`, `src/db/` | Schema (append-only), migrator, tenant connection pools | 🔴 red |
+ | `src/auth/`, `src/keys/`, `src/policy/` (engine) | Token/API-key verification, audience, scopes, key minting, ABAC | 🔴 red |
+ | `src/entities/*forget*`, `src/documents/document-purge.util.ts` | GDPR forget cascades | 🔴 red |
+ | `src/ingest/conflict-resolver.ts`, `src/ingest/fact-resolver.service.ts`, `src/facts/` | Conflict resolution, fact lifecycle (supersede / COMPETING / retract) | 🔴 red |
+ | `src/common/*-flags.ts`, `src/admin/config-catalog.data.ts`, `test/golden/` | Flag readers, config catalogue, flag budget | 🔴 red (new flags) |
+ | `test/eval/**` | Eval batteries, golden baselines | 🔴 red |
+ | `.github/workflows/`, `scripts/ci/`, `Dockerfile`, `docker-compose*.yml` | CI, deploy (every green `main` deploys to prod) | 🔴 red |
+ | Published API shapes (breaking changes to `src/contracts/`, MCP tool schemas) | What integrators code against | 🔴 red |
+
+ 🟢 just do it · 🟡 tests + a note in the PR on what you touched and why ·
+ 🔴 **stop**: open an issue with a spec (template "Product idea"), a maintainer
+ approves the design before code.
+
+ ## Setup
+
+ **One click:** [Open in GitHub Codespaces](https://codespaces.new/inite-ai/inite-brain-service)
+ — `.devcontainer/` gives Node 22, pnpm 10, deps installed, a `.env` copied from
+ `.env.example`, SurrealDB running on `localhost:8000` and Docker-in-Docker for
+ the e2e suites.
+
+ **Local** (Node 22, pnpm 10 via `corepack enable`, Docker for SurrealDB):
+
+ ```bash
+ pnpm install --frozen-lockfile
+ cp .env.example .env # set OPENAI_API_KEY for ingest/search
+ docker compose up -d surrealdb # SurrealDB v3.2.4 on localhost:8000
+ pnpm start:dev # http://localhost:3000/health
```
- Self-hosted: add `BRAIN_BASE_URL` (or `BRAIN_MCP_URL`). The shim (v0.2) is a
- transparent passthrough for tools AND resources (`resources/list`,
- `resources/templates/list`, `resources/read`), and bridges sampling in
- reverse — brain's `sampling/createMessage` reaches YOUR model, falling back
- to a server-side template when your harness doesn't advertise sampling.
- Auth, tenancy, scopes, and PII fencing are enforced server-side from the
- key. Key scopes decide what `tools/list` returns: `brain:read` → read
- surface; `brain:write` adds mutations; `brain:admin` adds GDPR forget.
- Two further scopes exist but are hosting-operator-only (granted via env-key
- config, never mintable through tokens): `brain:read_media` (media/biometric
- evidence — stricter than `brain:read_pii`, not implied by it) and
- `brain:platform_admin` (cross-tenant operator authority).
+ Creating a local API key: README § Run from source. Unit and socket tests need
+ no database, no Docker and no API key. The e2e suites start their own
+ SurrealDB container via testcontainers (`test/global-setup.ts`; set
+ `SURREALDB_URL` to reuse a running one). Landing app: `cd brain-landing &&
+ pnpm install --frozen-lockfile && pnpm dev` (port 3030).
- ## Tool surface
+ ## Commands
- Read (with `brain:read`):
+ Mirrors [.github/workflows/ci.yml](.github/workflows/ci.yml) (CI installs with
+ `--ignore-workspace`; same lockfile).
- | Tool | One-line semantics |
- |---|---|
- | `search_knowledge` | Semantic search over the graph; entities + top facts; `asOf` for "what was TRUE on X" (valid time — a fact learned later still shows) |
- | `search_multi_hop` | Planner-LLM decomposes into ≤5 anchored hops; returns an auditable evidence chain (`finalEntityIds` + `supportingFactIds`); `synthesize=true` for a grounded answer |
- | `graph_retrieve` | Graph-first retrieval around named entities (1-hop neighbourhood); use when you already know WHICH entities |
- | `synthesize` | Hybrid search → citation-bearing answer (each claim ends `[factId]`) → verifier LLM; guardrails `strict`/`lenient`/`off` |
- | `memory_diff` | Everything learned / retracted / superseded / forgotten between two ISO cursors `[from, to)` — "what changed since last conversation?" |
- | `get_entity_profile` | One entity: canonical name, type, externalRefs, active facts |
- | `get_entity_timeline` | Chronological audit of all facts about an entity, retracted included |
- | `summarize_entity` | One-line briefing; `styleHint='client_llm'` delegates wording to YOUR model via MCP sampling |
- | `get_competing_facts` | Unresolved same-predicate disagreements (COMPETING status) for an entity |
- | `detect_contradiction` | Dry-run the conflict resolver: "if I recorded this fact, what would happen?" (`INSERTED`/`SUPERSEDED`/`COMPETING`/`REJECTED`) |
- | `find_related_entities` | Entities connected via graph edges; here `asOf` cuts EDGES on knowledge time (as they were believed at T) — the one read surface where it is not valid time |
- | `match_procedure`, `list_procedures` | Procedural memory: reusable how-to recipes |
- | `search_communities`, `list_communities`, `find_entity_communities` | Graph community summaries (thematic clusters) |
- | `why`, `recall_decisions` | Code-memory: why code is the way it is; past recorded decisions |
- | `get_source_reputation` | Learned trust profile of a source vertical |
- | `workspace_status` | Which tenant this connection writes to, what is already in its memory, and `nextSteps` — the setup actions that would change something. First call after connecting |
- | `rename_workspace` | Name the workspace (`brain:write`). Offered only while it has none, and gone once it does |
- | `get_fact` | One fact as stored: statement, validity, source attribution, lifecycle (`retracted: true` still resolves), and `groundingStatus` (`grounded`/`ungrounded`; absent = legacy row). Registered when the server runs `FACTS_API_ENABLED` |
- | `get_fact_provenance` | Why a fact is remembered: the verbatim grounding turns (with char-span quotes when stamped); plus `derivedFacts`/`closure`/`supportEdges` on servers running the closure/support-graph read flags. Same `FACTS_API_ENABLED` gate |
+ | What | Command |
+ | ---------------------------- | --------------------------------------------------------------------------------------- |
+ | Install | `pnpm install --frozen-lockfile` |
+ | Dev server | `pnpm start:dev` |
+ | Lint (CI) | `pnpm lint:ci` (zero warnings; `pnpm lint` auto-fixes) |
+ | Format check (CI) | `pnpm format:check` (fix with `pnpm format`) |
+ | Typecheck src + tests | `pnpm typecheck` |
+ | Build | `pnpm build` |
+ | Unit tests | `pnpm test` (CI: `pnpm test:unit:cov`, coverage ratchet) |
+ | One test file | `pnpm test -- test/where-builder.unit-spec.ts` (or a name fragment) |
+ | Socket tests (loopback HTTP) | `pnpm test:socket` |
+ | E2E (Docker) | `pnpm test:e2e` (CI shards: `pnpm exec jest --config ./test/jest-e2e.json --shard=1/4`) |
+ | Jobs e2e (Docker) | `pnpm test:e2e:jobs` |
+ | Landing (`brain-landing/`) | `pnpm lint:i18n && pnpm lint:ci && pnpm typecheck && pnpm test && pnpm build` |
+ | Regenerate OpenAPI | `pnpm openapi:build` |
+ | Sync plugin skills | `pnpm plugin:sync` (`--check` reports drift) |
- MCP resources (droppable into context without a tool call):
- `brain://entity/<id>` (profile) and `brain://entity/<id>/timeline`.
+ CI jobs: `engine-checks` = `lint:ci` + `format:check` + `typecheck` + `build`;
+ `engine-unit`; `engine-socket`; `engine-e2e` (4 shards); `engine-jobs-e2e`;
+ `web` (landing); `docker`; `build-test` judges those; `supply-chain`
+ (`pnpm audit --audit-level=high`) is its own required check. The real-LLM
+ `quality-eval` (nightly) and `real-e2e` (manual dispatch) never run per PR.
+ `devcontainer.yml` builds `.devcontainer/` when it or the lockfile changes.
- Write (adds with `brain:write`): `record_fact` (accepts `conversationId` +
- `evidence[]` — see grounding below), `link_entities`, `retract_fact`,
- `record_procedure`, `retire_procedure`, `record_feedback`,
- `ingest_document` (Source→Indexer→Candidates pipeline; prefer over
- `record_fact` for anything longer than one claim; accepts
- `toolObservationRef: tool_observation:<id>` to close the tool result →
- document → fact provenance loop under `TOOL_OBSERVATIONS_ENABLED`),
- `record_decision`.
+ **Done = `lint:ci`, `format:check`, `typecheck`, `build`, `test`, `test:socket`
+ green locally, and every CI job green on the PR.** `format:check` is the one
+ people forget.
- Admin (adds with `brain:admin`): `forget_entity` — GDPR hard-delete with a
- synchronous cascade and tombstones.
+ ## Architecture map
- ## Memory semantics you must know
+ Request flow: `src/main.ts` → `AppModule` (`src/app.module.ts`) → global
+ throttler → controller guarded by `ApiKeyGuard` (`src/auth/api-key.guard.ts`:
+ API key or Auth-service JWT → `req.brainAuth = { companyId, scopes, … }`) →
+ `@RequireScopes` + `@PolicyAction` (ABAC, `src/policy/`) → service →
+ `SurrealService.withCompany(companyId, …)` → tenant DB `co_<companyId>`.
+ MCP: `POST /mcp` or `/mcp/:companyId` → `src/mcp/mcp.controller.ts` →
+ `McpService` builds one MCP server per request with only the tools the
+ credential's scopes allow.
- - **Facts vs episodes.** Brain ingests raw conversation/document *episodes*
- and derives typed *facts* (entity, predicate, object, confidence) from
- them. Reads serve facts; provenance points back at the raw source.
- - **Bitemporal by default.** Every fact carries valid time
- (`validFrom`/`validUntil` — when it held in reality) and knowledge time
- (`recordedAt`/`retractedAt` — when brain learned/retracted it). Default
- reads = "actual now". `asOf` on search/synthesize/`get_entity_profile`
- asks "what was TRUE at T" — VALID time: `validFrom <= T < validUntil`,
- a retraction dated before T hides the fact, and a fact brain learned
- after T still shows (a backdated fact is not disappeared by a late
- ingest). It is NOT a knowledge-time snapshot; that exists only on
- `GET /v1/entities/:id?recordedAt=` and the entity timeline. See
- [docs/bitemporal-semantics.md](docs/bitemporal-semantics.md) for the
- per-surface table.
- - **Conflicts are explicit.** A new fact meeting an overlapping same-predicate
- prior is auto-superseded, or parked as COMPETING when too close to call —
- never silently last-write-wins. Preflight contested writes with
- `detect_contradiction`; inspect stalemates with `get_competing_facts`.
- - **Per-user scope is fail-closed.** Read/write tools accept `userId`.
- Omitting it returns tenant-global facts ONLY — a user's personal memory is
- never mixed in unless you ask for that user. Always pass `userId` when
- acting for a specific person.
- - **Retraction ≠ forgetting.** `retract_fact` closes a fact in knowledge
- time — auditable, history kept, visible in timelines and `memory_diff`.
- `forget_entity` (admin, reasons incl. `gdpr_request`) is a hard delete;
- only tombstones remain. Do not use forget for "this fact is now wrong" —
- record the new fact or retract.
- - **Provenance is first-class.** Every fact is attributable:
- `get_fact` / `get_fact_provenance` (and their REST twins) return the
- source chain ([docs/fact-provenance-api.md](docs/fact-provenance-api.md));
- `search_multi_hop` returns supporting fact ids; `synthesize` cites
- `[factId]` per claim and a verifier judges support.
- - **Ground your writes.** `record_fact` accepts `conversationId` (the
- conversation the fact was observed in) and `evidence[]` (≤10 typed
- pointers: event/message/conversation/url/document/commit/other). Naming
- the observation marks the fact **grounded** — servers stamping
- `groundingStatus` (EVIDENCE_GROUNDING_STAMP) can exclude ungrounded
- claims from consolidation and from strict serving. A bare `record_fact`
- is permanently observation-free; always pass what you have.
- - **`evidenceCitations` is a SEPARATE array from `citations`.** `synthesize`
- answers carry fact citations in `citations[]` (`c.factId`); episode- or
- fragment-grounded claims arrive in `evidenceCitations[]` (episode arm:
- `episodeId` + optional verified `span`; fragment arm: `fragmentId` +
- `assetId` + `capability` + rendered `excerpt`). Never flatten the two —
- consumers reading `factId` off an evidence citation (or vice versa) will
- corrupt their provenance handling.
- - **Abstention is a feature.** `synthesize` in `strict` mode returns `null`
- rather than an unsupported answer. Treat `null` as "brain does not know",
- not as an error to retry around. Beyond the classic reasons
- (`no_results`, `low_coverage`, verifier verdicts) there are two
- evidence-plane reasons with DIFFERENT remedies: `ungrounded_evidence`
- (every cited fact is `groundingStatus='ungrounded'` — remedy: ground the
- claims, not "the memory doesn't know") and `evidence_capability_unmet`
- (a claim's predicate requires non-text evidence — visual/audio/document
- — and no cited evidence of that capability exists; remedy: attach/verify
- the media).
- - **Cached answers are legitimate.** A `synthesize` response with
- `cached: true` was served from the fact-lifecycle-gated answer cache:
- citations were re-validated against live fact rows at serve time, and
- `results` is EMPTY because retrieval never ran. Do not treat the empty
- `results[]` as "no evidence".
- - **Your tool calls may be observed — content-free.** Under
- `TOOL_OBSERVATIONS_ENABLED` the server records per-call observation rows
- (tool name, argument/result DIGESTS, ok flag, duration — no content
- unless the operator additionally opts into a sanitized 512-char excerpt).
- Denied calls record nothing. Reference an observation from
- `ingest_document.toolObservationRef` to link derived documents back to
- the tool result they came from.
+ | Area | Where |
+ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+ | Write path | `src/ingest/` (mentions, entity resolution, conflict resolver), `src/documents/` (Source → Indexer → Candidates), `src/episodes/` (raw substrate) |
+ | Extraction | `src/ai/extractor-*.ts`, `src/ai/extractor-internals/` (lanes), profile in `src/ai/extraction-profile.ts`, predicates in `src/ai/predicate-registry.service.ts` |
+ | Read path | `src/search/` (legs, rerank, `retrieval-profile.ts`), `src/synthesize/` (answer lanes, verifier, citations), `src/multi-hop/`, `src/entities/`, `src/facts/` |
+ | Background | `src/jobs/` (Surreal-native queue), `src/dreams/`, `src/compaction/`, `src/communities/`, `src/scenes/`, `src/beliefs/` |
+ | Connectors | `src/source-plane/`, `src/sources/` |
+ | Operator surface | `src/admin/` (operator routes behind `brain:platform_admin`; tenant pack admin under `brain:admin`), config catalogue `src/admin/config-catalog.data.ts` |
+ | Wire contracts | `src/contracts/` (zod; feed `docs/openapi.json` via `scripts/build-openapi.ts`) |
+ | Domain packs | `src/ai/domain-packs/`, `packs/*.pack.json` ([docs/domain-packs.md](docs/domain-packs.md)) |
- ## Common mistakes
+ Engine layering is enforced by `test/engine-gates.unit-spec.ts`:
+ `episodes ← derive ← search ← synthesize` (no reverse imports), and no
+ `process.env` inside `src/search/`, `src/synthesize/`, `src/ai/extractor-*`
+ except `src/search/retrieval-profile.ts` — engine code takes the resolved
+ profile.
- 1. Omitting `userId` and concluding a user's memory is empty (fail-closed
- scope, see above).
- 2. Passing `asOf` expecting a knowledge-time snapshot ("what did we know at
- T") — on search it is VALID time; knowledge time is `recordedAt` on the
- entity profile and timeline. (On `find_related_entities` alone, `asOf`
- is knowledge time for edges.)
- 3. Re-deriving "what changed" by diffing two full dumps — use `memory_diff`;
- windows are half-open, adjacent windows never double-count.
- 4. Recording multi-claim prose via `record_fact` — use `ingest_document` and
- let extraction + conflict resolution do the work.
- 5. Treating COMPETING facts as noise — they are unresolved disagreements
- awaiting adjudication; surface them to the user when relevant.
+ **Add an MCP tool:** register it with
+ `server.registerTool(name, { title, description, inputSchema }, handler)` in
+ the matching `src/mcp/*-tools.ts` (reads: `read-tools.ts`, writes:
+ `write-tools.ts`; a file near the 800-line lint cap gets a new sibling
+ `<family>-tools.ts` whose `register…Tools()` is called from **both**
+ `McpService.buildServer` in `src/mcp/mcp.service.ts` and
+ `src/mcp/gated-catalogue.ts`). A new service dependency is injected in the
+ `McpService` constructor and its module imported in `src/mcp/mcp.module.ts`.
+ Then: add the action to `ACTIONS` in `src/policy/action-registry.ts` (its
+ `kind` drives the ABAC gate and the MCP annotations) → decide on the `core`
+ profile (`src/mcp/tool-profiles.ts`) and `HEALTH_TOOLS` (`mcp.service.ts`) →
+ tests: add the name to the expected lists in `test/mcp-tools.unit-spec.ts`;
+ `test/mcp-tool-annotations.unit-spec.ts`,
+ `test/mcp-tool-descriptions.unit-spec.ts` (a description says what the tool
+ does, not what the model should do) and
+ `test/mcp-tool-schema-cost.unit-spec.ts` (use `isoDateTime()` from
+ `src/mcp/iso-datetime.ts`, not `z.string().datetime()`) must stay green; an
+ e2e like `test/mcp-fact-tools.e2e-spec.ts` → list it in
+ [docs/agents-using-brain.md](docs/agents-using-brain.md).
- ## Honesty note on eval claims
+ **Add a REST route:** controller in the feature dir with
+ `@UseGuards(ApiKeyGuard)`, `@RequireScopes('brain:read'|…)`,
+ `@PolicyAction('<name>')` (the MCP tool's name when one exists, else a
+ `rest.*` entry in `ACTIONS`; model: `src/facts/facts.controller.ts`), register it
+ in that module's `controllers` → request/response shape as a zod contract in
+ `src/contracts/<area>/` with a `test/contracts-*.unit-spec.ts` parity guard →
+ if integrator-facing, add the path in `scripts/build-openapi.ts` and run
+ `pnpm openapi:build` (`test/openapi-doc.unit-spec.ts` checks both copies) →
+ document in [docs/api.md](docs/api.md).
- When you cite brain's memory-accuracy numbers, cite them with the protocol:
- strict binary judge, own full-context baseline, paired stats, held-out split
- ([docs/eval-protocol.md](docs/eval-protocol.md)). Do not quote numbers from
- other systems' self-reported benchmarks as comparable — most published
- memory scores use lenient judges and are inflated; ours are deliberately
- hard to inflate. The in-repo memory-fitness numbers come from the
- mechanical, judge-free battery (`pnpm eval:memory-fitness`,
- `pnpm eval:state-transitions` — see
- [docs/eval-methodology.md](docs/eval-methodology.md)).
+ **Add a migration (red zone):** `src/db/migrations/<next NNNN>_<what>.surql`,
+ header comment explaining semantics and backfill, idempotent statements.
+ `src/db/schema.surql` is reference-only and not applied. Known SurrealDB
+ traps live in existing migration headers — read the last few first.
- ## More
+ **Add an extraction lane:** pure module in `src/ai/extractor-internals/`
+ (model: `literal-harvest.ts`), wired in `src/ai/extractor-runner.service.ts`,
+ switched by a field on the extraction profile (`src/ai/extraction-profile.ts`),
+ never `process.env` in the lane. An `EXTRACTOR_*` switch is an engine flag →
+ flag budget golden + config catalogue entry → maintainer approval, plus eval
+ evidence ([docs/extraction-harvest-lanes.md](docs/extraction-harvest-lanes.md)).
+ Retrieval/answer lanes: `LaneId` in `src/search/retrieval-profile.ts` and
+ `LANE_REGISTRY` in `src/synthesize/answer-router.ts` (one entry per id, gated).
- - API reference: [docs/api.md](docs/api.md) · data model:
- [docs/data-model.md](docs/data-model.md)
- - Domain packs (tenant ontology extensions, pack-declared MCP tools):
- [docs/domain-packs.md](docs/domain-packs.md),
- [docs/mcp-pack-tools.md](docs/mcp-pack-tools.md)
- - Per-user rolling profile: [docs/user-profile-api.md](docs/user-profile-api.md)
- - Operations & self-hosting: [docs/operations.md](docs/operations.md)
+ **Add an env var / flag:** read booleans only via `envFlagEnabled` /
+ `envFlagNotDisabled` (`src/common/env-validation.ts`); add a
+ `CONFIG_CATALOG` entry whose default matches the reader; document it in
+ `.env.example` and [docs/operations.md](docs/operations.md).
- License: AGPL-3.0-or-later.
+ **Add a UI page:** `brain-landing/app/[lang]/…` (Next app router); admin
+ strings go through `getMessages()`, never JSX literals (`pnpm lint:i18n`).
+
+ ## From idea to PR
+
+ The loop an agent runs **with** the human (in Claude Code: `/contribute`):
+
+ 1. **Restate** the idea in plain words; ask at most 3 clarifying questions.
+ 2. **Locate** it: which files, which zone (table above). Search issues
+ (`gh issue list --search "<words>"`) — reuse an existing one.
+ 3. **Spec** (short): problem · user-visible behaviour · API/MCP/UI change ·
+ data change (migration?) · risks (tenancy, PII, cost, eval impact) · out
+ of scope. **Red zone → STOP**: file it with the "Product idea" issue
+ template, paste the spec, and wait for a maintainer.
+ 4. **Plan** small steps, each leaving the build green.
+ 5. **Implement with tests**, tests first where practical. Behaviour change in
+ retrieval/extraction/synthesis: say which eval battery would measure it and
+ ask before running a paid one.
+ 6. **Run the gates** (Commands → Done).
+ 7. **Docs:** update the doc that describes the behaviour (`docs/*.md`,
+ `.env.example`, `docs/agents-using-brain.md` for the MCP surface). Never
+ edit `CHANGELOG.md`.
+ 8. **Commit** with Conventional Commits — release-please derives the version
+ and changelog from them: `feat(scope): …`, `fix(scope): …`, `docs: …`,
+ `test: …`, `refactor: …`, `perf: …`, `ci: …`, `chore: …`; `!` (e.g.
+ `feat(api)!:`) for a breaking change. Body explains _why_. AI-assisted
+ commits carry a `Co-Authored-By:` trailer.
+ 9. **Open a PR** (`gh pr create`) with the template filled: a plain-language
+ summary a non-engineer can follow, the zone, gates run, tests added,
+ AI disclosure. The PR title must be a Conventional Commit
+ (`pr-title.yml` checks it).
+
+ ## Conventions
+
+ - **Style:** Prettier (single quotes, trailing commas, width 100) +
+ type-aware ESLint (`eslint.config.mjs`): no `any`, no floating promises,
+ `max-lines` 800, `max-lines-per-function` 200, `max-params` 3,
+ `complexity` 25 in `src/`. `noUncheckedIndexedAccess` is on.
+ - **Naming:** kebab-case files by role — `*.service.ts`, `*.controller.ts`,
+ `*.module.ts`, `dto/*.dto.ts`; pure helpers as plain `*.ts`. One class per
+ file.
+ - **Comments:** docblocks explain _why_ (the incident, the measurement, the
+ trap), not _what_. Match the density of the file you edit.
+ - **Errors:** throw Nest HTTP exceptions (`NotFoundException`,
+ `ForbiddenException`, …); a disabled surface answers 404, indistinguishable
+ from an absent route.
+ - **Logging:** `private readonly logger = new Logger(X.name)`; ids and counts,
+ never memory content, keys or tokens.
+ - **Tests:** flat under `test/`, named `<topic>.unit-spec.ts`,
+ `.socket-spec.ts`, `.e2e-spec.ts`, `.real-e2e-spec.ts`.
+ - **Branches:** `feat/…`, `fix/…`, `docs/…`, `ci/…`, `chore/…`. Small PRs; one
+ concern each. Squash-merged with the PR title as the commit.
+ - **Commit subjects** read as the outcome: `fix(registry): a novel predicate
+ is registered once, however many reads coin it at once`.
+
+ ## Testing
+
+ | Suite | Pattern / config | Needs |
+ | ---------------- | ------------------------------------------------------------------------ | ------------------------------------------------- |
+ | Unit | `*.unit-spec.ts`, `test/jest-unit.json` | nothing (no DB, no network) |
+ | Socket | `*.socket-spec.ts`, `test/jest-socket.json` | loopback `listen()` |
+ | E2E | `*.e2e-spec.ts`, `test/jest-e2e.json` | Docker (testcontainers SurrealDB), stubbed models |
+ | Jobs e2e | `jobs.real-e2e-spec.ts` via `pnpm test:e2e:jobs` | Docker; no model keys |
+ | Real e2e / evals | `brain.real-e2e-spec.ts` (`pnpm test:e2e:real`), `test/eval/*/runner.ts` | real provider keys, money — ask first |
+
+ - Unit example: `test/where-builder.unit-spec.ts` (pure function).
+ E2E example: `test/mcp-fact-tools.e2e-spec.ts` — `createApp()` from
+ `test/app-fixture.ts` boots the app against the test DB with a unique
+ `companyId`; restore any env you set in `afterAll`.
+ - Test doubles: `test/test-doubles.ts`; fixtures `test/fixtures/`; goldens
+ `test/golden/`, snapshots `test/__snapshots__/`.
+ - Mock only external systems (OpenAI, Cohere, Surreal in pure unit tests) —
+ never the thing under test.
+ - Many `*-truth.unit-spec.ts` specs pin docs, workflows and configs to the
+ code. When one fails, the claim and the code disagree — fix the one that is
+ wrong.
+ - Mechanical, judge-free batteries: `pnpm eval:memory-fitness`,
+ `pnpm eval:state-transitions` ([docs/eval-methodology.md](docs/eval-methodology.md)).
+
+ ## When to ask a human
+
+ - Anything in a 🔴 zone, or a change to a published API/MCP shape.
+ - A new dependency, env var, feature flag, or MCP tool.
+ - Running a real-model eval, the real e2e suite, or anything that costs money.
+ - A truth/golden spec fails and you are not sure which side is wrong.
+ - Tests or CI fail for reasons you cannot explain after one honest attempt.
+ - The idea conflicts with the docs (bitemporal semantics, "system of insight,
+ not system of record", the no-eval-forks rule).
+ - Anything touching production, deploy, secrets or another tenant's data.
+
+ ## Help
+
+ - Questions: [GitHub Discussions](https://github.com/inite-ai/inite-brain-service/discussions);
+ bugs and ideas: [issues](https://github.com/inite-ai/inite-brain-service/issues/new/choose).
+ - Starter tasks: [`good first issue`](https://github.com/inite-ai/inite-brain-service/issues?q=is%3Aopen+label%3A%22good+first+issue%22).
+ - Human guide: [CONTRIBUTING.md](CONTRIBUTING.md) · docs hub:
+ [docs/README.md](docs/README.md) · architecture:
+ [docs/architecture.md](docs/architecture.md).
+ - Vulnerabilities: [SECURITY.md](SECURITY.md) — never a public issue.