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
+39 -14
View File
@@ -28,12 +28,15 @@ reajuste. As chaves continuam no JSON, na mesma posição — só o valor é `nu
`horas_semanais` e `memoria_calculo`, que só existiam para explicar a conta,
saem `null` pelo mesmo motivo.
O que NÃO é derivado, por decisão da gestão (2026-09-03): `ordem_servico.linha`.
OS e linha são cadastro do sistema de gestão e mudam durante o ano; um de-para
local envelheceria em silêncio. Quem emite a SD declara `linha_os` no
entregável, copiado da tela da OS; aqui só o formato `{OS}-L{n}` é conferido,
e ausente é pendência. Existência, item e status quem confere é o banco, na
carga — que recusa nomeando o que falta.
O que NÃO é derivado: `ordem_servico.linha`. OS e linha são cadastro do sistema
de gestão e mudam durante o ano. Quem emite a SD declara `linha_os` no
entregável, copiado da tela da OS; aqui só o formato `{OS}-L{n}` é conferido, e
existência, item e status quem confere é o banco, na carga — que recusa
nomeando o que falta. AUSENTE, a tabela contrato/linhas-os.yaml (se existir)
dá a linha padrão do item — fallback, decidido em 2026-09-04, para poupar a
digitação; sem padrão é pendência, com as candidatas nomeadas quando a tabela
tem mais de uma. A linha declarada nunca é conferida contra a tabela (decisão
de 2026-09-03): o .md manda.
O que este script NÃO faz, por decisão da gestão (2026-09-03): não confere os
tetos de texto da §2/§3/§4 (nome ≤ 160, objetivo ≤ 4000 etc.) — o importador
@@ -85,6 +88,7 @@ from regras_sd import (
enquadramento_tr,
janela_dias,
ler_sd_arquivo,
linha_da_tabela,
perfil_do_cadastro,
secao,
semanas_por_datas,
@@ -421,15 +425,31 @@ def _montar(sd: dict, corpo: str, canon: Canonico, pend: list[Pendencia]) -> dic
"na carga (time-box × semanas das datas). Apague a declaração.")
# -- linha de OS -----------------------------------------------------------
# Declarada no SD.md por quem emite a SD, e só o formato é conferido
# aqui. OS e linha são cadastro do sistema de gestão e mudam durante o
# ano; quem sabe se existem é o banco, na carga. Não há de-para local.
# Declarada no SD.md: vai como está, só o formato é conferido — OS e
# linha são cadastro do sistema de gestão, e quem sabe se existem é o
# banco, na carga. Ausente, a tabela contrato/linhas-os.yaml (se houver)
# dá a linha padrão do item; sem padrão é pendência, e com mais de uma
# candidata a mensagem as nomeia para a pessoa escolher.
linha = None
queixa = conferir_formato_linha_os(e.get("linha_os"))
if queixa:
falta("linha_os", queixa)
declarada = e.get("linha_os")
if declarada in (None, ""):
padrao, candidatas = linha_da_tabela(canon.linhas_os, item) if item_ok else (None, [])
if padrao:
linha = padrao
elif len(candidatas) > 1:
falta("linha_os", f"`linha_os` ausente, e a tabela linhas-os.yaml tem {len(candidatas)} "
f"linhas para {item} sem `padrao` marcado: {', '.join(candidatas)}. "
"Escolha uma aqui, ou marque o padrão na tabela", opcoes=candidatas)
else:
sem_entrada = f" — a tabela linhas-os.yaml não tem linha para {item}" \
if canon.linhas_os and item_ok else ""
falta("linha_os", conferir_formato_linha_os(None) + sem_entrada)
else:
linha = str(e.get("linha_os")).strip()
queixa = conferir_formato_linha_os(declarada)
if queixa:
falta("linha_os", queixa)
else:
linha = str(declarada).strip()
# -- listas filhas -----------------------------------------------------------
for campo in ("atividades", "criterios_aceite"):
@@ -695,8 +715,13 @@ def main() -> int:
print(f" ERRO {f}: {exc}", file=sys.stderr)
falhas.append(f)
continue
# Se montar() passou com `linha_os` ausente, a linha veio da tabela —
# dizer quantas: é a única pista de que o .md ainda não a declara.
da_tabela = sum(1 for e in (sd.get("entregaveis") or [])
if isinstance(e, dict) and e.get("linha_os") in (None, ""))
print(f" {rotulo} → {saida} {len(payload['entregaveis'])} entregável(is) "
f"({', '.join(payload['sd']['itens_contratuais'])})")
f"({', '.join(payload['sd']['itens_contratuais'])})"
+ (f" · {da_tabela} linha(s) de OS da tabela linhas-os.yaml" if da_tabela else ""))
if falhas:
# Isolar sem nomear é pior que o estouro que substitui: o lote