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>
100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
"""
|
|
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")
|