From 5af626ebbf3e9d368350d28d73009aad1125dc2c Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Tue, 18 Aug 2026 18:00:00 -0300 Subject: [PATCH] =?UTF-8?q?ops(OPS63):=20docs-guards=20no=20pre-push=20?= =?UTF-8?q?=E2=80=94=20changelog=20append-only,=20agregado=20e=20marcadore?= =?UTF-8?q?s=20de=20conflito=20no=20gate:ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/rules/agent-pr-workflow.mdc | 2 +- .agents/rules/engineering-standards.mdc | 2 +- docs/AGENT-OPS.md | 2 +- docs/CHANGELOG-AGENTS.md | 2 + docs/changelog/2026-08-18-ops63.md | 1 + .../plans/ops63-doc-guard-no-pre-push-impl.md | 65 +++++++++++++++++++ scripts/gate-ci.mjs | 13 +++- 7 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 docs/changelog/2026-08-18-ops63.md create mode 100644 docs/plans/ops63-doc-guard-no-pre-push-impl.md diff --git a/.agents/rules/agent-pr-workflow.mdc b/.agents/rules/agent-pr-workflow.mdc index 16d6a85b..000bad39 100644 --- a/.agents/rules/agent-pr-workflow.mdc +++ b/.agents/rules/agent-pr-workflow.mdc @@ -37,7 +37,7 @@ When the environment says to use `ManagePullRequest` instead of `gh pr create`: **Use `pnpm push`, never bare `git push`.** Não depende do Husky estar wired: 1. `ensure-repo-deps` — `pnpm install` se `node_modules` faltar/stale + `pnpm run prepare` (Husky) se hooks não estiverem apontando para `.husky` -2. `pnpm gate:push` — espelho do `ci-pr.yml` sem e2e (e2e roda uma vez, no CI, após o push; local opcional: `pnpm test:e2e:affected`) +2. `pnpm gate:push` — espelho do `ci-pr.yml` sem e2e (e2e roda uma vez, no CI, após o push; local opcional: `pnpm test:e2e:affected`), com os `docs-guards` locais (OPS63: changelog append-only, agregado sync e marcadores de conflito — o escape `changelog-rewrite:` fica só no CI, body do PR) 3. `git push --no-verify` — só porque o gate já rodou no passo 2 (não é escape hatch) ```bash diff --git a/.agents/rules/engineering-standards.mdc b/.agents/rules/engineering-standards.mdc index 7b8ae662..efb353be 100644 --- a/.agents/rules/engineering-standards.mdc +++ b/.agents/rules/engineering-standards.mdc @@ -11,7 +11,7 @@ Established 2026-07-23 by the engineering-standards hardening (tracker: `docs/IM 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** — OPS59: o e2e roda **uma vez, no CI**, em PR e na main; local opcional para feedback antecipado: `pnpm test:e2e:affected`). 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 e na main; 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`; deploy gated pelo verificador `ci.yml`. **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`). +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** — OPS59: o e2e roda **uma vez, no CI**, em PR e na main; local opcional para feedback antecipado: `pnpm test:e2e:affected`; **com os `docs-guards` locais** — OPS63: changelog append-only, agregado sync 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 e na main; 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`; deploy gated pelo verificador `ci.yml`. **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 ` first (knip still cannot load `payload.config.ts`, so its graph is incomplete; ledgered as P3). ## Type honesty diff --git a/docs/AGENT-OPS.md b/docs/AGENT-OPS.md index b99fb4ef..18432634 100644 --- a/docs/AGENT-OPS.md +++ b/docs/AGENT-OPS.md @@ -57,7 +57,7 @@ Labels: `ready|in-progress|blocked|done|in-prod`, `prio:*`, `kind:*`, `needs:*`. 1. Schema → atualizar `db:seed:minimal` no mesmo PR (`needs:migration`). 2. Migrations NÃO são serializadas entre PRs: o CI (migrate + int, incl. `campaignMigrationReconciliation`) valida a cadeia em todo PR e na main; rebase antes de `migrate:create` continua obrigatório. 3. Ready + auto-merge: `gh pr create --base main` → `gh pr merge --auto --rebase`; `gh pr checks --watch --required` (Vercel Git não é gate). **Merge é por rebase** (nunca merge commit): preserva a linha histórica limpa do `main` e é o modo que o CI guarda contra conflitos latentes. -4. **Changelog da entrega (OPS44):** toda entrega escreve `docs/changelog/-.md` (ex. `2026-08-13-ops44.md`) e roda `pnpm changelog:build` antes do push — o agregado `docs/CHANGELOG-AGENTS.md` é **insert-only** (nunca remove/reescreve entradas; entradas históricas não são migradas). Guard de CI `docs-guards`: (a) agregado é append-only (multiset sobre blobs — perda de linha falha); (b) `docs/changelog/` é additions-only (M/D falham); (c) agregado está up to date com `docs/changelog/` (`changelog:check`); (d) nenhum marcador de conflito em diffs de markdown (docs/, AGENTS.md, .agents/ — cobre PRs docs-only, onde a suíte unit não roda). Restauração legítima (header muda, restauração estilo D8) escapa escrevendo **`changelog-rewrite: ` como linha própria** no body do PR — definição canônica aqui; o texto do checkbox do PR template nunca ativa (linha-ancorado). A própria troca `--merge` → `--rebase` deste doc usou o escape — header do agregado mudou. +4. **Changelog da entrega (OPS44):** toda entrega escreve `docs/changelog/-.md` (ex. `2026-08-13-ops44.md`) e roda `pnpm changelog:build` antes do push — o agregado `docs/CHANGELOG-AGENTS.md` é **insert-only** (nunca remove/reescreve entradas; entradas históricas não são migradas). Guard de CI `docs-guards`: (a) agregado é append-only (multiset sobre blobs — perda de linha falha); (b) `docs/changelog/` é additions-only (M/D falham); (c) agregado está up to date com `docs/changelog/` (`changelog:check`); (d) nenhum marcador de conflito em diffs de markdown (docs/, AGENTS.md, .agents/ — cobre PRs docs-only, onde a suíte unit não roda). **Desde OPS63 os mesmos três checks rodam também no pre-push local** (`gate:push` → `gate:ci`, fase 1 — mesmos scripts, default `origin/main`). Restauração legítima (header muda, restauração estilo D8) escapa escrevendo **`changelog-rewrite: ` como linha própria** no body do PR — definição canônica aqui; o texto do checkbox do PR template nunca ativa (linha-ancorado). **O escape é CI-only (body do PR):** no pre-push local a restauração legítima falha com a mesma mensagem do CI — bypass documentado é `git push --no-verify` direto (o CI, com o escape no body, continua sendo a via da restauração). A própria troca `--merge` → `--rebase` deste doc usou o escape — header do agregado mudou. ## Registries compartilhados (serialização por arquivo, não por PR) diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index ec4d4d8e..682b6419 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -47,6 +47,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente **Recently resolved (2026-08-18):** **OPS61** — contratos de merge/flip do Forgejo consertados. **(S1)** Os workflows pós-merge (`issue-done-on-main-merge.yml`, `plan-issue-ready-on-main-merge.yml`) rodavam com o `GITHUB_TOKEN` nativo, que 403 em escrita de Issues — o flip `Closes #N → done+in-prod` (PRs #42/#48) quebrava em silêncio (o script engolia o 403 e o job terminava success; flip manual necessário). Agora injetam `FORGEJO_API_TOKEN` (o PAT que o pool já usa para o mesmo tipo de escrita) via `env:` no job, e o `forgejo-issue-transition.mjs` sai **exit 1** se qualquer flip falhar — o job vermelha em vez de mentir. **Fork guard** nos dois workflows (`head.repo.full_name == github.repository`, a mesma classe do `agent-pr-ready-automerge`): PR de fork mergeada faria checkout do script do fork rodando com o PAT do repo (RCE). **(S3)** A branch protection de `main` **não existia no servidor** (0 regras no `GET /branch_protections` — o `pnpm configure:branch-protection` era passo humano pendente do OPS50 e nunca rodou; as docs diziam "aplicado" e mentiam), e o valor `checks` do script **nunca casaria** os contextos reais do Forgejo (casamento por glob contra `CI (PR) / checks (pull_request)` — o sufixo ` (evento)` faz parte do contexto; literal `checks` = todo merge bloqueado). Script corrigido: contexto `CI (PR) / checks*`, idempotente (GET → POST/PATCH/no-op, verifica após aplicar, dry-run honesto) com decisão pura em `scripts/lib/branch-protection.mjs` + spec nova. **(2b — achado ao vivo na própria entrega, PR #67)** O safety net tinha **dois** bloqueios independentes. (1) O gate antigo do `waitForChecks` (`pending.length === 0`) era **insatisfazível**: o Forgejo posta o status do próprio job do safety net como `pending` durante o run inteiro. (2) O merge POST faz um push do branch rebaseado para `main` e o hook pre-receive do Forgejo rejeita esse push quando o usuário do token não tem write no repo — o `GITHUB_TOKEN` nativo age como o usuário sistema do Actions (o mesmo que 403 em Issues), então o merge do CLI morria com 409 "User permission denied for writing" (confirmado na fonte 9.0.3: o hook cai no `AssertCanWriteCode` antes do caminho de merge). Resultado: o CLI **nunca auto-mergeou nenhuma PR** — todo merge Forgejo até aqui foi manual (`merged_by` do dono), inclusive o da PR #52 com o CI vermelho (a narrativa da "raça do snapshot" da intenção estava errada: o merge vermelho foi manual; e runs que "exitaram 0" eram leituras de PR já mergeada). Gate novo: espera o rollup `CI (PR) / checks` presente+verde (único contexto que só existe quando a cascata inteira do ci-pr assentou) e **ignora o próprio contexto** do safety net; `mergeable === false` só conta como conflito após a cascata assentar; timeouts sobem (CLI 30→45 min, workflow 40→55 min); e o workflow do safety net agora injeta `FORGEJO_API_TOKEN` (mesmo PAT dos pós-merge) — o merge com o PAT (admin) passa no hook. A regra no servidor é o gate real para todo mundo: o merge POST é bloqueado com 405 enquanto o contexto exigido não estiver verde — inclusive merges manuais via API (a PR #52 não teria entrado vermelha). **Validação ao vivo na própria entrega:** a regra foi criada e verificada no servidor (re-run = no-op), e a PR #67 (`Closes #53`) mergeou pelo novo gate do rollup com a regra aplicada; o `issue-done` flipou a #53 para `done`+`in-prod` com o PAT. Sem migration/access/Consent/UI. Testes: `forgejoApi.unit.spec.ts` (rollup não-posto continua poll, rollup failure lança, próprio status do safety net não bloqueia, mergeable pré-settle — fixtures dos waitForChecks/autoMerge existentes atualizados para o contexto real), `branchProtection.unit.spec.ts` nova (create/update/noop, glob casa o contexto observado). +**Recently resolved (2026-08-18):** **OPS63** — os `docs-guards` (OPS44) rodam também no pre-push local: o espelho `scripts/gate-ci.mjs` ganhou os três checks na fase 1 (changelog append-only, agregado sync `--check`, marcadores de conflito em markdown) como chamadas diretas dos mesmos scripts do CI — labels espelhando os steps do job `docs-guards`; header do arquivo e linha final atualizados. `package.json`, `.husky/pre-push`, `git-push.mjs` e workflows `.forgejo/*` intocados: `gate:push` = `gate:ci` continua a cadeia única, o job `docs-guards` segue dono do escape `changelog-rewrite:` (body do PR — CI-only; restauração legítima D8 falha local com a mesma mensagem do CI e usa o bypass documentado `git push --no-verify`, com o CI como via do escape). Docs sincronizadas: `.agents/rules/engineering-standards.mdc` e `agent-pr-workflow.mdc` (gate descrito como "espelho do ci-pr.yml sem e2e, com docs-guards locais"), `docs/AGENT-OPS.md` (contrato item 4 — OPS44: checks no pre-push + nota do escape CI-only). Verificado por grep: `AGENTS.md` e skills não descreviam o gate sem os checks de docs — sem mudança. Anti-goals respeitados: sem bypass novo (falha local honesta), sem `plans-only-closes` local (contrato de PR body). Sem migration/access/Consent/UI/schema. Verificação: três checks green na branch; caminho de falha validado (commit descartável removendo linha do agregado → append-only falha com mensagem acionável); `pnpm gate:ci` local full; CI roda full nesta PR por HIGH_RISK (`gate-ci.mjs`). + **Recently resolved (2026-08-17):** **OPS51** — o banco de produção saiu do Neon para o Postgres do homeserver (`teqo_1313`, container `teqo-1313`), sem mudança de config: o `~/stack/teqo-1313.env` já apontava para o banco preparado (criado 16/08 com role própria + cadeia completa de 56 migrations + zero conteúdo — verificado), então restaurar = a produção "apareceu" no container. **Divergência deliberada da intenção (questão A/B validada como B):** rejeitado criar `teqo_prod` novo — o argumento "o banco do 1313 tem vida própria" não se sustenta (o site 1313 É a produção deste repo) e o OPS53 já trata `teqo_1313` como produção. Execução 100% no homeserver (PII nunca tocou a workstation): URL do Neon puxada do Vercel (`vercel env pull`, projeto `jorgesolla`, `DATABASE_URL_UNPOOLED` → `NEON_DATABASE_URL` em `~/stack/.env` chmod 600), `pg_dump -Fc` (Neon PG 17.10) → `/srv/hdd/backups/teqo-neon-pre-migracao/teqo-neon-full-20260817-204800.dump` (5,8 MB, sha256 `f12196f2…`), `pg_restore --no-owner --no-privileges` como a role `teqo_1313` (drop schema + grant, padrão do `db-pull`), verificado: contagens por tabela idênticas ao Neon (1919 contatos, 39 posts, 1487 assinaturas, 1484 subscrições, 35 campaign_users, 392 lideranças, 2 pledges, 435 municípios, 6 atividades; supporter segue 0 — blocker LGPD ainda de pé), sequences no topo (`last_value == max(id)`), objetos com dono `teqo_1313`, `payload migrate` no-op (56 = 56, serviço de maintenance `teqo-1313-migrate`), 3 credenciais WebAuthn + 34 sessions + 5 chaves Consent restauradas (RP ID não mudou — biometria segue valendo). Smoke sem deploy: `/`, artigo com conteúdo restaurado, `/campanha/login`, `/admin`, `POST /api/revalidate` (`posts` + `global_privacy-policy`), WebAuthn `login-options`, barreira 307 sem sessão — verdes; smoke de browser (login, biometria, convites) validado manualmente. **Rollback documentado:** Neon nunca foi escrito (só lido); voltar = re-apontar `DATABASE_URL` do `teqo-1313.env` para o Neon + `compose up -d`. **Débito registrado (→ OPS52):** as 40 rows de `media` têm URLs relativas (`/api/media/file/…`) sem arquivos em lugar nenhum — capas dão 500 no homeserver e **404 no próprio Vercel/produção atual** (estado pré-existente; migração manteve paridade). Repo: plano da intenção concluído, `AGENTS.md` e help de `db:pull`/`check-push-chain` sem a referência ao Neon; infra-solla `STATE.md` atualizado à parte. **Recently resolved (2026-08-17):** **OPS53** — o deploy do site 1313 virou etapa do CI: mergir em `main` com a suite verde == site atualizado em `jorgesolla1313.com.br`, sem passo manual. O job `deploy` em `.forgejo/workflows/ci.yml` roda no host do runner (workstation, `runs-on: host`) e streama `scripts/deploy-homeserver.sh` para o homeserver via SSH (`ssh homeserver "bash -s -- "`); o script remoto (bash puro, homeserver sem dependências novas) faz: guard de HEAD (`git ls-remote` de `main` == SHA do job, senão skip "stale"), `flock` de serialização, workspace `~/teqo-deploy` (clone do Forgejo local), `docker login localhost:5000` (REGISTRY_USER/PASSWORD de `~/stack/.env`), build dos stages `runner`+`migrator` no homeserver (`--network stack_default` para a geração estática alcançar o `postgres:5432` do compose; secrets de `~/stack/teqo-1313.env` via BuildKit, sem camadas/logs), push no registry local, swap dos tags de imagem no compose (com backup `docker-compose.yml.pre-`), **migrate pelo serviço de maintenance `teqo-1313-migrate` antes do rollout**, `docker compose up -d`, espera do healthcheck e smoke (`/`, `/campanha/login`, `/admin`, barreira 307, WebAuthn `login-options` com body JSON, `POST /api/revalidate` com o secret real — rehearsal validado contra o site vivo). Falha pós-swap → rollback automático best-effort (restore do backup + `up -d` da imagem anterior) + job vermelho. **Divergência deliberada da intenção (fato novo):** a intenção assumia o runner no homeserver; verificado (`infra-solla/STATE.md` + ao vivo) que o runner é a `workstation-runner` na workstation e o stack de produção vive no homeserver — a abordagem A foi corrigida para "job `runs-on: host` na workstation + todo o deploy no homeserver via SSH", o que manteve **zero secrets novos no Forgejo** e zero mudança de infra no compose. Nenhum secret no repo; envs seguem chmod-600 no homeserver. Runbook (rollback manual, falhas conhecidas, decisão de segurança do host label): `docs/ops/teqo-1313-deploy.md` (novo). Guarda estrutural nova: `tests/unit/deployScript.unit.spec.ts` (bash -n + invariantes do pipeline: HEAD guard, BuildKit secrets, ordem migrate→rollout, smoke). AGENTS.md/AGENT-OPS.md atualizados. Primeiro deploy real = o merge deste próprio item (tracer bullet); infra-solla `STATE.md` anotado à parte. diff --git a/docs/changelog/2026-08-18-ops63.md b/docs/changelog/2026-08-18-ops63.md new file mode 100644 index 00000000..51628c8a --- /dev/null +++ b/docs/changelog/2026-08-18-ops63.md @@ -0,0 +1 @@ +**Recently resolved (2026-08-18):** **OPS63** — os `docs-guards` (OPS44) rodam também no pre-push local: o espelho `scripts/gate-ci.mjs` ganhou os três checks na fase 1 (changelog append-only, agregado sync `--check`, marcadores de conflito em markdown) como chamadas diretas dos mesmos scripts do CI — labels espelhando os steps do job `docs-guards`; header do arquivo e linha final atualizados. `package.json`, `.husky/pre-push`, `git-push.mjs` e workflows `.forgejo/*` intocados: `gate:push` = `gate:ci` continua a cadeia única, o job `docs-guards` segue dono do escape `changelog-rewrite:` (body do PR — CI-only; restauração legítima D8 falha local com a mesma mensagem do CI e usa o bypass documentado `git push --no-verify`, com o CI como via do escape). Docs sincronizadas: `.agents/rules/engineering-standards.mdc` e `agent-pr-workflow.mdc` (gate descrito como "espelho do ci-pr.yml sem e2e, com docs-guards locais"), `docs/AGENT-OPS.md` (contrato item 4 — OPS44: checks no pre-push + nota do escape CI-only). Verificado por grep: `AGENTS.md` e skills não descreviam o gate sem os checks de docs — sem mudança. Anti-goals respeitados: sem bypass novo (falha local honesta), sem `plans-only-closes` local (contrato de PR body). Sem migration/access/Consent/UI/schema. Verificação: três checks green na branch; caminho de falha validado (commit descartável removendo linha do agregado → append-only falha com mensagem acionável); `pnpm gate:ci` local full; CI roda full nesta PR por HIGH_RISK (`gate-ci.mjs`). diff --git a/docs/plans/ops63-doc-guard-no-pre-push-impl.md b/docs/plans/ops63-doc-guard-no-pre-push-impl.md new file mode 100644 index 00000000..cf96b849 --- /dev/null +++ b/docs/plans/ops63-doc-guard-no-pre-push-impl.md @@ -0,0 +1,65 @@ +# Impl: OPS63 — Doc-guard também no pre-push (changelog append-only, agregado e marcadores de conflito) + +Status: aprovado +Atualizado em: 2026-08-18 +Issue: #60 +Intenção: docs/plans/ops63-doc-guard-no-pre-push.md +Appetite restante: herdado (~0,5 dia eng; uma mudança aditiva pequena) + +## Leitura da intenção + +- **Outcome:** `pnpm gate:push` (via `pnpm push` e `.husky/pre-push`) roda os três checks de docs — changelog append-only, agregado up to date (`--check`) e zero marcadores de conflito em markdown — junto da cascata atual; falha bloqueia o push com a mesma mensagem do CI; o CI (`docs-guards` + escape `changelog-rewrite:`) não muda. +- **O que NÃO negociar:** sem bypass novo local (falha local honesta + `git push --no-verify` documentado para o caso raro); sem `plans-only-closes` no pre-push (contrato de body de PR); reuso dos guards existentes, sem duplicar lógica; workflows `.forgejo/*` intocados. +- **O que reavaliar:** nada — a hipótese de direção (adicionar etapa de docs no `scripts/gate-ci.mjs`) confere após leitura do código: os três scripts existem, são autossuficientes (default `origin/main` quando `GITHUB_BASE_REF` ausente) e o gate local é exatamente o espelho serial que omite o job `docs-guards`. + +## Abordagem recomendada + +```mermaid +flowchart LR + P[push / pre-push] --> G[gate:push = gate:ci] + G --> D[docs-guards: 3 checks] + D --> A1[node check-changelog-append-only.mjs] + D --> A2[node build-changelog.mjs --check] + D --> A3[node check-docs-conflict-markers.mjs] + A1 & A2 & A3 --> R[demais checks da fase 1] + R --> OK[push] +``` + +**Opções consideradas:** A | B | C +**Recomendação:** A — chamadas diretas dos três scripts existentes na fase 1 (cheap) do `scripts/gate-ci.mjs`, com labels espelhando os steps do job `docs-guards` do `ci-pr.yml`. Porque: zero duplicação (os scripts são o CI), o espelho local continua sendo o único ponto de cascata, custo ~1s por check e os scripts já fazem no-op quando não há diff. É o mesmo padrão dos `check-test-locations`/`lint`/`format:check` (incondicionais). +**Rejeitadas:** B — wrapper novo `scripts/gate-docs.mjs` (indireção sem volatilidade: 3 chamadas virariam um arquivo; os scripts individuais continuariam sendo a fonte). C — script pnpm `gate:docs` no `package.json` (package.json ganha um agregado que só o gate usa; chamar `node scripts/...` direto espelha os steps do CI e não mexe em nada além do espelho). + +### Componentes / mudanças + +- **`scripts/gate-ci.mjs`**: 3 blocos `run()` na fase 1 (após `format:check`, antes do bloco condicional de typecheck — incondicionais, como o job `docs-guards`): `node scripts/check-changelog-append-only.mjs`, `node scripts/build-changelog.mjs --check`, `node scripts/check-docs-conflict-markers.mjs`. Header do arquivo ganha menção ao OPS63. Linha final `✓ all checks passed` menciona os docs guards. Nada mais muda no arquivo. +- **`package.json` / `.husky/pre-push` / `scripts/git-push.mjs`**: intocados — `gate:push` = `gate:ci` continua sendo a cadeia única. +- **`.forgejo/workflows/ci-pr.yml` / `ci.yml`**: intocados — `docs-guards` continua dono do escape `changelog-rewrite:` (PR body); sem mudança. +- **Migration:** sem migration. +- **Access / Consent:** N/A. +- **UI:** Impeccable A — N/A (sem superfície UI). + +## Fases verificáveis + +1. **Espelho** — editar `scripts/gate-ci.mjs` (fase 1 + header + linha final). Verificação: os três checks green nesta branch (sem diff de changelog ainda → "no changelog diff"/"no markdown changes"/"aggregate up to date"); caminho de falha validado com um commit descartável que remove linha do agregado (append-only deve falhar). +2. **Docs do fluxo** — `.agents/rules/engineering-standards.mdc` (linha do gate: "espelho do ci-pr.yml sem e2e" → "sem e2e, com docs-guards OPS63"), `.agents/rules/agent-pr-workflow.mdc` (item 2 do fluxo do agente), `docs/AGENT-OPS.md` (contrato item 4 — OPS44: os três checks rodam também no pre-push local; o escape `changelog-rewrite:` segue só no CI/PR body). Verificação: grep final por descrições do gate que omitam os docs guards. +3. **Changelog + gates** — `docs/changelog/2026-08-18-ops63.md` + `pnpm changelog:build`; `pnpm gate:ci` local (cascata completa em `teqo_test`; os docs guards rodam na fase 1 — esta PR é a primeira que os exercita de verdade, pois o agregado muda); CI roda full no PR (`scripts/gate-ci.mjs` é HIGH_RISK). + +## Rabbit holes / Não escopo (engenharia) + +- **`plans-only-closes` no pre-push** — depende do body do PR; CI-only por design da intenção. +- **Escape local para o append-only** — a restauração legítima (D8) falha localmente sem o PR body; decisão da intenção já fixada: mensagem acionável do guard + bypass documentado `git push --no-verify`. +- **Flag/env de "pular docs"** — anti-goal explícito da intenção. +- **Varredura da working tree vs HEAD** — `check-docs-conflict-markers` lê `HEAD:` (commitado), como o CI lê o diff do PR; rascunho não commitado fica fora — consistente, sem mudança. +- **Novo script pnpm / wrapper** — rejeitado (opções B/C acima). + +## Riscos e mitigação + +- **Falha local de append-only em PR que usa o escape `changelog-rewrite:`** (ex. restauração D8): o guard falha com a mensagem do CI sem o escape; o agente usa `git push --no-verify` (escape já documentado em AGENT-OPS). O CI continua aceitando o escape via PR body — nenhuma entrega legítima fica presa. Mitigação: nota no impl do changelog/AGENT-OPS. +- **`origin/main` desatualizado no worktree**: merge-base velho pode acusar diff inexistente ou deixar passar diff novo. Mesmo comportamento dos guards existentes (`ci-scope` usa `origin/main`); o `fetch` do rebase obrigatório no fluxo reduz a janela. Sem ação nova. +- **Acoplamento com OPS59** (mesmo arquivo): já mergeado em `main`; a branch deste worktree nasceu dele — mudança aditiva sem conflito; rebase obrigatório antes do `pnpm push` de qualquer forma. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção ainda coberto (três checks no pre-push; CI intocado) +- [ ] Invariantes AGENTS/engineering-standards (gate bare; `format:check`; knip/cycles sem arquivos novos) +- [ ] Testes de domínio previstos: nenhum teste novo (nenhum módulo compartilhado muda); suíte unit + `pnpm gate:ci` verdes; caminho de falha do append-only verificado manualmente com commit descartável diff --git a/scripts/gate-ci.mjs b/scripts/gate-ci.mjs index 726a1add..8c0268b3 100644 --- a/scripts/gate-ci.mjs +++ b/scripts/gate-ci.mjs @@ -4,6 +4,10 @@ * then phase-2 expensive (int/build). e2e is NOT part of the local mirror * (OPS59): it runs once, in CI (PR job `e2e` → `checks`; main `ci.yml` e2e → * `checks` → `deploy`). Local optional feedback: `pnpm test:e2e:affected`. + * Docs guards (OPS63): the `docs-guards` checks (changelog append-only, + * aggregate sync, conflict markers) also run here — cheap git diffs, same + * scripts the CI job runs (the `changelog-rewrite:` escape stays CI-only, + * PR body). * Skips align with `scripts/ci-scope.mjs` (`origin/main` by default). * * Preflight checks only `teqo_test` (ci-pr never touches the dev DB). Escape @@ -81,6 +85,11 @@ const main = async () => { run('lint', 'pnpm', ['lint']) run('format:check', 'pnpm', ['format:check']) + // --- Docs guards (OPS63): same checks as the CI `docs-guards` job --- + run('docs-guards (changelog append-only)', 'node', ['scripts/check-changelog-append-only.mjs']) + run('docs-guards (aggregate sync)', 'node', ['scripts/build-changelog.mjs', '--check']) + run('docs-guards (conflict markers)', 'node', ['scripts/check-docs-conflict-markers.mjs']) + if (scope.code.mode === 'none') { console.log('\n[gate:ci] ⊘ typecheck/knip/cycles skipped (no code surface)') } else { @@ -149,7 +158,9 @@ const main = async () => { console.log(`\n[gate:ci] ▶ e2e: runs in CI (job e2e, ${specs}) — not in this gate (OPS59)`) } - console.log('\n[gate:ci] ✓ all checks passed (ci-pr mirror without e2e — e2e verified in CI)') + console.log( + '\n[gate:ci] ✓ all checks passed (ci-pr mirror: docs guards local — OPS63; e2e verified in CI)', + ) } await main()