fix(OPS41): resolve marcadores de conflito commitados no AGENTS.md + guardrail anti-reincidência (#689)

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).
This commit is contained in:
Francisco Solla 2026-08-11 20:20:32 -03:00
parent 3ac5dbe0bd
commit d6dfdf70ed
4 changed files with 100 additions and 3 deletions

File diff suppressed because one or more lines are too long

View file

@ -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/<workspace>/canvases/plan-<id>-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/<slug>-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 `<section data-shot="<rótulo>">` (ex. `mobile`, `desktop-vazio`) → `docs/plans/<slug>-ui-draft-<rótulo>.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).

View file

@ -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)

View file

@ -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(
[],
)
})
})