mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
feat(OPS85): matar o agregado commitado do changelog — registro por-arquivo + HISTORY congelado + leitura sob demanda (Closes #830)
This commit is contained in:
parent
c69376f3c2
commit
60a71eea4b
20 changed files with 239 additions and 156 deletions
|
|
@ -38,7 +38,7 @@ Closes #<N>
|
|||
**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 (OPS72: o e2e é passo **explícito da skill** — 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`, e o full fica no `verify` do deploy manual), 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)
|
||||
2. `pnpm gate:push` — espelho do `ci-pr.yml` sem e2e (OPS72: o e2e é passo **explícito da skill** — 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`, e o full fica no `verify` do deploy manual), com os `docs-guards` locais (OPS63/OPS85: changelog additions-only + HISTORY congelado + agregado não-commitado, 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
|
||||
|
|
|
|||
|
|
@ -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** — 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 manual; **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; 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` (dispatch manual — OPS71; 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`).
|
||||
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 manual; **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` (dispatch manual — OPS71; 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 still cannot load `payload.config.ts`, so its graph is incomplete; ledgered as P3).
|
||||
|
||||
## Type honesty
|
||||
|
|
|
|||
|
|
@ -62,11 +62,12 @@ antes do deploy**.
|
|||
## Fechar em main
|
||||
|
||||
1. Branch do ator (ver deltas) — nunca crie branch nova fora dela.
|
||||
2. **Changelog da entrega (OPS44):** escreva `docs/changelog/<data>-<id>.md`
|
||||
(ex. `2026-08-13-ops44.md`) — uma entrada curta no formato do agregado —
|
||||
e rode `pnpm changelog:build` para regenerar `docs/CHANGELOG-AGENTS.md`
|
||||
(insert-only: entradas históricas nunca mudam; o diff será só a entrada
|
||||
nova + o agregado). Rode `pnpm changelog:check` para confirmar.
|
||||
2. **Changelog da entrega (OPS44, OPS85):** escreva `docs/changelog/<data>-<id>.md`
|
||||
(ex. `2026-08-13-ops44.md`) — uma entrada curta no formato do agregado. É o
|
||||
único registro commitado: **não rode `pnpm changelog:build` nem commite o
|
||||
agregado** (ele é gitignored desde OPS85). Opcional: `pnpm changelog:read`
|
||||
para conferir a leitura completa localmente (seed do HISTORY). O diff
|
||||
commitado é só a entrada nova.
|
||||
3. **`pnpm push -u origin HEAD`** — origin é o **GitHub** (OPS71; o tracker de
|
||||
Issues continua no Forgejo por API).
|
||||
4. PR no **GitHub** via `GITHUB_TOKEN=<PAT> node scripts/github-pr.mjs --head <branch> --title "<id> — <título>" --body-file <arquivo>` — **Ready** (nunca draft; o script não tem flag draft), base `main`, body com `Closes #<N>` (ou `Related #N` em plans-only). Em Cursor Cloud: `ManagePullRequest` com `draft: false`.
|
||||
|
|
|
|||
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
|
|
@ -11,8 +11,8 @@ Closes #<!-- número da Issue (claim) — omita em PRs que alteram só docs/plan
|
|||
- [ ] Órfãos da mudança deletados na mesma entrega (knip em CI confirma)
|
||||
- [ ] **Se tocou schema** (migration, collection, campo, Consent key, dado de boot): `db:seed:minimal` atualizado **neste PR** (`scripts/lib/seed-minimal-manifest.mjs` + pin unit) — senão a label `needs:migration` está errada
|
||||
- [ ] Nenhum `DATABASE_URL`/`ALLOW_REMOTE_DB` de prod em script ou workflow
|
||||
- [ ] Se muda o que o agente precisa saber sempre: `AGENTS.md`/kernels atualizados; entregas registram UMA entrada em `docs/changelog/<data>-<id>.md` + `pnpm changelog:build` (OPS44 — nunca editam `docs/CHANGELOG-AGENTS.md` na mão)
|
||||
- [ ] Se o changelog perdeu linhas de propósito (restauração/header): `changelog-rewrite: <motivo>` no body deste PR
|
||||
- [ ] Se muda o que o agente precisa saber sempre: `AGENTS.md`/kernels atualizados; entregas registram UMA entrada em `docs/changelog/<data>-<id>.md` (OPS44 — nunca editam `docs/CHANGELOG-AGENTS.md` nem o HISTORY `docs/CHANGELOG-AGENTS-HISTORY.md` na mão; o agregado é gitignored e regenerado sob demanda, OPS85)
|
||||
- [ ] Se o changelog perdeu conteúdo de propósito (restauração/correção do HISTORY): `changelog-rewrite: <motivo>` no body deste PR
|
||||
|
||||
## Notas para review
|
||||
|
||||
|
|
|
|||
2
.github/workflows/ci-pr.yml
vendored
2
.github/workflows/ci-pr.yml
vendored
|
|
@ -116,8 +116,6 @@ jobs:
|
|||
GITHUB_BASE_REF: ${{ github.base_ref }}
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
run: node scripts/check-changelog-append-only.mjs
|
||||
- name: Aggregate is up to date with docs/changelog
|
||||
run: node scripts/build-changelog.mjs --check
|
||||
- name: No conflict markers in markdown diffs
|
||||
env:
|
||||
GITHUB_BASE_REF: ${{ github.base_ref }}
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -93,3 +93,6 @@ next-env.d.ts
|
|||
|
||||
/data/encode/
|
||||
/data/poster-candidates/
|
||||
|
||||
# Changelog aggregate is a regenerable artifact (OPS85) — entries live in docs/changelog/
|
||||
/docs/CHANGELOG-AGENTS.md
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
Production is self-hosted on the homeserver (`jorgesolla1313.com.br`, behind the Cloudflare tunnel; container/database `teqo-1313`/`teqo_1313`). **There is no Vercel deploy anymore** — the Vercel deploy job, `vercel-promote.yml`, the Vercel scripts/`vercel.json` and the `VERCEL_*` repo secrets were removed in OPS50 (2026-08-17). **CI/PR/deploy live on GitHub Actions (OPS71, 2026-08-19); the ISSUE TRACKER stays on the Forgejo** (`git.solla.dev/fsolla/teqo` — labels, claims, `pnpm issue`/`agent:*` and the post-merge flips all keep talking to the Forgejo API; GitHub is only the Actions host). The PR CI (`ci-pr.yml`, job único `checks` — OPS62) runs the same cascade as always (guards → lint → format → typecheck → knip → cycles → unit → int → build → e2e selected/afetado, skips via `ci-scope.mjs`); its check-run `CI (PR) / checks` is the required check of `main`. **There is NO main verifier (`ci.yml`) anymore**: publishing is a **manual `workflow_dispatch`** (`.github/workflows/deploy.yml`) with two jobs — `verify` (hosted `ubuntu-latest`, suíte **full** incl. e2e full) → `deploy` (`needs: [verify]`, `runs-on: [self-hosted, homeserver]`): the GitHub self-hosted runner ON the homeserver executes `scripts/deploy-homeserver.sh` **locally** (no SSH; outbound-only runner, works behind the tunnel; the hosted job never touches the homeserver). The remote Docker deployment is gone; the production container runs the monolith directly. The Docker Compose file is used ONLY for local dev databases. The `run-deploy.yml` (OPS38/OPS46) was deleted in OPS71. There is no `run-deploy.yml` or equivalent in GitHub Actions — deploy is `deploy.yml` only.
|
||||
|
||||
**Guarding `pnpm push` (OPS24+):** every PR push goes through `scripts/git-push.mjs`, which runs `ensure-repo-deps` (install + Husky in worktrees — OPS51), then `gate:push` (the pre-push hook), which chains `gate:ci` — a serialized mirror of the CI `checks` job (**without e2e**: OPS72 moved e2e to an explicit skill step) — plus the `docs-guards` (changelog append-only, aggregation sync, conflict markers — OPS63; the rewrite escape stays in CI only). The hook blocks `git push` on failure; escape: `git push --no-verify` (WIP only). There is no YAML `push` gate — the hook is the gate. **CI red on your PR is yours**, even pre-existing or infra — "out of feature scope" is not a stop condition (policy "Dono do PR, dono do CI" in `docs/AGENT-OPS.md`).
|
||||
**Guarding `pnpm push` (OPS24+):** every PR push goes through `scripts/git-push.mjs`, which runs `ensure-repo-deps` (install + Husky in worktrees — OPS51), then `gate:push` (the pre-push hook), which chains `gate:ci` — a serialized mirror of the CI `checks` job (**without e2e**: OPS72 moved e2e to an explicit skill step) — plus the `docs-guards` (changelog entries additions-only + frozen HISTORY snapshot + uncommitted regenerable aggregate — OPS63/OPS85; conflict markers; the rewrite escape stays in CI only). The hook blocks `git push` on failure; escape: `git push --no-verify` (WIP only). There is no YAML `push` gate — the hook is the gate. **CI red on your PR is yours**, even pre-existing or infra — "out of feature scope" is not a stop condition (policy "Dono do PR, dono do CI" in `docs/AGENT-OPS.md`).
|
||||
|
||||
## Database & local development
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ These are product/architecture constraints — not a ban on editing existing cod
|
|||
|
||||
## Histórico de entregas (movido)
|
||||
|
||||
"Known Gaps" e os blocos "Recently resolved" saíram deste arquivo em 2026-07-30: vivem em [`docs/CHANGELOG-AGENTS.md`](docs/CHANGELOG-AGENTS.md). Novas entregas registram UMA entrada curta em `docs/changelog/<data>-<id>.md` e rodam `pnpm changelog:build` (agregado insert-only, OPS44) — nunca editam `docs/CHANGELOG-AGENTS.md` na mão. A referência genérica de Payload (config/collections/fields/hooks/queries/components) foi para [`docs/PAYLOAD-REFERENCE.md`](docs/PAYLOAD-REFERENCE.md) — consulta sob demanda, não always-on. Operação do paradigma de agentes paralelos (claim→PR→main→deploy gated): [`docs/AGENT-OPS.md`](docs/AGENT-OPS.md).
|
||||
"Known Gaps" e os blocos "Recently resolved" saíram deste arquivo em 2026-07-30: o registro commitado vive em [`docs/changelog/`](docs/changelog/) (uma entrada por arquivo, imune a conflito). O agregado legível `docs/CHANGELOG-AGENTS.md` é **gitignored e gerado sob demanda** (OPS85): `pnpm changelog:build` grava, `pnpm changelog:read` imprime no stdout — com seed do snapshot congelado `docs/CHANGELOG-AGENTS-HISTORY.md`. Novas entregas registram UMA entrada curta em `docs/changelog/<data>-<id>.md` — nunca editam `docs/CHANGELOG-AGENTS.md` nem o HISTORY na mão. A referência genérica de Payload (config/collections/fields/hooks/queries/components) foi para [`docs/PAYLOAD-REFERENCE.md`](docs/PAYLOAD-REFERENCE.md) — consulta sob demanda, não always-on. Operação do paradigma de agentes paralelos (claim→PR→main→deploy gated): [`docs/AGENT-OPS.md`](docs/AGENT-OPS.md).
|
||||
|
||||
## Core Principles
|
||||
|
||||
|
|
|
|||
|
|
@ -58,16 +58,20 @@ Labels (GitHub): `ready|in-progress|blocked|done|in-prod`, `prio:*`, `kind:*`, `
|
|||
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; rebase antes de `migrate:create` continua obrigatório.
|
||||
3. Ready + auto-merge: `pnpm push -u origin HEAD` (GitHub) → PR via `node scripts/github-pr.mjs` (Ready, base `main`, `Closes #N`) → o safety net `agent-pr-ready-automerge.yml` (`scripts/github-pr-automerge.mjs`) arma o **auto-merge nativo do GitHub por rebase** — o servidor espera o required check `CI (PR) / checks` e mergea; nada a armar por PR, não há `gh` (OPS50) e não há poll (OPS71). **Merge é por rebase** (nunca merge commit): preserva a linha histórica limpa do `main` e é o modo do auto-merge.
|
||||
4. **Changelog da entrega (OPS44):** toda entrega escreve `docs/changelog/<data>-<id>.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: <motivo>` 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).
|
||||
4. **Changelog da entrega (OPS44, OPS85):** toda entrega escreve `docs/changelog/<data>-<id>.md` (ex. `2026-08-13-ops44.md`) — o **único registro commitado** do histórico. O agregado `docs/CHANGELOG-AGENTS.md` é um **artefato gitignored regenerado sob demanda** (OPS85): `pnpm changelog:build` grava o arquivo local, `pnpm changelog:read` imprime no stdout, ambos com seed do snapshot congelado `docs/CHANGELOG-AGENTS-HISTORY.md` quando o arquivo local não existe — **nunca commitar o agregado nem editar o HISTORY na mão**. Guard de CI `docs-guards`: (a) `docs/changelog/` é additions-only (M/D falham); (b) o HISTORY é congelado (só a criação é permitida); (c) o agregado nunca aparece no diff de um PR (a remoção única de OPS85 foi a exceção que criou o HISTORY); (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 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, correção do HISTORY) escapa escrevendo **`changelog-rewrite: <motivo>` 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).
|
||||
|
||||
## Registries compartilhados (serialização por arquivo, não por PR)
|
||||
|
||||
Arquivos de "lista" que N agentes escrevem em paralelo (changelog, plans, migrações, catalog) conflitam por construção: cada escrita reescreve o arquivo inteiro. Regras:
|
||||
Arquivos de "lista" que N agentes escrevem em paralelo (plans, migrações, catalog) conflitam por construção: cada escrita reescreve o arquivo inteiro. Regras:
|
||||
|
||||
- **Cada entrega escreve UM arquivo por registro** (`docs/changelog/<data>-<id>.md`, `docs/plans/<slug>.md`, migration com nome único, etc.) — o arquivo comum é **derivado** (build/CI) ou **só-leitura por PR** (guard).
|
||||
- **Cada entrega escreve UM arquivo por registro** (`docs/plans/<slug>.md`, migration com nome único, etc.) — o arquivo comum é **derivado** (build/CI) ou **só-leitura por PR** (guard). O changelog é o caso resolvido em OPS85: o registro commitado é por-arquivo (`docs/changelog/<data>-<id>.md`), o agregado legível virou artefato regenerado gitignored e o snapshot `docs/CHANGELOG-AGENTS-HISTORY.md` é só-leitura por guard.
|
||||
- **`serializes` no plano de intenção** nomeia o registro exato que a Issue toca (ex. `serializes: 'docs/plans/'`), para o executor agendar com clareza — é dica de ordem, não lock (rebase continua obrigatório).
|
||||
- O rebase (Contrato de PR item 3) é o mecanismo real de conciliação: conflitos de append resolvem com `git checkout --ours/--theirs` por lado.
|
||||
|
||||
## Decisões arquivadas
|
||||
|
||||
- **Docs agent-facing (changelog, plans) NÃO migram para fora do repo (arquivado 2026-08-24, OPS85):** a evidência da OPS85 matou a pergunta — o conflito de merge era do **agregado** (arquivo comum derivado, 171 blocos reescritos no mesmo anchor), não das entradas por-arquivo (imunes por construção desde OPS44) nem dos plans (sempre funcionaram). Migrar 12+ skills com paths hardcoded + o ciclo claim→plano→PR para um alvo externo trocaria conflito de merge por desincronização de registro que os guards não enxergam (anti-goal "segunda fonte de verdade"). **Se um dia houver custo novo que justifique, o alvo registrado é Wiki.js** no homeserver (Node+DB, revisões por página, edição paralela sem git); Outline é segunda opção quando o custo de infra adicional se justificar; Pages estático é a resposta errada para a classe "conflito de git"; coleção `Doc` no Payload acopla docs ao deploy. Nada foi instalado.
|
||||
|
||||
## CI por alvo (GitHub Actions, OPS71)
|
||||
|
||||
| Workflow | Trigger | Banco | Passos |
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
<!-- CONGELADO em 2026-08-24 (OPS85): snapshot do agregado docs/CHANGELOG-AGENTS.md na data da morte do agregado commitado. Seed do artefato regenerado sob demanda; NUNCA editar (guard: docs-guards — zero-diff). -->
|
||||
# Changelog de contexto para agentes (fatias do AGENTS.md)
|
||||
|
||||
Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agentes paralelos): "Known Gaps" e todos os blocos "Recently resolved". Histórico — leia só quando precisar do contexto de uma entrega passada; o sempre-presente fica no `AGENTS.md`. **Desde OPS44 (2026-08-13)** cada entrega registra UMA entrada em `docs/changelog/<data>-<id>.md` e o agregado é regenerado por `pnpm changelog:build` (insert-only — este arquivo nunca é editado na mão; guard de CI `docs-guards` e escape `changelog-rewrite:` documentados em AGENT-OPS).
|
||||
1
docs/changelog/2026-08-24-ops85.md
Normal file
1
docs/changelog/2026-08-24-ops85.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
**Recently resolved (2026-08-24):** **OPS85** (Issue #830) — eliminar a classe de conflito de merge no changelog: o agregado commitado `docs/CHANGELOG-AGENTS.md` (171 blocos reescritos no mesmo anchor a cada entrega paralela) morre — `docs/changelog/<data>-<id>.md` vira o **único registro commitado** e a leitura "Recently resolved" é gerada sob demanda: `pnpm changelog:build` grava o artefato **gitignored** no mesmo path, `pnpm changelog:read` imprime no stdout, ambos com seed do snapshot congelado **`docs/CHANGELOG-AGENTS-HISTORY.md`** (cópia integral do agregado na data da migração, incluindo as 106 entradas pré-OPS44 que só existiam lá; nunca é editado). Guard de CI `docs-guards` reescrito: entradas additions-only (imutável), HISTORY congelado (só a criação é permitida) e agregado proibido no diff (a remoção única desta entrega foi a exceção que criou o HISTORY); o multiset do agregado (perda de linha) morre — a garantia de não-perda vira additions-only + pós-condição do gerador ("toda entrada parseada aparece no agregado"). O step "Aggregate is up to date" sai do `ci-pr.yml` e do `gate-ci.mjs` (o `--check` vira sanidade local opcional); escape `changelog-rewrite:` permanece CI-only (agora também cobre correção do HISTORY). Contratos atualizados: AGENTS.md, AGENT-OPS.md (item 4 + "Registries compartilhados" + seção nova **"Decisões arquivadas"**), execution-pipeline (entrada sem build), PR template, roadmap, agent-pool-prompt, engineering-standards, agent-pr-workflow, AGENTS-infra. **Docs agent-facing NÃO migram para fora do repo — arquivado com evidência:** o conflito era do agregado, não das entradas por-arquivo; alvo registrado se um dia houver custo novo: Wiki.js. Sem migration/access/Consent/UI/schema. Esta entrega é o tracer bullet do fluxo novo (entrada commitada, agregado não).
|
||||
|
|
@ -3,5 +3,5 @@
|
|||
**Congelado em 2026-07-30.** Este arquivo não é mais editado.
|
||||
|
||||
- **Fonte canônica de spec/status/deps/prio/modelo:** [Issues do Forgejo](https://git.solla.dev/fsolla/teqo/issues) (frontmatter `id/depends/serializes/priority/model`; estado em labels `ready | in-progress | blocked | done | in-prod`). Fluxo: skills `plan-issue` → `work-issue` → `project-status` (ver `docs/AGENT-OPS.md`). Status rápido: `pnpm agent:status`.
|
||||
- **Histórico e entregas:** [`docs/CHANGELOG-AGENTS.md`](CHANGELOG-AGENTS.md) + [`docs/plans/`](plans/).
|
||||
- **Histórico e entregas:** [`docs/changelog/`](changelog/) (registro por-arquivo) + [`docs/plans/`](plans/) — leitura agregada sob demanda: `pnpm changelog:read` (ou `pnpm changelog:build`, gitignored).
|
||||
- **Conteúdo integral deste roadmap:** preservado no git history (última versão viva no commit imediatamente anterior a este stub).
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
"push": "node scripts/git-push.mjs",
|
||||
"check:cycles": "madge --circular --extensions ts,tsx --ts-config tsconfig.json src",
|
||||
"changelog:build": "node scripts/build-changelog.mjs",
|
||||
"changelog:read": "node scripts/build-changelog.mjs --stdout",
|
||||
"changelog:check": "node scripts/build-changelog.mjs --check",
|
||||
"generate:importmap": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload generate:importmap",
|
||||
"generate:types": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload generate:types",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* build-changelog — regenerate docs/CHANGELOG-AGENTS.md from the per-delivery
|
||||
* entries in docs/changelog/ (OPS44). Deliveries write one
|
||||
* `<date>-<id>.md` file and run `pnpm changelog:build`; the aggregate keeps
|
||||
* the single readable "Recently resolved" history. Insert-only: historical
|
||||
* blocks are never touched.
|
||||
* build-changelog — regenerate the aggregated changelog (OPS44/OPS85).
|
||||
*
|
||||
* Usage: node scripts/build-changelog.mjs [--check]
|
||||
* --check verify the aggregate is up to date without writing (CI use)
|
||||
* docs/changelog/<date>-<id>.md is the only committed changelog record.
|
||||
* The readable aggregate docs/CHANGELOG-AGENTS.md is a gitignored artifact
|
||||
* generated on demand, seeded from the frozen snapshot
|
||||
* docs/CHANGELOG-AGENTS-HISTORY.md when the local file is absent. Insert-only:
|
||||
* historical blocks are never touched.
|
||||
*
|
||||
* Usage: node scripts/build-changelog.mjs [--stdout] [--check]
|
||||
* --stdout print the aggregate to stdout instead of writing the file
|
||||
* --check verify the local aggregate (when present) is up to date —
|
||||
* local sanity only, not a CI/gate requirement since OPS85
|
||||
*/
|
||||
import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
|
@ -16,10 +20,14 @@ import {
|
|||
buildChangelog,
|
||||
CHANGELOG_AGGREGATE,
|
||||
CHANGELOG_DIR,
|
||||
CHANGELOG_HEADER,
|
||||
CHANGELOG_HISTORY,
|
||||
listChangelogEntries,
|
||||
splitChangelogBody,
|
||||
} from './lib/changelog.mjs'
|
||||
|
||||
const repoRoot = process.cwd()
|
||||
const toStdout = process.argv.includes('--stdout')
|
||||
const checkOnly = process.argv.includes('--check')
|
||||
|
||||
function die(message) {
|
||||
|
|
@ -30,11 +38,23 @@ function die(message) {
|
|||
const aggregatePath = join(repoRoot, CHANGELOG_AGGREGATE)
|
||||
const changelogDir = join(repoRoot, CHANGELOG_DIR)
|
||||
|
||||
let aggregateContent
|
||||
let localContent = null
|
||||
try {
|
||||
aggregateContent = readFileSync(aggregatePath, 'utf8')
|
||||
localContent = readFileSync(aggregatePath, 'utf8')
|
||||
} catch {
|
||||
die(`não consegui ler ${CHANGELOG_AGGREGATE}`)
|
||||
// absent — regenerable artifact; seed from the frozen HISTORY snapshot
|
||||
}
|
||||
|
||||
let aggregateContent = localContent
|
||||
if (aggregateContent === null) {
|
||||
let historyContent
|
||||
try {
|
||||
historyContent = readFileSync(join(repoRoot, CHANGELOG_HISTORY), 'utf8')
|
||||
} catch {
|
||||
die(`não consegui ler ${CHANGELOG_AGGREGATE} nem o seed ${CHANGELOG_HISTORY}`)
|
||||
}
|
||||
const { blocks } = splitChangelogBody(historyContent)
|
||||
aggregateContent = [CHANGELOG_HEADER, ...blocks].join('\n\n') + '\n'
|
||||
}
|
||||
|
||||
let files
|
||||
|
|
@ -49,26 +69,43 @@ if (errors.length > 0) {
|
|||
die(`entradas inválidas em ${CHANGELOG_DIR}/:\n ${errors.join('\n ')}`)
|
||||
}
|
||||
|
||||
const bodies = entries.map(({ path }) => {
|
||||
const withContent = entries.map(({ path, name, date, id }) => {
|
||||
try {
|
||||
return readFileSync(join(repoRoot, path), 'utf8')
|
||||
return { path, name, date, id, content: readFileSync(join(repoRoot, path), 'utf8') }
|
||||
} catch {
|
||||
die(`não consegui ler ${path}`)
|
||||
}
|
||||
})
|
||||
|
||||
const next = buildChangelog({
|
||||
entries: bodies.map((content) => ({ content })),
|
||||
entries: withContent.map(({ content }) => ({ content })),
|
||||
aggregateContent,
|
||||
})
|
||||
|
||||
// Post-condition (OPS85): no parsed entry may silently vanish from the
|
||||
// generated aggregate — replaces the old committed-aggregate multiset.
|
||||
for (const { path, content } of withContent) {
|
||||
if (!next.includes(content.trimEnd())) {
|
||||
die(`entrada ausente do agregado gerado (perda silenciosa?): ${path}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (checkOnly) {
|
||||
if (next !== aggregateContent) {
|
||||
if (localContent === null) {
|
||||
console.log('[build-changelog] ok — agregado local ausente (artefato regenerável)')
|
||||
process.exit(0)
|
||||
}
|
||||
if (next !== localContent) {
|
||||
die(`${CHANGELOG_AGGREGATE} desatualizado — rode \`pnpm changelog:build\``)
|
||||
}
|
||||
console.log('[build-changelog] ok — aggregate up to date')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (toStdout) {
|
||||
process.stdout.write(next)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
writeFileSync(aggregatePath, next)
|
||||
console.log(`[build-changelog] regenerado ${CHANGELOG_AGGREGATE} com ${entries.length} entrada(s)`)
|
||||
|
|
|
|||
|
|
@ -1,23 +1,26 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-changelog-append-only — CI guard (OPS44): the aggregated changelog
|
||||
* must never lose an existing line, and docs/changelog/ entries are
|
||||
* additions-only. Legitimate restorations (D8 pattern) and header rewrites
|
||||
* escape via the documented `changelog-rewrite:` token in the PR body.
|
||||
* check-changelog-append-only — CI + pre-push guard (OPS44/OPS85): the
|
||||
* committed changelog record is per-file only. docs/changelog/ entries are
|
||||
* additions-only, docs/CHANGELOG-AGENTS-HISTORY.md is a frozen snapshot
|
||||
* (only its creation is allowed), and the regenerable aggregate
|
||||
* docs/CHANGELOG-AGENTS.md must never be committed — its one-time removal
|
||||
* is allowed only together with the HISTORY creation (the OPS85 migration).
|
||||
* Legitimate exceptions (D8-style restorations, HISTORY corrections) escape
|
||||
* via the documented `changelog-rewrite:` token in the PR body.
|
||||
*
|
||||
* Paths from git diff (merge-base…HEAD); body from PR_BODY env or `gh pr
|
||||
* view` when PR_NUMBER is set. The escape is honored only when the diff
|
||||
* actually touches the aggregate or docs/changelog/, and only as a
|
||||
* standalone line (never the PR template's own checkbox text).
|
||||
* actually touches the changelog paths, and only as a standalone line
|
||||
* (never the PR template's own checkbox text).
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import {
|
||||
assertChangelogAppendOnly,
|
||||
CHANGELOG_AGGREGATE,
|
||||
CHANGELOG_DIR,
|
||||
CHANGELOG_HISTORY,
|
||||
CHANGELOG_REWRITE_ESCAPE_RE,
|
||||
} from './lib/changelog.mjs'
|
||||
|
||||
|
|
@ -42,7 +45,15 @@ try {
|
|||
function listChangedPaths() {
|
||||
const raw = execFileSync(
|
||||
'git',
|
||||
['diff', '--name-status', `${mergeBase}...HEAD`, '--', CHANGELOG_AGGREGATE, CHANGELOG_DIR],
|
||||
[
|
||||
'diff',
|
||||
'--name-status',
|
||||
`${mergeBase}...HEAD`,
|
||||
'--',
|
||||
CHANGELOG_AGGREGATE,
|
||||
CHANGELOG_HISTORY,
|
||||
CHANGELOG_DIR,
|
||||
],
|
||||
{ encoding: 'utf8' },
|
||||
)
|
||||
|
||||
|
|
@ -79,7 +90,7 @@ if (diffLines.length === 0) {
|
|||
}
|
||||
|
||||
// R (rename): status + old path + new path — use the new path.
|
||||
const changelogDiff = diffLines.map((line) => {
|
||||
const changed = diffLines.map((line) => {
|
||||
const [status, ...rest] = line.split('\t')
|
||||
return { status, path: status.startsWith('R') ? rest[rest.length - 1] : rest.join('\t') }
|
||||
})
|
||||
|
|
@ -90,19 +101,10 @@ if (CHANGELOG_REWRITE_ESCAPE_RE.test(body)) {
|
|||
process.exit(0)
|
||||
}
|
||||
|
||||
function blob(ref, path) {
|
||||
return execFileSync('git', ['show', `${ref}:${path}`], { encoding: 'utf8' })
|
||||
}
|
||||
|
||||
const aggregateDiff = changelogDiff.filter(({ path }) => path === CHANGELOG_AGGREGATE)
|
||||
const oldAggregate = aggregateDiff.length > 0 ? blob(mergeBase, CHANGELOG_AGGREGATE) : ''
|
||||
const newAggregate =
|
||||
aggregateDiff.length > 0 ? readFileSync(join(process.cwd(), CHANGELOG_AGGREGATE), 'utf8') : ''
|
||||
|
||||
const result = assertChangelogAppendOnly({
|
||||
oldAggregate,
|
||||
newAggregate,
|
||||
changelogDiff: changelogDiff.filter(({ path }) => path.startsWith(`${CHANGELOG_DIR}/`)),
|
||||
changelogDiff: changed.filter(({ path }) => path.startsWith(`${CHANGELOG_DIR}/`)),
|
||||
historyDiff: changed.filter(({ path }) => path === CHANGELOG_HISTORY),
|
||||
aggregateDiff: changed.filter(({ path }) => path === CHANGELOG_AGGREGATE),
|
||||
})
|
||||
|
||||
if (!result.ok) {
|
||||
|
|
|
|||
|
|
@ -87,9 +87,8 @@ const main = async () => {
|
|||
run('lint', 'pnpm', ['lint'])
|
||||
run('format:check', 'pnpm', ['format:check'])
|
||||
|
||||
// --- Docs guards (OPS63): same checks as the CI `docs-guards` job ---
|
||||
// --- Docs guards (OPS63/OPS85): 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') {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const buildPoolWorkerPrompt = ({ issueNumber, issueTitle, issueId, planPa
|
|||
: '2. O body da Issue é a spec de intenção (não há plano linkado). Em Plan mode, escreva mesmo assim um `docs/plans/<id>-impl.md` a partir do body.',
|
||||
'3. Execute o impl plan. Gates: `pnpm gate:fast` na iteração; entrega com `pnpm push` (não `git push` nu). Comandos bare, nunca piped.',
|
||||
'4. Rode `/simplify` completo; `capture-review-debts` em modo autônomo (só expensive_lock ≥4).',
|
||||
`5. PR obrigatoriamente com \`gh pr create --base main\` e "Closes #${issueNumber}" no body (Ready, nunca draft). Escreva a entrada do changelog (\`docs/changelog/<data>-<id>.md\` + \`pnpm changelog:build\`) antes do push (OPS44).`,
|
||||
`5. PR obrigatoriamente com \`gh pr create --base main\` e "Closes #${issueNumber}" no body (Ready, nunca draft). Escreva a entrada do changelog (\`docs/changelog/<data>-<id>.md\` — OPS44; o agregado é gitignored e não é commitado, OPS85) antes do push.`,
|
||||
'6. `gh pr merge --auto --rebase <PR>` e acompanhe `gh pr checks <PR> --watch --required` até o merge. Falha de CI no seu PR é sua (docs/AGENT-OPS.md — "Dono do PR, dono do CI"): corrija na mesma branch.',
|
||||
'',
|
||||
'## Proibido',
|
||||
|
|
|
|||
|
|
@ -1,20 +1,34 @@
|
|||
/**
|
||||
* changelog — pure policy for the per-delivery changelog format (OPS44).
|
||||
* changelog — pure policy for the per-delivery changelog format (OPS44/OPS85).
|
||||
*
|
||||
* Deliveries write `docs/changelog/<date>-<id>.md` (one entry per file,
|
||||
* immune to merge conflicts by construction — same pattern as docs/plans/).
|
||||
* `pnpm changelog:build` inserts those entries into the single readable
|
||||
* docs/CHANGELOG-AGENTS.md in chronological position, keeping the
|
||||
* "Recently resolved" reading intact. The build is insert-only: it never
|
||||
* removes or rewrites existing blocks (historical entries stay as-is —
|
||||
* OPS44 scope), so the aggregate is append-only by construction. CI guards
|
||||
* that property (scripts/check-changelog-append-only.mjs) and that docs
|
||||
* diffs never carry conflict markers
|
||||
* Since OPS85 the committed changelog record is exactly those files: the
|
||||
* readable aggregate docs/CHANGELOG-AGENTS.md is a gitignored artifact
|
||||
* regenerated on demand (`pnpm changelog:build` writes it, `changelog:read`
|
||||
* prints it), seeded from the frozen snapshot
|
||||
* docs/CHANGELOG-AGENTS-HISTORY.md when the local file is absent. The build
|
||||
* stays insert-only: it never removes or rewrites existing blocks. CI guards
|
||||
* (scripts/check-changelog-append-only.mjs) that docs/changelog/ entries are
|
||||
* additions-only, the HISTORY snapshot is frozen, and the regenerable
|
||||
* aggregate is never committed; docs diffs must never carry conflict markers
|
||||
* (scripts/check-docs-conflict-markers.mjs).
|
||||
*/
|
||||
|
||||
export const CHANGELOG_DIR = 'docs/changelog'
|
||||
export const CHANGELOG_AGGREGATE = 'docs/CHANGELOG-AGENTS.md'
|
||||
export const CHANGELOG_HISTORY = 'docs/CHANGELOG-AGENTS-HISTORY.md'
|
||||
|
||||
/**
|
||||
* Header of the regenerated aggregate (OPS85). Used as the seed header when
|
||||
* the local aggregate is absent and the build starts from the HISTORY
|
||||
* snapshot. Must contain the `---` separator (splitChangelogBody contract).
|
||||
*/
|
||||
export const CHANGELOG_HEADER = `# Changelog de contexto para agentes (fatias do AGENTS.md)
|
||||
|
||||
Conteúdo movido do \`AGENTS.md\` em 2026-07-30 (fatiamento do paradigma de agentes paralelos): "Known Gaps" e todos os blocos "Recently resolved". Histórico — leia só quando precisar do contexto de uma entrega passada; o sempre-presente fica no \`AGENTS.md\`. **Este arquivo é um artefato regenerado sob demanda (OPS85):** é gitignored e nunca é commitado. Cada entrega registra UMA entrada em \`docs/changelog/<data>-<id>.md\` (único registro versionado); o agregado é gerado por \`pnpm changelog:build\` (grava aqui) ou \`pnpm changelog:read\` (stdout), com seed do snapshot congelado \`docs/CHANGELOG-AGENTS-HISTORY.md\` quando o arquivo local não existe. Guard de CI \`docs-guards\` (entradas additions-only, HISTORY congelado, agregado não-commitado) e escape \`changelog-rewrite:\` documentados em AGENT-OPS.
|
||||
|
||||
---`
|
||||
|
||||
/** `2026-08-13-ops44.md` — date desc, then id asc. */
|
||||
const ENTRY_FILE_RE = /^(\d{4})-(\d{2})-(\d{2})-([a-z0-9-]+)\.md$/
|
||||
|
|
@ -151,68 +165,80 @@ export function entryDateOf(block) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Append-only policy for the changelog (CI guard).
|
||||
* Changelog policy for the committed diff (CI + pre-push guard).
|
||||
*
|
||||
* The aggregate must never lose an existing line: every line of the old
|
||||
* content must still appear in the new content with equal or greater
|
||||
* count (multiset inclusion — honest under line moves/reordering, unlike
|
||||
* a diff parser). Files under docs/changelog/ must be additions only
|
||||
* (entries are immutable once committed; a deletion or modification is a
|
||||
* silent data-loss path).
|
||||
* Since OPS85 the committed changelog record is per-file only:
|
||||
* - files under docs/changelog/ must be additions only (entries are
|
||||
* immutable once committed; a deletion or modification is a silent
|
||||
* data-loss path);
|
||||
* - docs/CHANGELOG-AGENTS-HISTORY.md is a frozen snapshot of the old
|
||||
* aggregate — only its creation (status A) is allowed, so any later
|
||||
* modification or deletion fails;
|
||||
* - the regenerable docs/CHANGELOG-AGENTS.md must never appear in the
|
||||
* diff; the one-time removal (status D) is allowed only in the same
|
||||
* diff that creates the HISTORY snapshot (the OPS85 migration).
|
||||
*
|
||||
* Legitimate restorations (D8-style) and header rewrites escape via the
|
||||
* documented `changelog-rewrite:` token in the PR body (see AGENT-OPS).
|
||||
* Legitimate exceptions (D8-style restorations, HISTORY corrections) escape
|
||||
* via the documented `changelog-rewrite:` token in the PR body (see
|
||||
* AGENT-OPS). The old committed-aggregate multiset check died with OPS85:
|
||||
* the "no silent entry loss" guarantee now lives in the per-file
|
||||
* additions-only rule plus the generator post-condition in
|
||||
* scripts/build-changelog.mjs.
|
||||
*
|
||||
* @param {{ oldAggregate: string, newAggregate: string,
|
||||
* changelogDiff: { status: string, path: string }[] }} input
|
||||
* @param {{ changelogDiff: { status: string, path: string }[],
|
||||
* historyDiff: { status: string, path: string }[],
|
||||
* aggregateDiff: { status: string, path: string }[] }} input
|
||||
* @returns {{ ok: true } | { ok: false, message: string }}
|
||||
*/
|
||||
export function assertChangelogAppendOnly({ oldAggregate, newAggregate, changelogDiff }) {
|
||||
const missing = missingLines(oldAggregate, newAggregate)
|
||||
if (missing.length > 0) {
|
||||
export function assertChangelogAppendOnly({ changelogDiff, historyDiff, aggregateDiff }) {
|
||||
const touchedEntries = changelogDiff.filter((change) => change.status !== 'A')
|
||||
if (touchedEntries.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
message:
|
||||
`docs/CHANGELOG-AGENTS.md não é append-only: ${missing.length} linha(s) existente(s) ` +
|
||||
`somem (ex.: ${JSON.stringify(missing[0])}). Restaurações legítimas: use ` +
|
||||
'`changelog-rewrite: <motivo>` no body do PR (linha própria; definição em AGENT-OPS).',
|
||||
}
|
||||
}
|
||||
|
||||
const touched = changelogDiff.filter((change) => change.status !== 'A')
|
||||
if (touched.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
message:
|
||||
`docs/changelog/ não é additions-only: ${touched
|
||||
`docs/changelog/ não é additions-only: ${touchedEntries
|
||||
.map((change) => `${change.status} ${change.path}`)
|
||||
.join(', ')}. Entradas são imutáveis; restaurações legítimas: use ` +
|
||||
'`changelog-rewrite: <motivo>` no body do PR (linha própria; definição em AGENT-OPS).',
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} oldContent
|
||||
* @param {string} newContent
|
||||
* @returns {string[]} - distinct old lines missing or under-counted in new
|
||||
*/
|
||||
export function missingLines(oldContent, newContent) {
|
||||
const counts = new Map()
|
||||
for (const line of newContent.split('\n')) {
|
||||
counts.set(line, (counts.get(line) ?? 0) + 1)
|
||||
}
|
||||
|
||||
const missing = []
|
||||
for (const line of oldContent.split('\n')) {
|
||||
const remaining = counts.get(line) ?? 0
|
||||
if (remaining <= 0) {
|
||||
missing.push(line)
|
||||
} else {
|
||||
counts.set(line, remaining - 1)
|
||||
const touchedHistory = historyDiff.filter((change) => change.status !== 'A')
|
||||
if (touchedHistory.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
message:
|
||||
`${CHANGELOG_HISTORY} é um snapshot congelado: ${touchedHistory
|
||||
.map((change) => `${change.status} ${change.path}`)
|
||||
.join(', ')} não é permitido. Correções legítimas: use ` +
|
||||
'`changelog-rewrite: <motivo>` no body do PR (linha própria; definição em AGENT-OPS).',
|
||||
}
|
||||
}
|
||||
return [...new Set(missing)]
|
||||
|
||||
const nonDeletions = aggregateDiff.filter((change) => change.status !== 'D')
|
||||
if (nonDeletions.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
message:
|
||||
`${CHANGELOG_AGGREGATE} não deve ser commitado — é um artefato gitignored regenerado ` +
|
||||
`sob demanda (OPS85): ${nonDeletions
|
||||
.map((change) => `${change.status} ${change.path}`)
|
||||
.join(', ')}.`,
|
||||
}
|
||||
}
|
||||
|
||||
const aggregateDeleted = aggregateDiff.some((change) => change.status === 'D')
|
||||
const historyCreated = historyDiff.some((change) => change.status === 'A')
|
||||
if (aggregateDeleted && !historyCreated) {
|
||||
return {
|
||||
ok: false,
|
||||
message:
|
||||
`${CHANGELOG_AGGREGATE} removido sem criar ${CHANGELOG_HISTORY} — a remoção do ` +
|
||||
'agregado commitado só é válida na entrega que congela o snapshot (OPS85). ' +
|
||||
'Exceções legítimas: use `changelog-rewrite: <motivo>` no body do PR (linha ' +
|
||||
'própria; definição em AGENT-OPS).',
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,10 +25,12 @@ describe('buildPoolWorkerPrompt', () => {
|
|||
expect(prompt).toContain('plano de intenção como contrato de engenharia')
|
||||
})
|
||||
|
||||
it('pins the PR contract: base main, Closes #N, changelog entry, rebase auto-merge, CI watch', () => {
|
||||
it('pins the PR contract: base main, Closes #N, changelog entry (no aggregate build), rebase auto-merge, CI watch', () => {
|
||||
expect(prompt).toContain('gh pr create --base main')
|
||||
expect(prompt).toContain('Closes #42')
|
||||
expect(prompt).toContain('changelog:build')
|
||||
expect(prompt).toContain('docs/changelog/<data>-<id>.md')
|
||||
expect(prompt).toContain('não é commitado, OPS85')
|
||||
expect(prompt).not.toContain('changelog:build')
|
||||
expect(prompt).toContain('gh pr merge --auto --rebase')
|
||||
expect(prompt).toContain('gh pr checks <PR> --watch')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ import { describe, expect, it } from 'vitest'
|
|||
import {
|
||||
assertChangelogAppendOnly,
|
||||
buildChangelog,
|
||||
CHANGELOG_AGGREGATE,
|
||||
CHANGELOG_HISTORY,
|
||||
CHANGELOG_REWRITE_ESCAPE_RE,
|
||||
entryDateOf,
|
||||
listChangelogEntries,
|
||||
missingLines,
|
||||
parseChangelogEntryName,
|
||||
splitChangelogBody,
|
||||
} from '../../scripts/lib/changelog.mjs'
|
||||
|
|
@ -189,28 +190,6 @@ describe('buildChangelog', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('missingLines', () => {
|
||||
it('returns [] for a pure addition', () => {
|
||||
expect(missingLines('a\nb\n', 'x\na\nb\n')).toEqual([])
|
||||
})
|
||||
|
||||
it('flags a removed line', () => {
|
||||
expect(missingLines('a\nb\n', 'a\n')).toEqual(['b'])
|
||||
})
|
||||
|
||||
it('treats a moved line as present (multiset, not position)', () => {
|
||||
expect(missingLines('a\nb\nc\n', 'c\na\nb\n')).toEqual([])
|
||||
})
|
||||
|
||||
it('flags a line whose count decreases (duplicate lost)', () => {
|
||||
expect(missingLines('a\na\n', 'a\n')).toEqual(['a'])
|
||||
})
|
||||
|
||||
it('returns distinct missing lines', () => {
|
||||
expect(missingLines('a\nb\nb\n', 'a\n')).toEqual(['b'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('CHANGELOG_REWRITE_ESCAPE_RE', () => {
|
||||
it('matches a standalone changelog-rewrite line in a PR body', () => {
|
||||
expect(
|
||||
|
|
@ -229,44 +208,73 @@ describe('CHANGELOG_REWRITE_ESCAPE_RE', () => {
|
|||
})
|
||||
|
||||
describe('assertChangelogAppendOnly', () => {
|
||||
it('passes on a pure addition with no changelog file touched', () => {
|
||||
it('passes on a pure addition in docs/changelog/ with no other changelog diff', () => {
|
||||
expect(
|
||||
assertChangelogAppendOnly({
|
||||
oldAggregate: 'a\nb\n',
|
||||
newAggregate: 'a\nb\nc\n',
|
||||
changelogDiff: [{ status: 'A', path: 'docs/changelog/2026-08-13-ops44.md' }],
|
||||
historyDiff: [],
|
||||
aggregateDiff: [],
|
||||
}),
|
||||
).toEqual({ ok: true })
|
||||
})
|
||||
|
||||
it('fails when an aggregate line disappears', () => {
|
||||
it('passes on the OPS85 migration — aggregate removed together with HISTORY creation', () => {
|
||||
expect(
|
||||
assertChangelogAppendOnly({
|
||||
changelogDiff: [{ status: 'A', path: 'docs/changelog/2026-08-24-ops85.md' }],
|
||||
historyDiff: [{ status: 'A', path: CHANGELOG_HISTORY }],
|
||||
aggregateDiff: [{ status: 'D', path: CHANGELOG_AGGREGATE }],
|
||||
}),
|
||||
).toEqual({ ok: true })
|
||||
})
|
||||
|
||||
it('fails when the aggregate is added or modified (it must never be committed)', () => {
|
||||
for (const status of ['A', 'M']) {
|
||||
const result = assertChangelogAppendOnly({
|
||||
changelogDiff: [],
|
||||
historyDiff: [],
|
||||
aggregateDiff: [{ status, path: CHANGELOG_AGGREGATE }],
|
||||
})
|
||||
expect(result.ok).toBe(false)
|
||||
if (!result.ok) {
|
||||
expect(result.message).toContain('não deve ser commitado')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('fails when the aggregate is removed without creating the HISTORY snapshot', () => {
|
||||
const result = assertChangelogAppendOnly({
|
||||
oldAggregate: 'a\nb\n',
|
||||
newAggregate: 'a\n',
|
||||
changelogDiff: [],
|
||||
historyDiff: [],
|
||||
aggregateDiff: [{ status: 'D', path: CHANGELOG_AGGREGATE }],
|
||||
})
|
||||
expect(result.ok).toBe(false)
|
||||
if (!result.ok) {
|
||||
expect(result.message).toContain('não é append-only')
|
||||
expect(result.message).toContain('removido sem criar')
|
||||
expect(result.message).toContain('changelog-rewrite')
|
||||
}
|
||||
})
|
||||
|
||||
it('passes on an addition inside docs/changelog/', () => {
|
||||
expect(
|
||||
assertChangelogAppendOnly({
|
||||
oldAggregate: 'a\n',
|
||||
newAggregate: 'a\nb\n',
|
||||
changelogDiff: [{ status: 'A', path: 'docs/changelog/2026-08-13-ops44.md' }],
|
||||
}),
|
||||
).toEqual({ ok: true })
|
||||
it('fails when the HISTORY snapshot is modified or deleted', () => {
|
||||
for (const status of ['M', 'D']) {
|
||||
const result = assertChangelogAppendOnly({
|
||||
changelogDiff: [],
|
||||
historyDiff: [{ status, path: CHANGELOG_HISTORY }],
|
||||
aggregateDiff: [],
|
||||
})
|
||||
expect(result.ok).toBe(false)
|
||||
if (!result.ok) {
|
||||
expect(result.message).toContain('congelado')
|
||||
expect(result.message).toContain('changelog-rewrite')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('fails when a docs/changelog file is modified', () => {
|
||||
const result = assertChangelogAppendOnly({
|
||||
oldAggregate: 'a\n',
|
||||
newAggregate: 'a\nb\n',
|
||||
changelogDiff: [{ status: 'M', path: 'docs/changelog/2026-08-13-ops44.md' }],
|
||||
historyDiff: [],
|
||||
aggregateDiff: [],
|
||||
})
|
||||
expect(result.ok).toBe(false)
|
||||
if (!result.ok) {
|
||||
|
|
@ -277,9 +285,9 @@ describe('assertChangelogAppendOnly', () => {
|
|||
|
||||
it('fails when a docs/changelog file is deleted', () => {
|
||||
const result = assertChangelogAppendOnly({
|
||||
oldAggregate: 'a\n',
|
||||
newAggregate: 'a\n',
|
||||
changelogDiff: [{ status: 'D', path: 'docs/changelog/2026-08-12-b200.md' }],
|
||||
historyDiff: [],
|
||||
aggregateDiff: [],
|
||||
})
|
||||
expect(result.ok).toBe(false)
|
||||
if (!result.ok) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue