Commit inicial: app de exportação da SD + deploy Docker/Coolify

Aplicação Flask (enviar SD.md -> corrigir pendências -> exportar JSON),
scripts de contrato (scripts/) e infraestrutura de deploy (Dockerfile,
docker-compose.yml) para VPS via Coolify.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 14:07:38 -03:00
co-authored by Claude Sonnet 5
commit 0d4fdd2533
29 changed files with 4268 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
"""
escrita_sd.py — regrava o SD.md preservando os comentários do frontmatter.
Por que round-trip e não yaml.safe_dump: os comentários do frontmatter carregam
decisão de contrato, não enfeite. O campo `estado` da P2·SD10 tem quatro linhas
de correção datada ("CORREÇÃO 17/08/2026 (gestão do contrato): o protótipo
navegável está EM CRIAÇÃO, não em produção como registrado em 11/08"). Um dump
comum apagaria isso sem deixar rastro, e o histórico da decisão morreria num
clique de "Salvar".
Nada aqui grava sozinho: quem chama mostra o diff primeiro e pede confirmação.
"""
from __future__ import annotations
import difflib
import io
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
from caminhos import PROJETOS, RAIZ # noqa: E402
def _yaml_rt():
from ruamel.yaml import YAML
y = YAML()
y.preserve_quotes = True
# Largura efetivamente infinita: o padrão do ruamel é 80 e requebraria
# linhas longas que hoje estão inteiras, produzindo um diff cheio de ruído
# em campos que ninguém tocou.
y.width = 4096
return y
def render_sd(dados, corpo: str) -> str:
"""Monta o texto completo do SD.md: frontmatter + prosa, verbatim."""
buf = io.StringIO()
_yaml_rt().dump(dados, buf)
frontmatter = buf.getvalue()
if not frontmatter.endswith("\n"):
frontmatter += "\n"
corpo = corpo or ""
if corpo and not corpo.startswith("\n"):
corpo = "\n" + corpo
if corpo and not corpo.endswith("\n"):
corpo += "\n"
return f"---\n{frontmatter}---\n{corpo}"
def destino_provavel(sd_id: str) -> Path | None:
"""O SD.md do repositório cujo campo `sd:` casa com o da sessão.
Só devolve caminho quando há UMA correspondência. Duas seria ambiguidade, e
escolher sozinho o arquivo que vai ser sobrescrito não é decisão de código.
"""
if not sd_id:
return None
import yaml
achados = []
for f in sorted(PROJETOS.glob("*/sds/*/SD.md")):
try:
linhas = f.read_text(encoding="utf-8").splitlines()
if not linhas or linhas[0].strip() != "---":
continue
fim = next((i for i, l in enumerate(linhas[1:], 1) if l.strip() == "---"), None)
if fim is None:
continue
fm = yaml.safe_load("\n".join(linhas[1:fim])) or {}
except Exception:
continue
if str(fm.get("sd", "")).strip() == str(sd_id).strip():
achados.append(f)
return achados[0] if len(achados) == 1 else None
def diff_contra(destino: Path, texto_novo: str) -> str:
atual = destino.read_text(encoding="utf-8") if destino.exists() else ""
return "".join(difflib.unified_diff(
atual.splitlines(keepends=True),
texto_novo.splitlines(keepends=True),
fromfile=f"a/{destino.name} (no disco)",
tofile=f"b/{destino.name} (da sessão)",
n=3,
)) or "(sem diferenças)"
def dentro_do_repositorio(destino: Path) -> bool:
try:
destino.resolve().relative_to(RAIZ)
return True
except ValueError:
return False
def gravar(destino: Path, texto: str) -> None:
destino.parent.mkdir(parents=True, exist_ok=True)
destino.write_text(texto, encoding="utf-8")