From d6dfdf70ed15f12f3e273aceb03f79edb6d3d185 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Tue, 11 Aug 2026 20:20:32 -0300 Subject: [PATCH] =?UTF-8?q?fix(OPS41):=20resolve=20marcadores=20de=20confl?= =?UTF-8?q?ito=20commitados=20no=20AGENTS.md=20+=20guardrail=20anti-reinci?= =?UTF-8?q?d=C3=AAncia=20(#689)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O commit OPS33 (f0f5b73f) entrou com o arquivo já conflitado na região do bullet 'Per-worktree environments' (<<<<<<< HEAD indentado + lado HEAD pré-OPS33 com artefato '- # ' + fechamento corrompido '> > > > > > >'; o ======= foi removido no próprio commit) e o OPS37 herdou o estado. Resolução perda zero validada por diff: manter o lado superset (OPS28 + OPS31 + OPS33 — claim determinístico, --issue N, prompt citado) e descartar o lado HEAD; -3 linhas no total; scan de todos os trackeados confirmou AGENTS.md como único arquivo afetado. Guardrail: describe 'no conflict markers in committed files' em codebaseConventions.unit.spec.ts — varre git ls-files e falha em <<<<<<< / >>>>>>> (tolerante a indentação) e na forma corrompida ^\s*(> ){7}; validado negativo contra o estado sujo (AGENTS.md:37, 40). --- AGENTS.md | 3 - docs/CHANGELOG-AGENTS.md | 2 + .../agents-md-sem-marcadores-conflito-impl.md | 70 +++++++++++++++++++ tests/unit/codebaseConventions.unit.spec.ts | 28 ++++++++ 4 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 docs/plans/agents-md-sem-marcadores-conflito-impl.md diff --git a/AGENTS.md b/AGENTS.md index 3e6e711a..f9a0799d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,10 +35,7 @@ These are product/architecture constraints — not a ban on editing existing cod - `Consent.text` was reconciled from `varchar` to `jsonb` via `20260715_163500_consent_text_to_jsonb` (matching the `richText` field). Any future field-type change is a migration, not a manual DB edit. - To copy production content locally, use `pnpm db:pull` (`scripts/db-pull.mjs`): it only reads prod, only writes local, and excludes supporter PII (`contact`/`signature`/`subscription` row data). - `pnpm db:doctor` diagnoses local database connectivity for the dev AND test targets: it names who holds the port, points at exited/crash-looping containers, and flags Postgres containers sharing one data volume (two postmasters on one PGDATA corrupt the WAL — this destroyed the local cluster's checkpoint on 2026-07-25; the canonical setup is ONE compose container on 5432 hosting `teqo` + `teqo_test`). `pnpm dev` runs the same preflight via `guard-dev-db` and refuses to start with a named remedy instead of a later ECONNREFUSED. `db:start`/`db:stop` pin the compose project with `-p teqo`, so they always target that ONE container even from a worktree directory. **`db:start` is idempotent (OPS37):** with `teqo-postgres-1` up and healthy it is a no-op — a plain `compose up` from another worktree would recreate the shared container and kill every live dev/e2e connection (the compose file's relative `./docker/postgres/init` bind mount makes the config hash differ per worktree); apply a real config/image change on purpose with `pnpm db:start --force-recreate`. - <<<<<<< HEAD -- # **Per-worktree environments (`pnpm worktree next`, 2026-08-08; default-go 2026-08-10):** parallel agents each get a deterministic isolated env — no fighting over port 3000, the shared `teqo_test`, or a second Postgres container. `worktree next [--stay]` provisions: a dev-server port `3100+slot` (slot = numeric part of the issue code, hash fallback, bumped on collision among live worktrees), plus dedicated databases `teqo_wt` (dev) and `teqo_wt_test` (tests) created on the shared container with the full migration chain AND the minimal seed applied (`pnpm migrate` + `pnpm db:seed:minimal` per database — OPS28 parity: CI runs the same pair before e2e, so a worktree's `pnpm test:e2e` mirrors CI instead of failing on seed-dependent specs; the seed is idempotent and local-guarded, and `--no-migrate` skips both); writes `.env.local` + `.env.test.local` (gitignored, marker `# teqo worktree env (generated by pnpm worktree next)`; manual envs are never overwritten) and copies `PAYLOAD_SECRET`/Blob/VAPID from the main repo. **`next`/`plan`/`new` print `cd ` as the last line BY DEFAULT** — the caller applies it (shell function `worktree()` in `.agents/shell/worktree.sh`, or the `/worktree` opencode command); `--stay` suppresses the cd, `--go` explicit stays accepted as a no-op (it was the old default), and `kill` ends with `cd
` so the shell never sits in a destroyed directory. **In the interactive terminal the shell function sets `TEQO_WORKTREE_TERMINAL=1` and `next`/`plan`/`new` additionally print a `launch opencode --model deepseek/deepseek-v4-flash --auto [--prompt ]` directive BEFORE the `cd` line, which the function executes after the cd (OPS26, 2026-08-10; OPS31 for `plan`): `next` opens the opencode TUI in the worktree with `/work-issue` already sent (the OPS25 command runs the whole cycle), `plan` opens with `/plan-issue` already sent (the TUI lands straight in the planning flow), `new` opens with no prompt ("apenas conversar", no skill — the opencode CLI's `--prompt` always auto-submits, there is no prefill-without-submit; that stays an upstream-only feature request, annotated in the opencode proposition). The launch is without `exec` (exiting the TUI returns to the shell in the worktree, no auto-`kill`), `--stay` suppresses both the cd and the launch, the presets are constants in `scripts/lib/worktree.mjs` (`OPENCODE_PRESET_MODEL`, `OPENCODE_SKILL_COMMAND_BY_PURPOSE`, unit-tested), and **the `/worktree` opencode command never launches a TUI** (no marker — no nested TUI). `worktree plan [bag] [--stay]` (2026-08-08) is the sibling for the `/plan-issue` skill: **each invocation creates a DIFFERENT planning worktree** so parallel `/plan-issue` sessions never share one — branch `plans/plan-issue-` when a `bag` slug is given (suffixed `-2`/`-3` if that name is taken) or the next free sequential `plans/plan-issue-` when not (`PLAN_BRANCH_PREFIX`/`planBranchName`, `scripts/lib/worktree.mjs`); the lowercase `plans/…` prefix deliberately never matches `next`'s uppercase-led `-`, so a later `next` never collides in branch or slot. `worktree new [bag] [--stay]` (OPS27, 2026-08-10) is the NEUTRAL sibling: a worktree with **no pre-defined function\*\* (explore an idea, chat, plan without registering anything) — also a DIFFERENT one per invocation, branch `work/` (suffixed `-2`/`-3` on collision) or the next free sequential `work/` (`WORK_BRANCH_PREFIX`/`workBranchName`, same `namespaceBranchName` core as `plan`); the lowercase `work/…` prefix collides with neither `next` nor `plan`, and the provisioning is identical with `purpose: 'new'` (env comment `gerado por pnpm worktree new`; the frozen env marker keeps its historical `…next` text). `kill` drops the worktree's generated databases best-effort. Tests read `.env.test.local` after `.env.test` (vitest.setup.ts / playwright.config.ts / db-doctor / gate-ci mirror the layering), and `assertTestDatabase` accepts `teqo_test` or `teqo__test` on localhost only — still fail-closed on remote. Pure derivation lives in `scripts/lib/worktree-env.mjs` (unit-tested). Without Docker (Cursor Cloud) it degrades to the shared `teqo`/`teqo_test` with a warning — and still runs migrate + seed mínimo on those shared databases (same parity; the seed re-pins campaignGoals and 5 municipalities there, overwriting manual tweaks). - **Per-worktree environments (`pnpm worktree next`, 2026-08-08; default-go 2026-08-10):** parallel agents each get a deterministic isolated env — no fighting over port 3000, the shared `teqo_test`, or a second Postgres container. **`next` claims BEFORE creating the worktree (OPS33, 2026-08-10):** the same queue/order and optimistic lock as `pnpm agent:claim` (shared `claimIssue` in `scripts/lib/agent-github.mjs`); a failed claim prints the reason and stops with no orphan worktree; `--issue ` claims the directed issue (`ready`) or **reopens** an already-claimed one (`in-progress`, no re-claim, worktree reuse stays idempotent); the claim brief is printed and the output warns "já claimada — não rodar `pnpm agent:claim`" (`plan`/`new`/`kill` never touch Issues; the pool's coordinated `claimIssueForPool` is untouched). `worktree next [--issue N] [--stay]` provisions: a dev-server port `3100+slot` (slot = numeric part of the issue code, capped at 999 with hash fallback, bumped on collision among live worktrees), plus dedicated databases `teqo_wt` (dev) and `teqo_wt_test` (tests) created on the shared container with the full migration chain AND the minimal seed applied (`pnpm migrate` + `pnpm db:seed:minimal` per database — OPS28 parity: CI runs the same pair before e2e, so a worktree's `pnpm test:e2e` mirrors CI instead of failing on seed-dependent specs; the seed is idempotent and local-guarded, and `--no-migrate` skips both); writes `.env.local` + `.env.test.local` (gitignored, marker `# teqo worktree env (generated by pnpm worktree next)`; manual envs are never overwritten) and copies `PAYLOAD_SECRET`/Blob/VAPID from the main repo. **`next`/`plan`/`new` print `cd ` as the last line BY DEFAULT** — the caller applies it (shell function `worktree()` in `.agents/shell/worktree.sh`, or the `/worktree` opencode command); `--stay` suppresses the cd, `--go` explicit stays accepted as a no-op (it was the old default), and `kill` ends with `cd
` so the shell never sits in a destroyed directory. **In the interactive terminal the shell function sets `TEQO_WORKTREE_TERMINAL=1` and `next`/`plan`/`new` additionally print a `launch opencode --model deepseek/deepseek-v4-flash --auto [--prompt "/work-issue --issue "]` directive BEFORE the `cd` line, which the function executes after the cd (OPS26, 2026-08-10; OPS33: `next` sends `/work-issue --issue ` — the claimed issue — with the prompt value quoted, and the shell tokenizes the line via xargs, quote-aware, never eval; OPS31 for `plan`): `next` opens the opencode TUI in the worktree with `/work-issue --issue ` already sent (the OPS25 command runs the whole cycle; the skill reads the rest from the Issue), `plan` opens with `/plan-issue` already sent (the TUI lands straight in the planning flow), `new` opens with no prompt ("apenas conversar", no skill — the opencode CLI's `--prompt` always auto-submits, there is no prefill-without-submit; that stays an upstream-only feature request, annotated in the opencode proposition). The launch is without `exec` (exiting the TUI returns to the shell in the worktree, no auto-`kill`), `--stay` suppresses both the cd and the launch (the claim still happens), the presets are constants in `scripts/lib/worktree.mjs` (`OPENCODE_PRESET_MODEL`, `OPENCODE_SKILL_COMMAND_BY_PURPOSE`, unit-tested), and **the `/worktree` opencode command never launches a TUI** (no marker — no nested TUI). `worktree plan [bag] [--stay]` (2026-08-08) is the sibling for the `/plan-issue` skill: **each invocation creates a DIFFERENT planning worktree** so parallel `/plan-issue` sessions never share one — branch `plans/plan-issue-` when a `bag` slug is given (suffixed `-2`/`-3` if that name is taken) or the next free sequential `plans/plan-issue-` when not (`PLAN_BRANCH_PREFIX`/`planBranchName`, `scripts/lib/worktree.mjs`); the lowercase `plans/…` prefix deliberately never matches `next`'s uppercase-led `-`, so a later `next` never collides in branch or slot. `worktree new [bag] [--stay]` (OPS27, 2026-08-10) is the NEUTRAL sibling: a worktree with **no pre-defined function\*\* (explore an idea, chat, plan without registering anything) — also a DIFFERENT one per invocation, branch `work/` (suffixed `-2`/`-3` on collision) or the next free sequential `work/` (`WORK_BRANCH_PREFIX`/`workBranchName`, same `namespaceBranchName` core as `plan`); the lowercase `work/…` prefix collides with neither `next` nor `plan`, and the provisioning is identical with `purpose: 'new'` (env comment `gerado por pnpm worktree new`; the frozen env marker keeps its historical `…next` text). `kill` drops the worktree's generated databases best-effort. Tests read `.env.test.local` after `.env.test` (vitest.setup.ts / playwright.config.ts / db-doctor / gate-ci mirror the layering), and `assertTestDatabase` accepts `teqo_test` or `teqo__test` on localhost only — still fail-closed on remote. Pure derivation lives in `scripts/lib/worktree-env.mjs` (unit-tested). Without Docker (Cursor Cloud) it degrades to the shared `teqo`/`teqo_test` with a warning — and still runs migrate + seed mínimo on those shared databases (same parity; the seed re-pins campaignGoals and 5 municipalities there, overwriting manual tweaks). - > > > > > > > 9e18bec8 (OPS33: worktree next claima deterministicamente a Issue (--issue N, reopen) e launch com /work-issue --issue N) - To (re)populate news content, `pnpm db:seed:posts` fetches articles live from jorgesolla.com.br into the LOCAL db (same non-local `DATABASE_URL` guard as `pnpm dev`). It writes from a CLI process outside the deployed runtime, so after seeding — or after ANY direct-DB change — you must bust the production `posts` cache via `POST /api/revalidate`. See "Posts & Tags" below. - To (re)populate the TSE 2022 election baseline, `pnpm db:seed:tse` downloads TSE open data and imports the Bahia scope into the LOCAL db (same guard). Since the 2026-07-23 hardening, elections-tab reads are cached under the `election-tse` tag — after re-seeding a database the deployed app reads, bust it via `POST /api/revalidate?tag=election-tse`. See "Election baseline data (TSE 2022)" below. diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index fa7b14a1..3bc173ef 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- +**Recently resolved (2026-08-11):** **OPS41** (Issue #689) — o `AGENTS.md` de main carregava **marcadores de conflito commitados** no bullet "Per-worktree environments" (` <<<<<<< HEAD` indentado + lado HEAD pré-OPS33 com artefato `- # ` + fechamento corrompido `> > > > > > > 9e18bec8`; o `=======` foi removido no próprio commit). Origem: o commit OPS33 f0f5b73f entrou com o arquivo **já conflitado** (resolve do conflito OPS33×OPS28/OPS31 commitado com marcadores); OPS37 herdou o estado. Resolução **perda zero validada por diff**: manter o lado superset (OPS28 + OPS31 + OPS33 — claim determinístico, `--issue N`, prompt citado) e descartar o lado HEAD; −3 linhas no diff; scan whitespace-tolerant de todos os arquivos trackeados confirmou `AGENTS.md` como único afetado. **Guardrail anti-reincidência** (2ª vez que marcador chega a main): novo describe `no conflict markers in committed files` em `tests/unit/codebaseConventions.unit.spec.ts` — varre `git ls-files`, falha em `<<<<<<<`/`>>>>>>>` (tolerante a indentação) e na forma corrompida commitada `^\s*(> ){7}`; roda no CI via `pnpm test:unit`. Sem migration/access/Consent/UI/schema. Intenção: body da Issue (sem plano linkado); impl [`agents-md-sem-marcadores-conflito-impl.md`](plans/agents-md-sem-marcadores-conflito-impl.md). + **Recently resolved (2026-08-11):** **Plan-issue — rascunho UI/UX do gate vira HTML+Tailwind commitado no repo (ad-hoc, decisão do usuário).** O artefato deixa de ser **Cursor canvas** (`~/.cursor/projects//canvases/plan--ui-draft.canvas.tsx`, fora do repo, só renderizava no IDE de quem planejou e apodrecia) e passa a ser **HTML + Tailwind v4** (`docs/plans/-ui-draft.html` — `@tailwindcss/browser@4` via jsdelivr, mesma major do app 4.1.x; rede só na hora de renderizar) **commitado com o plano** e **renderizado em PNG** por `pnpm ui-draft:render` (`scripts/render-ui-draft.mjs`, Chromium do Playwright via `@playwright/test`, fallback `load` se o CDN não settle — nunca toca banco/app): uma PNG por cena `
` (ex. `mobile`, `desktop-vazio`) → `docs/plans/-ui-draft-.png`, ou full-page sem `data-shot`; PNGs **embutidos no plano** (visíveis no chat do gate E no PR — o aceite vira registro permanente; o HTML é a fonte iterável). **Contrato de fidelidade explícito em `ui-draft-html.md`** (substitui `ui-draft-canvas.md`, deletado; refs atualizadas em SKILL.md/frontmatter, intention-template.md — campo renomeado `Canvas UI:` → `Rascunho UI:` + secção de embed, skills-map.md, shaping.md): o rascunho DEVE ter layout real, espaçamentos/tamanhos na escala do app (`p-4`, `gap-3`, `h-11`, `text-sm`), breakpoints reais (390/1280) e estados críticos como cenas estáticas; é PROIBIDO ser funcional (zero JS de comportamento — cada estado é cena separada), usar código de app (`src/`), brand/polish (gradientes/emojis/sombras/logo/paleta do produto — cinza neutro + um acento), mock de dados realistas ou decisão de engenharia. Gate: iterar HTML → re-render → embutir até confirmação explícita. Verificado: render real do skeleton (classes v4 aplicadas — oklch zinc, rounded-lg 8px, text-lg 18px). Sem migration/access/Consent/UI de produto/schema. Sem issue — ad-hoc no worktree `work/5`. **Recently resolved (2026-08-11):** **OPS40** (Issue #682) — `pnpm dev` honra o `PORT` do `.env.local`: `next dev` resolve a porta **antes** de `@next/env` carregar env-files (commander `.env('PORT')` no `bin/next` — causa confirmada no fonte 15.4.11), então o `PORT=3100+slot` do provisionador de worktree era ignorado e todo dev server subia em 3000 (colisão entre worktrees paralelos; o e2e escapava porque o webServer injeta `PORT` no env real). **`pnpm dev`/`devsafe` agora passam por `scripts/dev.mjs`** (wrapper): importa o `guard-dev-db.mjs` (preflight fail-closed inline, mesma saída), `loadCliEnv()` (`.env.local` → `.env`, `override:false`) e entrega a porta ao `next dev` como **flag CLI `-p`** — a única alavanca que o repo controla. Precedência: env real do processo (webServer do Playwright manda) → `.env.local` (worktree) → `.env` → sem `-p` (default 3000 + `allowRetry` do Next intactos no main repo). Derivação pura unit-testada em `scripts/lib/cli.mjs` (`resolveDevPort` — null quando ausente, throw fail-closed em PORT inválida; `nextDevArgs`); spawn `node next/dist/bin/next` com `NODE_OPTIONS=--no-deprecation` (paridade cross-env), stdio inherit, forward SIGINT/SIGTERM/SIGHUP, propagação de exit code; extras (`--turbopack`) repassados após o `-p` derivado. Sem migration/access/Consent/UI. Verificação: boot manual no worktree → "Local: http://localhost:3140" (antes 3000), unit 8/8 novos, e2e dev-mode `campaignAuth` 3/3 (2 runs de prewarm estouraram o orçamento sob load ~50+ da máquina — flake de contenção fichado B17/B27/B32/B42, terceira run verde com cache quente). Nota fora de escopo: `AGENTS.md` em `main` carrega marcadores de conflito commitados (OPS33/OPS37) — área de main-mess da OPS38; o novo bullet do Database section foi adicionado **fora** da região conflitada. Intenção [`worktree-dev-port-nao-honrado.md`](plans/worktree-dev-port-nao-honrado.md); impl [`worktree-dev-port-nao-honrado-impl.md`](plans/worktree-dev-port-nao-honrado-impl.md). diff --git a/docs/plans/agents-md-sem-marcadores-conflito-impl.md b/docs/plans/agents-md-sem-marcadores-conflito-impl.md new file mode 100644 index 00000000..aa2c5bf5 --- /dev/null +++ b/docs/plans/agents-md-sem-marcadores-conflito-impl.md @@ -0,0 +1,70 @@ +# Impl: AGENTS.md em main com marcadores de conflito commitados (região Per-worktree environments — merge do OPS33) + +Status: aprovado +Atualizado em: 2026-08-11 +Issue: #689 +Intenção: sem plano de intenção — o body da Issue é a spec (chore pré-existente descoberto no gate do OPS40) +Appetite restante: herdado (chore P2, docs-only — appetite curto) + +## Leitura da intenção + +- **Outcome:** origin/main's `AGENTS.md` carrega ZERO artefatos de conflito (marcadores, lados duplicados); o bullet "Per-worktree environments" existe exatamente uma vez com o texto superset (OPS28 + OPS31 + OPS33). Verificação da Issue: `git show origin/main:AGENTS.md | grep -n '<<<<<<<'` → vazio. +- **O que NÃO negociar:** nada de produto aqui; frozen do repo: migrations (não tocadas), código de runtime (não tocado), histórico de merges (não reescrito). Escopo docs + guardrail de CI. +- **O que reavaliar:** a hipótese da Issue ("manter o lado `9e18bec8`, descartar o HEAD") foi **validada por diff completo** — ver evidência abaixo. O rótulo do marcador é enganoso (o lado "theirs" não é o blob cru `9e18bec8:AGENTS.md`, e sim o texto de main pós-OPS28/OPS31 + adições OPS33), mas a decisão de conteúdo é a mesma. + +## Evidência (coletada no Plan mode) + +- Conflito presente em origin/main == HEAD local (`1ed585a0`): linhas 37–40 do `AGENTS.md`: + - L37: ` <<<<<<< HEAD` (indentado 2 espaços — por isso a verificação coluna-0 da Issue só achou 1 linha) + - L38: lado HEAD = bullet pré-OPS33 (contém OPS28 + OPS31, sem claim) — com artefato `- # ` (hífen+hash) do resolve mal feito + - L39: lado "theirs" = superset (OPS28 + OPS31 + OPS33: sentença de claim, `--issue N`, prompt citado, "(the claim still happens)") + - L40: ` > > > > > > > 9e18bec8 (OPS33: …)` (marcador de fechamento com espaços + indentado; o `=======` do conflito original foi removido no commit) +- **Superset provado:** diff de L38 (normalizada `- # `) vs L39 (normalizada `- `) → **única** diferença são as sentenças OPS33 em L39. Manter L39 é perda zero. +- **Único arquivo afetado:** scan whitespace-tolerant de todos os arquivos trackeados em origin/main (`^\s*<<<<<<< `, `^\s*>>>>>>> `) → só `AGENTS.md`. +- **Origem:** o commit OPS33 f0f5b73f entrou com o arquivo já conflitado (o resolve do conflito OPS33×OPS28/OPS31 foi commitado com marcadores); OPS37 (7c17d437) herdou o estado sujo (branch cortada depois, não tocou a região). + +## Abordagem recomendada + +```mermaid +flowchart LR + A[Editar AGENTS.md: remover L37+L38+L40] --> B[git diff: só 3 linhas removidas] + B --> C[Guardrail: spec codebaseConventions] + C --> D[CHANGELOG-AGENTS: entrada OPS41] + D --> E[Gates + PR auto-merge] +``` + +**Opções consideradas:** A (edição direta do working tree) | B (checkout do blob histórico `9e18bec8:AGENTS.md` e re-aplicar) | C (manter lado HEAD e re-adicionar texto OPS33 à mão) +**Recomendação:** **A** — a região conflitada é a única diferença entre o working tree e qualquer versão histórica; edição de 3 linhas com diff verificável. +**Rejeitadas:** B (o blob `9e18bec8` é um commit irmão do OPS33 fora da história de main — difere de L39 em OPS28/OPS31 e faltaria re-aplicar o merge; mais risco de regressão que o valor de "fonte canônica"); C (reescrever texto OPS33 à mão reintroduz risco de erro de cópia). + +### Componentes / mudanças + +- **`AGENTS.md`** (raiz): remover L37 (marcador abertura), L38 (lado HEAD pré-OPS33 com artefato `- # `), L40 (marcador fechamento espaçado). Manter L39 (superset). Resultado: bullet único com o texto OPS33 completo. +- **`tests/unit/codebaseConventions.unit.spec.ts`**: novo describe `"no conflict markers in committed files"` — o owner natural dos guards programáticos de convenção (o artefato entrou em main **duas** vezes: esta e a janela que o OPS38 restaurou). Varre o working tree (exclui `.git`, `node_modules`, `.next`, `data/`), falha em `^\s*<<<<<<< `, `^\s*>>>>>>> ` e na forma corrompida commitada `^\s*(> ){7}` (o `=======` original foi removido no artefato e é ambíguo com setext — não entra na regra). Allowlist-style para caso legítimo futuro (prosa que cite marcadores), mesmo padrão do `banned campaign terminology`. +- **Migration:** sem migration (nenhum schema). +- **Access / Consent:** nenhum. +- **UI:** nenhuma. + +## Fases verificáveis + +1. **Fix do AGENTS.md** — remover as 3 linhas; verificar: `git diff` mostra exatamente −3 linhas (sem tocar no resto), `grep -n '<<<<<<<' AGENTS.md` vazio, região re-renderiza como um bullet. +2. **Guardrail + CHANGELOG** — describe novo na spec; entrada curta "Recently resolved (2026-08-11): OPS41 …" em `docs/CHANGELOG-AGENTS.md` (padrão das entradas OPS36/OPS37). +3. **Gates** — `pnpm gate:fast` (lint + typecheck + test:unit — a spec nova roda aqui), `pnpm format:check` (Prettier), `pnpm exec knip`, `pnpm check:cycles`. Int/e2e não se aplicam (zero `src/`, zero runtime). Push via `pnpm push` → PR em main com auto-merge. + +## Rabbit holes / Não escopo (engenharia) + +- Reconstruir o histórico de merges OPS33/OPS37 (dois commits homônimos `9e18bec8`/`f0f5b73f`, resolução confusa) — história frozen, irrelevante para o fix. +- Caçar `=======` como separador de conflito — ambíguo com setext heading; o vetor real (git commitando arquivo conflitado) sempre carrega `<<<<<<<`/`>>>>>>>`. +- Regra de pre-commit hook git — o guardrail em vitest roda em CI e local (`pnpm test`), suficiente. + +## Riscos e mitigação + +- **Perda de conteúdo na resolução:** mitigado — superset provado por diff e verificação pós-fix (diff = −3 linhas). +- **Reincidência (3ª vez):** mitigado pelo guardrail — qualquer merge futuro que commite marcadores falha no `test:unit` do CI. +- **Falso positivo do guardrail em prosa legítima:** mitigado — allowlist no padrão da spec de termos banidos. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção coberto (zero marcadores em origin/main; bullet único superset) +- [x] Invariantes AGENTS/engineering-standards (docs-only; migrations/histórico intocados) +- [x] Guardrail de regressão testado (o próprio teste falharia no estado atual — validar rodando contra o arquivo sujo antes do fix) diff --git a/tests/unit/codebaseConventions.unit.spec.ts b/tests/unit/codebaseConventions.unit.spec.ts index 7c27af90..7d4f0ade 100644 --- a/tests/unit/codebaseConventions.unit.spec.ts +++ b/tests/unit/codebaseConventions.unit.spec.ts @@ -1,3 +1,4 @@ +import { execSync } from 'node:child_process' import { existsSync, readdirSync, readFileSync } from 'node:fs' import { basename, relative, resolve } from 'node:path' import { describe, expect, it } from 'vitest' @@ -735,3 +736,30 @@ describe('public site metadata global access', () => { expect(offenders, 'import and call resolveSiteMetadata from @/utilities/seo').toEqual([]) }) }) + +describe('no conflict markers in committed files', () => { + // A real git conflict was committed to AGENTS.md twice: the OPS33 commit + // carried `<<<<<<< HEAD` plus a mangled `> > > > > > >` closer (the + // `=======` was deleted in the same bad resolution), and OPS37 inherited + // the state. The scan reads tracked paths from the index and greps the + // working tree, so a bad resolution fails here before it is ever pushed. + // The `(?:> ){7}` form is the exact corrupted closer that shipped; a + // genuine 7-deep nested blockquote would need a deliberate allowlist. + const conflictMarker = /^\s*<<<<<<<(?:\s|$)|^\s*>>>>>>>(?:\s|$)|^\s*(?:> ){7}/m + + it('keeps conflict markers out of tracked files', () => { + const offenders: string[] = [] + + for (const file of execSync('git ls-files', { encoding: 'utf8' }).split('\n')) { + if (!file) continue + const lines = readFileSync(resolve(repoRoot, file), 'utf8').split('\n') + for (const [index, line] of lines.entries()) { + if (conflictMarker.test(line)) offenders.push(`${file}:${index + 1}`) + } + } + + expect(offenders, 'resolve the conflict before committing; never commit marker lines').toEqual( + [], + ) + }) +})