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:
+94
-12
@@ -30,6 +30,7 @@ except ImportError:
|
||||
sys.exit("Falta pyyaml. pip install pyyaml")
|
||||
|
||||
from caminhos import ITENS as ITENS_YAML
|
||||
from caminhos import LINHAS_OS as LINHAS_OS_YAML
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -40,23 +41,44 @@ class Canonico(NamedTuple):
|
||||
"""O YAML de contrato, carregado uma vez.
|
||||
|
||||
`itens` dá a lista de itens do contrato e a matriz de perfis do TR
|
||||
4.1.1.7.1, que alimenta `alocacoes`. É o ÚNICO arquivo de dados de que o
|
||||
exportador depende. Time-box e tarifa saíram daqui em 2026-09-03: são
|
||||
tabelas do sistema de gestão, e é ele que deriva UST e valor na carga.
|
||||
4.1.1.7.1, que alimenta `alocacoes`. Time-box e tarifa saíram daqui em
|
||||
2026-09-03: são tabelas do sistema de gestão, e é ele que deriva UST e
|
||||
valor na carga.
|
||||
|
||||
O que NÃO está aqui, de propósito: OS e linhas de OS. Elas são cadastro do
|
||||
sistema de gestão e mudam durante o ano; qualquer cópia local envelhece em
|
||||
silêncio. O exportador exige `linha_os` declarado no SD.md e só confere o
|
||||
formato — existência, item e status quem confere é o banco, na carga.
|
||||
`linhas_os` é a tabela OS Mãe × linha × item (contrato/linhas-os.yaml), já
|
||||
preparada por preparar_linhas_os() — ou None quando o arquivo não existe.
|
||||
É FALLBACK, não fonte: só entra quando o entregável NÃO declara `linha_os`
|
||||
no SD.md. Linha declarada vai como está, e existência, item e status quem
|
||||
confere é o banco, na carga. Decisão de 2026-09-04, revendo a de
|
||||
2026-09-03 (que não admitia de-para nenhum): o .md continua mandando; a
|
||||
tabela poupa a digitação, e a aplicação grava no .md a linha que usou.
|
||||
"""
|
||||
itens: dict
|
||||
linhas_os: dict | None = None
|
||||
|
||||
|
||||
def carregar_canonico() -> Canonico:
|
||||
if not ITENS_YAML.exists():
|
||||
raise FileNotFoundError(f"Arquivo canônico ausente: {ITENS_YAML}")
|
||||
itens = yaml.safe_load(ITENS_YAML.read_text(encoding="utf-8"))
|
||||
return Canonico(itens)
|
||||
return Canonico(itens, carregar_linhas_os())
|
||||
|
||||
|
||||
def carregar_linhas_os(caminho: Path = LINHAS_OS_YAML) -> dict | None:
|
||||
"""A tabela de linhas de OS preparada, ou None quando o arquivo não existe.
|
||||
|
||||
Ausente NÃO é erro: o deploy monta contrato/ por volume, e um volume sem o
|
||||
arquivo tem de se comportar como antes de 2026-09-04 — `linha_os` exigido
|
||||
no SD.md. Presente e malformado É erro, e alto: quem edita é gente, e uma
|
||||
entrada torta silenciada apontaria linha errada sem ninguém notar.
|
||||
"""
|
||||
if not caminho.exists():
|
||||
return None
|
||||
bruto = yaml.safe_load(caminho.read_text(encoding="utf-8"))
|
||||
try:
|
||||
return preparar_linhas_os(bruto)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"{caminho.name}: {exc}") from exc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -500,10 +522,10 @@ def conferir_formato_linha_os(linha) -> str | None:
|
||||
|
||||
Só FORMATO. Existência da OS, da linha, item e status são cadastro do
|
||||
sistema de gestão e é ele que os confere na carga (resolução em dois passos
|
||||
no banco, §5) — a carga recusa nomeando o que falta e nada é gravado. Não
|
||||
há cópia local disso aqui de propósito: OS e linhas novas entram durante o
|
||||
ano, e um de-para em arquivo apontaria linha velha sem ninguém notar.
|
||||
Decisão da gestão em 2026-09-03.
|
||||
no banco, §5) — a carga recusa nomeando o que falta e nada é gravado. A
|
||||
tabela linhas-os.yaml NÃO é conferida contra o valor declarado: ela é
|
||||
fallback para a ausência (ver linha_da_tabela), não régua do que a pessoa
|
||||
copiou da tela da OS. Decisões da gestão em 2026-09-03 e 2026-09-04.
|
||||
"""
|
||||
if linha in (None, ""):
|
||||
return ("`linha_os` ausente — ordem_servico.linha é obrigatório na carga e só quem "
|
||||
@@ -514,6 +536,66 @@ def conferir_formato_linha_os(linha) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tabela de linhas de OS — contrato/linhas-os.yaml (fallback)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def preparar_linhas_os(bruto) -> dict:
|
||||
"""Valida o YAML da tabela e a indexa por item.
|
||||
|
||||
Devolve {"atualizado_em", "linhas", "por_item": {item: [linha, ...]},
|
||||
"padrao": {item: linha}}. Reprova, nomeando a entrada: sem `linha` ou
|
||||
`item`, `linha` fora de {OS}-L{n}, linha repetida, e mais de um
|
||||
`padrao: true` no mesmo item — a marca existe para desfazer ambiguidade,
|
||||
e duas a recriam.
|
||||
"""
|
||||
if not isinstance(bruto, dict) or not isinstance(bruto.get("linhas"), list):
|
||||
raise ValueError("esperava um mapa com a lista `linhas`")
|
||||
por_item: dict[str, list[str]] = {}
|
||||
padrao: dict[str, str] = {}
|
||||
vistas: set[str] = set()
|
||||
for k, ent in enumerate(bruto["linhas"], 1):
|
||||
if not isinstance(ent, dict):
|
||||
raise ValueError(f"entrada {k} de `linhas` não é um mapa linha/item/padrao")
|
||||
linha = " ".join(str(ent.get("linha") or "").split())
|
||||
item = " ".join(str(ent.get("item") or "").split())
|
||||
if not linha or not item:
|
||||
raise ValueError(f"entrada {k} de `linhas` precisa de `linha` e `item`")
|
||||
if not _LINHA_OS.match(linha):
|
||||
raise ValueError(f"entrada {k}: linha {linha!r} fora do formato {{OS}}-L{{n}} (ex.: 1090-L1)")
|
||||
if linha in vistas:
|
||||
raise ValueError(f"linha {linha} aparece duas vezes")
|
||||
vistas.add(linha)
|
||||
por_item.setdefault(item, []).append(linha)
|
||||
if "padrao" in ent and not isinstance(ent["padrao"], bool):
|
||||
raise ValueError(f"entrada {k} ({linha}): `padrao` precisa ser true/false, "
|
||||
f"não {ent['padrao']!r}")
|
||||
if ent.get("padrao") is True:
|
||||
if item in padrao:
|
||||
raise ValueError(f"{item} tem duas linhas com `padrao: true` ({padrao[item]} e {linha}) "
|
||||
"— só uma pode ser o padrão")
|
||||
padrao[item] = linha
|
||||
atualizado = bruto.get("atualizado_em")
|
||||
return {"atualizado_em": str(atualizado) if atualizado is not None else None,
|
||||
"linhas": bruto["linhas"], "por_item": por_item, "padrao": padrao}
|
||||
|
||||
|
||||
def linha_da_tabela(tabela: dict | None, item) -> tuple[str | None, list[str]]:
|
||||
"""(linha padrão do item, todas as candidatas) — (None, []) sem tabela ou sem o item.
|
||||
|
||||
Padrão é a linha marcada `padrao: true`; com UMA linha só para o item, ela
|
||||
vale como padrão sem precisar da marca. Com duas ou mais e nenhuma marcada,
|
||||
o padrão é None e o chamador pede para a pessoa escolher entre as candidatas.
|
||||
"""
|
||||
if not tabela or item is None:
|
||||
return None, []
|
||||
candidatas = list(tabela["por_item"].get(str(item), []))
|
||||
padrao = tabela["padrao"].get(str(item))
|
||||
if padrao is None and len(candidatas) == 1:
|
||||
padrao = candidatas[0]
|
||||
return padrao, candidatas
|
||||
|
||||
|
||||
def janela_dias(inicio: date, fim: date) -> int:
|
||||
"""Dias entre as datas, exclusivo — a mesma subtração que a carga faz."""
|
||||
return (fim - inicio).days
|
||||
|
||||
Reference in New Issue
Block a user