Linha de OS: tabela linhas-os.yaml como fallback quando o SD.md não declara

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>
This commit is contained in:
2026-09-04 20:22:15 -03:00
co-authored by Claude Fable 5.1
parent 4ef1a7a4c8
commit a4b1374fad
11 changed files with 329 additions and 121 deletions
+42 -1
View File
@@ -26,7 +26,38 @@ 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 # 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
@@ -56,6 +87,10 @@ class Sessao:
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.
@@ -63,7 +98,13 @@ class Sessao:
É 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