Decisão de 2026-09-04, revendo a de 2026-09-03 (sem de-para): o .md continua mandando, e a tabela contrato/linhas-os.yaml (OS Mãe × linha × item) só entra quando o entregável não declara `linha_os`. Linha declarada vai como está e não é conferida contra a tabela; existência/item/status seguem sendo do banco. - caminhos.LINHAS_OS; regras_sd: Canonico(itens, linhas_os), carregar_linhas_os (arquivo ausente = fallback desligado), preparar_linhas_os (valida: formato, linha repetida, dois `padrao`, `padrao` não booleano) e linha_da_tabela. - exporta_sd: ausente → padrão do item; 2+ candidatas sem padrão → pendência nomeando-as; item fora da tabela → pendência. CLI informa quantas linhas vieram da tabela. - app: sessao.adotar_linhas_da_tabela grava a linha no frontmatter da sessão com comentário de origem, para o Salvar SD.md deixar a SD autodeclarada; a tela marca `tabela` e mostra a data da tabela. - testes: SD18, SD27, SD31–36 passam a exportar de saída; SD10 só PO+objetivo; casos sintéticos para tabela ausente, ambígua, sem o item e malformada. - deploy: linhas-os.yaml vive em clientes/ (gitignored) — vai por scp ao volume. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
122 lines
5.0 KiB
Python
122 lines
5.0 KiB
Python
"""
|
|
sessao.py — a fonte única da SD entre o upload e o download.
|
|
|
|
O problema que esta camada resolve: a SD é carregada uma vez, vive em memória,
|
|
é editada e reavaliada — e o exportador recebe ESSE objeto, não um caminho de
|
|
arquivo. Sem isto, a correção feita no formulário nunca chegaria ao JSON.
|
|
|
|
O que a sessão sabe: os dados (frontmatter, como CommentedMap do ruamel para o
|
|
writeback preservar comentários), a prosa, e o resultado da última análise do
|
|
exportador — a prévia do payload e a lista de pendências. Pendência vazia é a
|
|
única condição para exportar, e ela é reavaliada no servidor a cada POST: o
|
|
botão desabilitado é conforto de tela.
|
|
|
|
Estado em memória de processo, uma SD por vez: é ferramenta local de uso
|
|
individual. Reiniciar o servidor descarta a sessão — o SD.md de origem continua
|
|
intacto no disco, e nada é gravado sem ação explícita.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
|
|
|
from exporta_sd import Pendencia, analisar # noqa: E402
|
|
from regras_sd import Canonico, linha_da_tabela # noqa: E402
|
|
|
|
|
|
def adotar_linhas_da_tabela(dados, canon: Canonico) -> dict[int, str]:
|
|
"""Escreve NO OBJETO DA SESSÃO a linha padrão da tabela onde o SD.md não
|
|
declara `linha_os`. Devolve {índice do entregável: linha} do que adotou.
|
|
|
|
Por que mutar aqui, e não deixar só o exportador consultar a tabela: o
|
|
Salvar SD.md regrava este objeto. Com a linha adotada nele, a SD sai do
|
|
disco autodeclarada e a tabela deixa de importar para ela — o risco de
|
|
tabela envelhecida morde uma vez só. O comentário de linha diz de onde
|
|
veio, para quem abrir o .md depois. O exportador faz a mesma consulta
|
|
(mesmo helper, mesma tabela), então a CLI sobre o .md original e a app dão
|
|
os mesmos bytes. Idempotente: só toca em quem não declara.
|
|
"""
|
|
tabela = canon.linhas_os
|
|
if not tabela:
|
|
return {}
|
|
adotadas: dict[int, str] = {}
|
|
for i, e in enumerate(dados.get("entregaveis") or []):
|
|
if not isinstance(e, dict) or e.get("linha_os") not in (None, ""):
|
|
continue
|
|
padrao, _ = linha_da_tabela(tabela, e.get("item"))
|
|
if not padrao:
|
|
continue
|
|
e["linha_os"] = padrao
|
|
if hasattr(e, "yaml_add_eol_comment"): # CommentedMap do ruamel
|
|
data = tabela.get("atualizado_em") or "sem data"
|
|
e.yaml_add_eol_comment(f"da tabela linhas-os.yaml ({data}) — o SD.md não declarava",
|
|
"linha_os")
|
|
adotadas[i] = padrao
|
|
return adotadas
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Estados — os que a tela precisa distinguir, e nenhum a mais
|
|
# ---------------------------------------------------------------------------
|
|
|
|
SEM_ARQUIVO = "sem_arquivo"
|
|
COM_PENDENCIAS = "com_pendencias"
|
|
PRONTA = "pronta"
|
|
EXPORTADA = "exportada"
|
|
|
|
ROTULO_ESTADO = {
|
|
SEM_ARQUIVO: "Arquivo não enviado",
|
|
COM_PENDENCIAS: "Com pendências",
|
|
PRONTA: "Pronta para exportar",
|
|
EXPORTADA: "JSON exportado",
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class Sessao:
|
|
nome_arquivo: str
|
|
dados: dict # CommentedMap do ruamel — FONTE ÚNICA, mutada pelo formulário
|
|
corpo: str
|
|
estado: str = COM_PENDENCIAS
|
|
pendencias: list[Pendencia] = field(default_factory=list)
|
|
previa: dict | None = None
|
|
caminho_origem: Path | None = None
|
|
ultimo_erro: str | None = None
|
|
exportada_em: str | None = None
|
|
# {índice do entregável: linha} adotado de linhas-os.yaml porque o SD.md
|
|
# não declarava — a tela marca a origem, e o Salvar SD.md grava.
|
|
linhas_da_tabela: dict = field(default_factory=dict)
|
|
tabela_atualizada_em: str | None = None
|
|
|
|
def reavaliar(self, canon: Canonico) -> None:
|
|
"""Roda o exportador em modo de análise sobre os dados COMO ESTÃO.
|
|
|
|
É grátis e instantâneo; roda a cada edição. Uma edição depois da
|
|
exportação volta a sessão para pronta/pendente: o que foi exportado era
|
|
outra versão dos dados, e a tela não pode dizer "exportada" sobre esta.
|
|
Antes de analisar, adota da tabela a linha de OS que o .md não declara
|
|
(ver adotar_linhas_da_tabela) — e só então o exportador vê os dados.
|
|
"""
|
|
adotadas = adotar_linhas_da_tabela(self.dados, canon)
|
|
if adotadas:
|
|
self.linhas_da_tabela.update(adotadas)
|
|
self.tabela_atualizada_em = (canon.linhas_os or {}).get("atualizado_em")
|
|
self.previa, self.pendencias = analisar(self.dados, self.corpo, canon)
|
|
self.estado = PRONTA if not self.pendencias else COM_PENDENCIAS
|
|
self.exportada_em = None
|
|
|
|
@property
|
|
def pode_exportar(self) -> bool:
|
|
return not self.pendencias
|
|
|
|
def marcar_exportada(self) -> None:
|
|
self.estado = EXPORTADA
|
|
self.exportada_em = datetime.now().isoformat(timespec="seconds")
|
|
|
|
def rotulo_estado(self) -> str:
|
|
return ROTULO_ESTADO.get(self.estado, self.estado)
|