teqo-backup/.agents/rules/engineering-standards.mdc
Francisco Solla 067857a03a fix(OPS124): desabilita o plugin Payload do knip e explicita as exclusões (Closes #1094)
O `pnpm knip` imprimia `ERROR: Error loading src/payload.config.ts
(...server-only...)` e saía 0 — gate verde com o ERROR silencioso, no CI
(`ci-pr.yml`/`deploy.yml`) e local. O plugin embutido do Payload carregava
`src/payload.config.ts` via jiti sob as condições Node padrão e o import
transitivo de `server-only` lançava (só o `react-server` traz o `empty.js`);
o `paths.server-only` do `knip.json` alimenta apenas o resolver estático, não
o carregamento jiti (e o `tsconfig` alias, que funcionaria no load, foi
rejeitado por re-apontar a guarda server/client — classe OPS69).

Fix: `"payload": false` e as contribuições do plugin explicitadas no
`knip.json` (`!src/payload-types.ts` load-bearing; `!src/migrations/**`
paridade), removendo o bloco `paths` inerte. Provado: `pnpm knip` sai 0 sem o
ERROR, um export morto plantado é detectado, e a corrida de controle com
`server-only` neutralizado dá findings idênticos.

Plano: docs/plans/ops-knip-importmap-artifact-impl.md
2026-09-16 19:55:07 -03:00

52 lines
8.5 KiB
Text

---
description: Teqo engineering standards — dead-code hygiene, client-boundary composition, caching ladder, typed escape hatches, zero-warning gate. Read before and enforce after ANY code change.
alwaysApply: true
---
# Engineering standards (Teqo)
Established 2026-07-23 by the engineering-standards hardening (tracker: `docs/IMPROVE-CODE-QUALITY-PLAN.md`; ledger: `docs/TECH-DEBT.md`; safety net: `docs/TESTING.md`). These are gates, not suggestions — CI enforces most of them.
## After every change (non-negotiable)
1. **Dead code dies immediately.** After your edits, check what your change orphaned and delete it in the same delivery: run `pnpm exec knip` (files/dependencies are CI-blocking errors; unused exports are warnings being driven to zero). Never leave commented-out code, unused exports, superseded components, or one-shot scripts behind — git history is the archive.
2. **Zero warnings.** `pnpm lint` runs with `--max-warnings=0`. Deliberate placeholders use a `_` prefix. Never silence a rule without an inline justification comment.
3. **Gate em duas velocidades (desde 2026-07-30, paradigma de agentes paralelos — `docs/AGENT-OPS.md`).** **Fast gate local do agente** (iteração durante a sessão): `pnpm gate:fast` (lint + typecheck + test:unit). **Push canônico:** `pnpm push` — `ensure-repo-deps` (install + Husky em worktrees) + `git push`; o hook pre-push roda `gate:push` → `gate:ci` (espelho serial do `ci-pr.yml` **sem e2e** — OPS72: o e2e é passo **explícito da skill** (`work-issue`/`agent-work-issue`): antes do push rode localmente os e2e que criou + os da mesma superfície, **discricionário** via `pnpm test:e2e:affected`; o CI de PR roda só o blast radius `selected` — nunca `full` — e o full fica no `verify` do deploy (o run nasce no merge — OPS104); **com os `docs-guards` locais** — OPS63/OPS85: changelog additions-only + HISTORY congelado + agregado não-commitado, e marcadores de conflito, mesmos scripts do CI; o escape `changelog-rewrite:` fica só no CI, body do PR). Não use `git push` nu. Fonte única: scripts `gate:*` e `scripts/git-push.mjs` em `package.json`; escape para WIP: `git push --no-verify`. Migrations NÃO são serializadas entre PRs: a cadeia é validada por `pnpm migrate` + `test:int` (incl. `campaignMigrationReconciliation`) em todo PR; rebase antes de `migrate:create` continua obrigatório (o antigo `migration-lock` foi removido em 2026-08-12 — deadlock de CI). Entrega = merge em `main`; publish gated pelo `verify` do `deploy.yml` (início automático no merge, approve humano na produção — OPS104; não há `ci.yml` verificador de main). **CI vermelho no teu PR é teu, mesmo pré-existente ou de infra** — "fora do escopo da feature" não é critério de parada (política "Dono do PR, dono do CI" em `docs/AGENT-OPS.md`).
4. **Run gate commands bare, never piped** (`pnpm test | tail` swallows the exit code). Never run `knip --fix` blind — verify each removal with `git grep -w <symbol>` first (knip's graph is deliberately scoped: the Payload plugin is disabled — `knip.json` `"payload": false`, OPS124 — because it could never load `payload.config.ts` under default Node conditions, so `--fix` can delete exports migrations/tests still reference).
## Type honesty
- **`as never` is banned** (ESLint `no-restricted-syntax`). Use the typed escape hatches that keep property-level checking: `hookFilledCreateData<Slug>()` for create data whose remaining required fields are filled by collection hooks; tests use `stub<T>(partial)` (`tests/helpers/stub.ts`). Casting from `unknown` after a runtime check is acceptable; a justified `eslint-disable-next-line` is the last resort.
- Prefer fixing the type over casting. When a generic can't be proven, restructure so the caller keeps concrete types (see `runStaffEntityMutation` — policy in the helper, typed mutation in the caller).
## Client boundary (Next.js composition)
- **Server-first.** Pages and data assembly are Server Components; `'use client'` only where there is interactivity (hooks, handlers, browser APIs). Static subtrees of an interactive area are passed as `children`/slots from the server (`LeaderContactsPanel` + `LeaderContactForm`, `CampaignDashboard.mapSlot` are the references).
- **Client components must not import server data modules for values.** Types/constants shared with the client live in a client-safe contract module (`municipalityMapContract.ts` is the reference; `municipalityMapData.ts` is `server-only`). `import type` is always safe.
- **State lives where it is used.** Hover/selection in the leaf that renders it (`MunicipalityMapSelection`); context providers wrap only their consumers (estimate-scenario provider wraps results, not filters). Do not lift state so high that siblings re-render.
- Heavy view payloads: pass view models with `select`ed fields, never whole Payload docs.
## Loading feedback (see also `campanha-action-feedback.mdc`)
- Every navigation-triggering control participates in a transition; the RESULTS region dims (`CampaignListPendingBoundary`/`CampaignListResults`), not just the control. Optimistic UI must revalidate on success (`revalidatePath`) or it will lie by reverting.
- Heavy server subtrees stream behind `<Suspense>` with a local skeleton; heavy route segments get their own `loading.tsx`.
## Caching ladder (decide in this order)
1. **React `cache()`** for per-request dedup of repeated reads in one RSC tree (`loadMunicipalityScope`, `getCampaignUser`).
2. **`unstable_cache` + tag** for immutable/slow-changing cross-request data (election reads use tag `election-tse`; bust via `POST /api/revalidate?tag=...` — allowlist in `revalidateRequest.ts`). Auth checks stay OUTSIDE the cached core.
3. **Committed artifact via re-runnable script** for truly immutable derived data (`pnpm build:election-aggregates` → `src/lib/electionAggregates/`, pinned to the catalog by a snapshot test). Never compute artifacts during `pnpm build` — deploys must not depend on DB content.
4. Never long-cache live 2026 campaign data (pledges, expectedVotes) without a write-path invalidation.
## Access control (see also `security-critical.mdc`)
- **Every collection declares `access` explicitly.** Payload's default ("any authenticated user") is a bug here: campaign JWTs work against `/api/*`. Admin-only collections use `payloadAdminOnly`.
- Access helpers live in `src/utilities/access/` per domain, re-exported through `campaignAccess.ts`. New collections get helpers there plus int tests proving the denied paths.
## Module organization
- `src/lib/` = pure logic and static data (client-safe unless marked). `src/utilities/` = Payload/Next-coupled server code (mark `server-only` when server-bound). React components never live in `utilities/`.
- Follow the domain prefixes (`campaign*`, `municipality*`, `supporter*`, `actionPlan*`); the access domain has a real folder (`utilities/access/`).
- **Edit the owner, don't twin.** New behavior lands in the module that already owns that concern (`CampaignTable`, list URL helpers, form-action wrappers, portfolio/relation cells, access helpers, …). A parallel file that reimplements the same machine is a defect unless the delivery names a deliberate policy fork. Prefer changing existing code over adding beside it; delete what your change supersedes in the same delivery (see "Dead code dies immediately"). Product constraints in `AGENTS.md` and frozen migration files are not a ban on editing application code.
- **Visual structure belongs to the `designer`.** Deciding layout, hierarchy, states and visual tokens is the `designer`'s call (`.opencode/agent/designer.md`; doctrine `.agents/skills/plan-issue/ui-design-html.md`); the implementer ports the approved artifact (`docs/plans/<slug>-ui-design.html`) to markup and wires data/routes/queries/copy. A diff that changes visual structure without the designer's verdict is a defect (data/copy wiring on the approved markup is a non-trigger) — the triggers, non-triggers and fail-closed closure live in `execution-pipeline.md` (§Design). This is ownership, not a new module: change the owner, don't improvise the visual structure in code.
- **Reuse vs premature abstraction.** Prefer extending the existing owner. Do **not** invent a new shared abstraction for a speculative third use. If you are about to copy an existing path and the knowledge is the same, **edit the owner** (or extract in the same delivery) — waiting for "call site #3" is how twins spawn. Defer only true look-alikes that differ in policy, with an explicit revisit trigger. Named policy wrappers (`runCampaignRedirectFormAction`, `runStaffEntityMutation`) stay justified even at few call sites.