docs: drop two dashes from the contract and fetch rules touched on this branch

Both lines were reworded here for the new gates and still carried a dash from before. They read the same with a colon and a comma.
This commit is contained in:
Maurice
2026-10-08 12:35:24 +02:00
parent a0e1a39acc
commit 5a5349b817
2 changed files with 2 additions and 2 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ Nest owns everything. Every domain is a DI module under `src/nest/<domain>/` (`c
- **Transactions are not optional.** Multi-statement writes go in `await this.uow.transactional(async () => …)` (`UnitOfWork` from `nest/database`, provided by the global `OrmModule`); never hand-roll `BEGIN`/`COMMIT` and never call `db.transaction()` (the Phase 1 async sweep removes it). Every DB-touching method is `async`; `@typescript-eslint/no-floating-promises` and `no-misused-promises` are errors, so an unawaited call or a `Promise` in a condition fails lint. Bind values with `?`; identifiers only via a literal allow-list. In `src/db/repositories/` and `src/db/seeders/`, ESLint refuses raw SQL in any spelling: `raw()`, the `sql` tag, and SQL text handed to `execute`/`run`/`all`/`get`. Build the statement with the query builder; `MaintenanceRepository`, `DemoRepository` and the six existing seeders are the named exceptions in `eslint.config.mjs`, and `tests/unit/eslint-rules/restricted-syntax-selectors.test.ts` pins how many SQL strings each still holds, so they can only lose SQL, never gain it.
- **Migrations are append-only.** A migration is identified by its class name in `mikro_orm_migrations`, and files sort by the timestamp in that name — never rename or delete one that has shipped, and never edit its body; add a new one. No env interpolation in SQL. (The old positional `schema_version` array and the hand-kept `db/migrations.ts` test builder are both gone; the numbered migrations are the only schema history, for tests too.) A new foreign key comes with an index that leads with its column(s), or deleting the parent scans the child table; `tests/unit/db/foreign-key-indexes.test.ts` fails on one without.
- **Every write endpoint validates** through the Zod pipe with a schema from `@trek/shared` — no `Record<string, unknown>` + ad-hoc checks. Also verify every referenced id exists and belongs to the same trip.
- **Every outbound `fetch`** gets a timeout, a response-size cap and boundary validation — no `as`-casting provider responses, no silent `catch → null`. User-influenced URLs go through the SSRF guard. Never fall back to another user's API key. ESLint requires an init with a `signal` on `fetch`, `safeFetch`, `safeFetchFollow` and `safeFetchAdminConfigured` alike, or a literal `timeoutMs` in the options object of `safeFetch`/`safeFetchFollow`; an init spelled `undefined`, `null` or `void 0` counts as none (`safeFetchLlm` takes `LLM_TIMEOUT_MS` inside). Behind that, the guarded wrappers (`utils/ssrfGuard.ts`) cap every body at `DEFAULT_MAX_RESPONSE_BYTES` (64 MB) and, for a call without a signal, wait `DEFAULT_RESPONSE_TIMEOUT_MS` (30 s) for the headers. Pass a tighter `maxBytes` when you buffer a small answer, `timeoutMs` for a deadline over the whole exchange, and `maxBytes: null` only where the body is streamed on or counted by the caller (`pipeAsset`, the doc-sync transfers, the Ollama pull).
- **Every outbound `fetch`** gets a timeout, a response-size cap and boundary validation: no `as`-casting provider responses, no silent `catch → null`. User-influenced URLs go through the SSRF guard. Never fall back to another user's API key. ESLint requires an init with a `signal` on `fetch`, `safeFetch`, `safeFetchFollow` and `safeFetchAdminConfigured` alike, or a literal `timeoutMs` in the options object of `safeFetch`/`safeFetchFollow`; an init spelled `undefined`, `null` or `void 0` counts as none (`safeFetchLlm` takes `LLM_TIMEOUT_MS` inside). Behind that, the guarded wrappers (`utils/ssrfGuard.ts`) cap every body at `DEFAULT_MAX_RESPONSE_BYTES` (64 MB) and, for a call without a signal, wait `DEFAULT_RESPONSE_TIMEOUT_MS` (30 s) for the headers. Pass a tighter `maxBytes` when you buffer a small answer, `timeoutMs` for a deadline over the whole exchange, and `maxBytes: null` only where the body is streamed on or counted by the caller (`pipeAsset`, the doc-sync transfers, the Ollama pull).
- **Fail closed.** Unrecognized flag values mean OFF; sandbox/permission opt-outs refuse in production; never pick a less-safe path based on the *absence* of a file. GET handlers must not write.
- **Don't fork canonical paths.** Auth verification is `verifyJwtAndLoadUser` (with the `password_version` gate) — reuse it.
- **Money**: integer cents; largest-remainder splits (`BudgetService.splitEqualShares` is the reference); FX frozen at entry; explicit flags, never sentinel values; custom splits must reconcile to zero before persisting.
+1 -1
View File
@@ -36,7 +36,7 @@ One folder per domain exporting Zod schemas plus inferred types (`export type X
The parity rule governs **existing** routes; it is not a license to mint new debt:
- **The contract describes the wire, not the storage engine.** New booleans are `z.boolean()` — convert SQLite 0/1 at the service boundary, never add a `z.union([z.boolean(), z.number()])`. New ids use `idSchema`/`idParamSchema`, never `number | string`. Request and response types for a field must agree.
- **Use the shared primitives** instead of re-declaring bare `z.number()`/`z.string()` per domain. Narrow an existing open object rather than adding a new `z.record(...)`/passthrough body — "any object" gives zero drift protection. `contracts:open` (`scripts/open-request-schemas.mjs`) counts the open shapes in every exported `*RequestSchema` (a loose object, a record of unknown, a `z.unknown()`/`z.any()` field or body, a `z.custom()`/`z.instanceof()`, each counted once per path it sits at) against `scripts/open-request-schemas-baseline.json`: a new request schema may hold none, an existing one no more than its entry. It reads the built `dist/`; `src/open-request-schemas.spec.ts` runs the same check over `src/` in `npm test`. After narrowing a schema, `npm run contracts:open -- --update` lowers the baseline (it never raises or adds an entry). The suffix is only a convention, so the server holds the schemas it really binds to the same count: `npm run contracts:dto-open --workspace=server` walks every `createZodDto` class, whatever its schema is called, against `server/scripts/dto-open-shapes-baseline.json`.
- **Use the shared primitives** instead of re-declaring bare `z.number()`/`z.string()` per domain. Narrow an existing open object rather than adding a new `z.record(...)`/passthrough body, because "any object" gives zero drift protection. `contracts:open` (`scripts/open-request-schemas.mjs`) counts the open shapes in every exported `*RequestSchema` (a loose object, a record of unknown, a `z.unknown()`/`z.any()` field or body, a `z.custom()`/`z.instanceof()`, each counted once per path it sits at) against `scripts/open-request-schemas-baseline.json`: a new request schema may hold none, an existing one no more than its entry. It reads the built `dist/`; `src/open-request-schemas.spec.ts` runs the same check over `src/` in `npm test`. After narrowing a schema, `npm run contracts:open -- --update` lowers the baseline (it never raises or adds an entry). The suffix is only a convention, so the server holds the schemas it really binds to the same count: `npm run contracts:dto-open --workspace=server` walks every `createZodDto` class, whatever its schema is called, against `server/scripts/dto-open-shapes-baseline.json`.
- **Add a schema, add a spec** — especially for lenient parsers fed by untrusted or LLM input.
- **Stay lean and isomorphic**: zero imports from client/server, no `node:` APIs, effectively no new runtime dependencies (the contract layer approaches `zod`-only; i18n is a separable concern — don't couple new contract code to it).
- **Never hand-copy a locale list.** `SUPPORTED_LANGUAGES` in `src/i18n/languages.ts` is the one registry — derive barrels, loaders and spec maps from it.