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:
@@ -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")
|
||||
Reference in New Issue
Block a user