Date: 2026-04-30
Status: Draft
Type: Refinamento (não-feature)
Problema
O ciclo editar → salvar → publicar no CMS DevTUI suja o git status do repo leandronsp.com com modificações que não correspondem ao que o usuário escreveu. Drafts viram .md no repo. Despublicar deixa .md órfão. Slug auto-gerado fica desconectado do título. Frontmatter slug: é ignorado. O usuário precisa de um fluxo onde o git diff do repo do blog reflete exatamente a intenção do autor, antes de começar a escrever o próximo artigo.
Invariantes do sistema (estado correto)
Estes são os contratos que o sistema deve respeitar pós-fix. Cada um vira um teste.
- Drafts vivem só no DB.
posts/ nunca contém .md de artigo com status=draft no DB.
.md no FS = conteúdo do editor, byte-a-byte. Nenhum campo de frontmatter é injetado, removido, reordenado ou normalizado pela camada de sync.
- Despublicar remove o
.md do FS. Imediato, no db::unpublish.
- Renomear slug remove o
.md antigo do FS. Vale para auto-slug e slug manual.
- Slug auto-gerado se reconcilia com título não-Untitled. Sempre que o save detecta
is_auto_slug(slug) && title != "Untitled", renomeia. Não depende de title ter mudado nesta sessão.
- Slug é editável via frontmatter. Save lê
slug: do frontmatter; se válido e disponível, faz update_slug + remove .md antigo.
- Sync é idempotente sem efeito colateral. Rodar
sync_to_filesystem N vezes consecutivas com DB inalterado não modifica nenhum byte em posts/.
- Sync nunca toca artigos não-gerenciados.
.md no posts/ sem linha correspondente no DB sobrevive a qualquer sync.
Mapa de bugs → arquivos → invariantes que falham
| # |
Bug |
Local |
Quebra invariante |
| B1 |
sync_to_filesystem itera todos os status, escreve drafts |
src/editor/db.rs:407 |
I1 |
| B2 |
inject_status reescreve frontmatter na hora do sync |
src/editor/db.rs:409 + db.rs:470 |
I2 |
| B3 |
sync_to_filesystem nunca remove .md (não cumpre docstring) |
src/editor/db.rs:398-419 |
I3, I4 |
| B4 |
db::unpublish muda status no DB mas não toca FS |
src/editor/db.rs:224 |
I3 |
| B5 |
Auto-slug rename só dispara quando title != article.title |
src/editor/mod.rs:159-164 |
I5 |
| B6 |
slug: no frontmatter é ignorado |
src/editor/mod.rs:159-164 |
I6 |
| B7 |
Teste sync_writes_draft_with_status_in_frontmatter (db.rs:905) congela o bug |
src/editor/db.rs:905 |
I1 (teste contra invariante) |
Itens fora do escopo deste PRD: preprocess do engine (confirmado in-memory pelo scout), imagens órfãs com prefix obsoleto, round-trip de assets. Não violam invariantes desta lista.
Sequência de fixes (TDD, ordem importa)
Cada item é um commit. Cada um adiciona um teste que prova a invariante antes de mudar produção. RED → GREEN → confirma a invariante.
Fix 1 — Drafts saem do FS (B1, B7)
- Teste RED:
sync_does_not_write_drafts_to_fs — cria draft, roda sync, asserta que posts/<slug>.md não existe.
- Teste a reverter:
sync_writes_draft_with_status_in_frontmatter (db.rs:905). O nome dele descreve o bug. Substitui por sync_removes_md_when_article_unpublished.
- Mudança:
sync_to_filesystem filtra status=published no loop principal. Para drafts (e ex-published que viraram draft), remove posts/<slug>.md se existir.
Fix 2 — Sync escreve conteúdo cru (B2)
- Teste RED:
sync_writes_content_byte_for_byte — cria artigo published com conteúdo X (sem status: no frontmatter), roda sync, asserta que posts/<slug>.md é exatamente X.
- Mudança: remover chamada a
inject_status. db.rs:409 passa a escrever article.content direto.
- Cuidado: o engine hoje filtra drafts via
status: draft no frontmatter. Como Fix 1 garante que drafts não chegam ao FS, o filtro do engine vira no-op para blogs gerenciados. Sem mudança no engine.
Fix 3 — db::unpublish remove .md (B4)
- Teste RED:
unpublish_removes_md_from_fs — publica + sync (cria .md), unpublish, asserta .md não existe.
- Mudança:
db::unpublish recebe posts_dir: &Path, faz remove_file após o UPDATE. Defesa em profundidade, não espera próximo build.
Fix 4 — Auto-slug se reconcilia mesmo sem mudança de título (B5)
- Teste RED:
save_reconciles_stale_auto_slug — cria artigo com title="Real Title" e força slug="untitled" no DB; chama save com conteúdo igual ao DB exceto adicionar uma linha; asserta que slug virou real-title após save.
- Mudança: em
edit_article (mod.rs:159-164), separar duas decisões:
- update_title: só quando
title no frontmatter difere do DB (mantém)
- rename_auto_slug: roda quando
is_auto_slug(article.slug) && title != "Untitled", independente de title ter mudado
Fix 5 — slug: no frontmatter vira fonte de verdade (B6)
- Teste RED:
save_updates_slug_from_frontmatter — artigo com slug foo, edit insere slug: bar no frontmatter, save. Asserta DB slug=bar e posts/foo.md removido.
- Teste RED:
save_rejects_slug_collision — artigo A (slug bar) existe; artigo B tenta slug: bar. Asserta DB de B inalterado.
- Mudança:
edit_article lê slug do frontmatter. Se presente e diferente do DB:
- colisão → log + skip (mantém slug atual)
- ok →
update_slug + remove .md antigo
- Ordem com Fix 4: se ambos disparariam, frontmatter ganha. Adicionar teste
frontmatter_slug_wins_over_auto_rename.
Fix 6 — Sync idempotente confirmado (I7)
- Teste:
sync_is_byte_idempotent — roda sync 3x, asserta mtimes não mudam após a 1ª.
- Mudança: o "skip when content unchanged" de
db.rs:411-414 já cobre isso uma vez Fix 2 esteja em pé. Teste confirma.
Fix 7 — Não tocar não-gerenciados (I8, regression test)
- Já existe
sync_leaves_orphan_md_files_untouched. Conferir que ainda passa após Fix 1-6. Se quebrar, é regressão.
Out of scope
- Back-fill de artigos existentes (id 77 e similares). O slug do 77 vai se reconciliar sozinho na próxima vez que o editor for aberto (Fix 4 cobre).
- Limpeza de imagens órfãs com prefix obsoleto.
- Round-trip de assets
images/ uploads/ no build.
- Mudanças no engine (
build.rs, config.rs). O engine fica intocado.
Critérios de aceite
- Após
cargo test, todos os testes acima passam.
- Após
make blog.build.leandronsp.com em DB inalterado, git -C ../leandronsp.com status --short mostra zero arquivos modificados em articles/.
- Criar draft, salvar com título "Foo Bar": DB tem
slug=foo-bar, posts/foo-bar.md não existe (draft), git status em ../leandronsp.com limpo.
- Publicar o draft acima:
posts/foo-bar.md aparece com conteúdo idêntico ao que está no DB. Despublicar: posts/foo-bar.md desaparece.
- Em artigo já publicado, mudar
slug: no frontmatter para custom-url e salvar: posts/foo-bar.md desaparece, posts/custom-url.md aparece com conteúdo novo.
- Artigo id 77: na próxima abertura/save, slug vira
path-to-vibe-engineering no DB. Como segue draft, nada vai pro FS.
Estimativa
7 commits, todos pequenos (≤80 linhas cada incluindo teste). Sequência total: ~half-day de pair work, considerando que cada fix começa pelo RED.
Date: 2026-04-30
Status: Draft
Type: Refinamento (não-feature)
Problema
O ciclo
editar → salvar → publicarno CMS DevTUI suja ogit statusdo repoleandronsp.comcom modificações que não correspondem ao que o usuário escreveu. Drafts viram.mdno repo. Despublicar deixa.mdórfão. Slug auto-gerado fica desconectado do título. Frontmatterslug:é ignorado. O usuário precisa de um fluxo onde ogit diffdo repo do blog reflete exatamente a intenção do autor, antes de começar a escrever o próximo artigo.Invariantes do sistema (estado correto)
Estes são os contratos que o sistema deve respeitar pós-fix. Cada um vira um teste.
posts/nunca contém.mdde artigo comstatus=draftno DB..mdno FS = conteúdo do editor, byte-a-byte. Nenhum campo de frontmatter é injetado, removido, reordenado ou normalizado pela camada de sync..mddo FS. Imediato, nodb::unpublish..mdantigo do FS. Vale para auto-slug e slug manual.is_auto_slug(slug) && title != "Untitled", renomeia. Não depende detitleter mudado nesta sessão.slug:do frontmatter; se válido e disponível, fazupdate_slug+ remove.mdantigo.sync_to_filesystemN vezes consecutivas com DB inalterado não modifica nenhum byte emposts/..mdnoposts/sem linha correspondente no DB sobrevive a qualquer sync.Mapa de bugs → arquivos → invariantes que falham
sync_to_filesystemitera todos os status, escreve draftssrc/editor/db.rs:407inject_statusreescreve frontmatter na hora do syncsrc/editor/db.rs:409+db.rs:470sync_to_filesystemnunca remove.md(não cumpre docstring)src/editor/db.rs:398-419db::unpublishmuda status no DB mas não toca FSsrc/editor/db.rs:224title != article.titlesrc/editor/mod.rs:159-164slug:no frontmatter é ignoradosrc/editor/mod.rs:159-164sync_writes_draft_with_status_in_frontmatter(db.rs:905) congela o bugsrc/editor/db.rs:905Itens fora do escopo deste PRD: preprocess do engine (confirmado in-memory pelo scout), imagens órfãs com prefix obsoleto, round-trip de assets. Não violam invariantes desta lista.
Sequência de fixes (TDD, ordem importa)
Cada item é um commit. Cada um adiciona um teste que prova a invariante antes de mudar produção. RED → GREEN → confirma a invariante.
Fix 1 — Drafts saem do FS (B1, B7)
sync_does_not_write_drafts_to_fs— cria draft, roda sync, asserta queposts/<slug>.mdnão existe.sync_writes_draft_with_status_in_frontmatter(db.rs:905). O nome dele descreve o bug. Substitui porsync_removes_md_when_article_unpublished.sync_to_filesystemfiltrastatus=publishedno loop principal. Para drafts (e ex-published que viraram draft), removeposts/<slug>.mdse existir.Fix 2 — Sync escreve conteúdo cru (B2)
sync_writes_content_byte_for_byte— cria artigo published com conteúdoX(semstatus:no frontmatter), roda sync, asserta queposts/<slug>.mdé exatamenteX.inject_status.db.rs:409passa a escreverarticle.contentdireto.status: draftno frontmatter. Como Fix 1 garante que drafts não chegam ao FS, o filtro do engine vira no-op para blogs gerenciados. Sem mudança no engine.Fix 3 —
db::unpublishremove.md(B4)unpublish_removes_md_from_fs— publica + sync (cria.md), unpublish, asserta.mdnão existe.db::unpublishrecebeposts_dir: &Path, fazremove_fileapós o UPDATE. Defesa em profundidade, não espera próximo build.Fix 4 — Auto-slug se reconcilia mesmo sem mudança de título (B5)
save_reconciles_stale_auto_slug— cria artigo comtitle="Real Title"e forçaslug="untitled"no DB; chama save com conteúdo igual ao DB exceto adicionar uma linha; asserta que slug viroureal-titleapós save.edit_article(mod.rs:159-164), separar duas decisões:titleno frontmatter difere do DB (mantém)is_auto_slug(article.slug) && title != "Untitled", independente de title ter mudadoFix 5 —
slug:no frontmatter vira fonte de verdade (B6)save_updates_slug_from_frontmatter— artigo com slugfoo, edit insereslug: barno frontmatter, save. Asserta DBslug=bareposts/foo.mdremovido.save_rejects_slug_collision— artigo A (slugbar) existe; artigo B tentaslug: bar. Asserta DB de B inalterado.edit_articlelêslugdo frontmatter. Se presente e diferente do DB:update_slug+ remove.mdantigofrontmatter_slug_wins_over_auto_rename.Fix 6 — Sync idempotente confirmado (I7)
sync_is_byte_idempotent— roda sync 3x, asserta mtimes não mudam após a 1ª.db.rs:411-414já cobre isso uma vez Fix 2 esteja em pé. Teste confirma.Fix 7 — Não tocar não-gerenciados (I8, regression test)
sync_leaves_orphan_md_files_untouched. Conferir que ainda passa após Fix 1-6. Se quebrar, é regressão.Out of scope
images/uploads/no build.build.rs,config.rs). O engine fica intocado.Critérios de aceite
cargo test, todos os testes acima passam.make blog.build.leandronsp.comem DB inalterado,git -C ../leandronsp.com status --shortmostra zero arquivos modificados emarticles/.slug=foo-bar,posts/foo-bar.mdnão existe (draft),git statusem../leandronsp.comlimpo.posts/foo-bar.mdaparece com conteúdo idêntico ao que está no DB. Despublicar:posts/foo-bar.mddesaparece.slug:no frontmatter paracustom-urle salvar:posts/foo-bar.mddesaparece,posts/custom-url.mdaparece com conteúdo novo.path-to-vibe-engineeringno DB. Como segue draft, nada vai pro FS.Estimativa
7 commits, todos pequenos (≤80 linhas cada incluindo teste). Sequência total: ~half-day de pair work, considerando que cada fix começa pelo RED.