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:
+35
-41
@@ -43,6 +43,9 @@ import servidor # noqa: E402
|
||||
|
||||
FALHAS: list[str] = []
|
||||
CLIENTE = servidor.app.test_client()
|
||||
# Lida da tabela, não fixada: o cabeçalho de linhas-os.yaml manda atualizar
|
||||
# `atualizado_em` a cada edição, e um teste pinado ficaria vermelho à toa.
|
||||
DATA_TABELA = servidor.canonico().linhas_os["atualizado_em"]
|
||||
|
||||
SD8 = PROJETOS / "P2-vacinacao/sds/SD8-mvp-vacina-em-dia/SD.md"
|
||||
SD10 = PROJETOS / "P2-vacinacao/sds/SD10-crie/SD.md"
|
||||
@@ -119,56 +122,44 @@ def exportar_pela_cli(sd_md: Path) -> str:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
def preencher_linhas(pag: str, linha: str = "1090-L1") -> str:
|
||||
"""Responde no formulário todas as pendências de `linha_os` com a mesma
|
||||
linha — o que a pessoa faria copiando da tela da OS. Devolve a página."""
|
||||
c = campos_do_form(pag)
|
||||
for k in c:
|
||||
if k.endswith("__linha_os"):
|
||||
c[k] = linha
|
||||
return texto(post("/revisar", c))
|
||||
|
||||
|
||||
def sd_pronta():
|
||||
print("\n1. SD31: só falta a linha de OS; preenchida, o clique exporta — e nada sai antes dele")
|
||||
print("\n1. SD31: o .md não declara linha de OS; a tabela preenche, o clique exporta — e nada sai antes dele")
|
||||
reiniciar()
|
||||
r = enviar(SD31)
|
||||
ck(not baixou(r), "o upload NÃO devolve download — devolve a tela de revisão")
|
||||
pag = texto(r)
|
||||
ck(estado_na_tela(pag) == "Com pendências" and "4 pendência(s)" in pag,
|
||||
"estado: com pendências — as 4 linhas de OS que o SD.md não declara")
|
||||
c = campos_do_form(pag)
|
||||
ck(set(c) == {f"e{i}__linha_os" for i in range(4)},
|
||||
f"o formulário traz SÓ os quatro campos de linha de OS, como texto ({sorted(c)})")
|
||||
ck("<select" not in pag, "sem select de candidatas: não há de-para local, a pessoa copia da tela da OS")
|
||||
ck(not baixou(post("/exportar")), "POST /exportar direto não baixa nada enquanto falta a linha")
|
||||
|
||||
pag = preencher_linhas(pag)
|
||||
ck(estado_na_tela(pag) == "Pronta para exportar", "preenchidas, estado: pronta para exportar")
|
||||
ck(estado_na_tela(pag) == "Pronta para exportar",
|
||||
"estado: pronta — as 4 linhas que o SD.md não declara vieram de contrato/linhas-os.yaml")
|
||||
ck("PENDENTE" not in pag and 'id="form-pendencias"' not in pag,
|
||||
"sem pendência não há formulário nenhum na tela")
|
||||
ck("1090-L1" in pag, "a linha de OS declarada aparece no resumo")
|
||||
ck(pag.count(">tabela</span>") >= 4 and "linhas-os.yaml" in pag and DATA_TABELA in pag,
|
||||
"a tela marca a origem em cada linha e diz a data da tabela")
|
||||
ck("1090-L1" in pag, "a linha padrão de I-02 aparece no resumo")
|
||||
ck("Exportar JSON" in pag and "disabled" not in pag.split("Exportar JSON")[0][-200:],
|
||||
"o botão Exportar está habilitado")
|
||||
ck(not baixou(CLIENTE.get("/revisar")), "GET /revisar não baixa nada — só o /exportar baixa")
|
||||
|
||||
r = post("/exportar")
|
||||
ck(baixou(r) and "P7-SD31.json" in r.headers.get("Content-Disposition", ""), "download entregue")
|
||||
baixado = texto(r)
|
||||
p = json.loads(baixado)
|
||||
ck(p["sd"]["codigo"] == "P7·SD31", "é o JSON da SD certa")
|
||||
ck(all(e["ordem_servico"]["linha"] == "1090-L1" for e in p["entregaveis"]),
|
||||
"todo entregável saiu com a linha de OS preenchida")
|
||||
ck(all(e["item"] == "I-02" and e["ordem_servico"]["linha"] == "1090-L1" for e in p["entregaveis"]),
|
||||
"todo entregável (I-02) saiu com a linha padrão do item na tabela")
|
||||
ck(p["sd"]["status"] == "Planejado" and all(e["status"] == "Rascunho" for e in p["entregaveis"]),
|
||||
"status nos rótulos do contrato")
|
||||
ck(estado_na_tela(pagina()) == "JSON exportado", "a tela registra a exportação")
|
||||
|
||||
# A mesma régua da CLI: grava o SD.md corrigido (Salvar SD.md) e exporta
|
||||
# esse arquivo pela linha de comando — tem de dar os mesmos bytes.
|
||||
# A mesma régua da CLI, dos dois lados: sobre o SD.md ORIGINAL (a CLI
|
||||
# consulta a mesma tabela) e sobre o SD.md gravado pelo Salvar SD.md (que
|
||||
# agora declara as linhas, com a origem em comentário).
|
||||
ck(baixado == exportar_pela_cli(SD31), "bytes idênticos aos do exporta_sd.py sobre o SD.md original")
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
alvo = Path(tmp) / "SD.md"
|
||||
post("/salvar-sd", {"destino": str(alvo), "confirmo": "sim"})
|
||||
ck(alvo.exists() and alvo.read_text(encoding="utf-8").count("linha_os: 1090-L1") == 4,
|
||||
"o SD.md gravado leva as quatro linhas declaradas")
|
||||
txt = alvo.read_text(encoding="utf-8") if alvo.exists() else ""
|
||||
ck(txt.count("linha_os: 1090-L1") == 4 and txt.count(f"# da tabela linhas-os.yaml ({DATA_TABELA})") == 4,
|
||||
"o SD.md gravado declara as quatro linhas, cada uma com a origem em comentário")
|
||||
ck(baixado == exportar_pela_cli(alvo), "bytes idênticos aos do exporta_sd.py sobre o SD.md gravado")
|
||||
|
||||
|
||||
@@ -237,13 +228,11 @@ def po_e_objetivo():
|
||||
reiniciar()
|
||||
pag = texto(enviar(SD10))
|
||||
c = campos_do_form(pag)
|
||||
ck(set(c) == {"sd__po_responsavel", "corpo__objetivo"} | {f"e{i}__linha_os" for i in range(5)},
|
||||
f"PO, objetivo e as cinco linhas de OS no formulário — nada mais ({sorted(c)})")
|
||||
ck(set(c) == {"sd__po_responsavel", "corpo__objetivo"},
|
||||
f"PO e objetivo no formulário — nada mais: as cinco linhas de OS vieram da tabela ({sorted(c)})")
|
||||
ck("[a designar]" not in pag and "[a redigir" not in pag, "nenhum placeholder inventado na tela")
|
||||
c["sd__po_responsavel"] = "Guilherme Bernardino"
|
||||
c["corpo__objetivo"] = "Permitir à equipe do CRIE manter as regras de elegibilidade sem deploy."
|
||||
for i in range(5):
|
||||
c[f"e{i}__linha_os"] = "1090-L1" if i == 2 else "2587-L3" # n3 é I-02; os demais, I-05
|
||||
ck(estado_na_tela(texto(post("/revisar", c))) == "Pronta para exportar", "preenchidos, a SD fica pronta")
|
||||
p = json.loads(texto(post("/exportar")))
|
||||
ck(p["sd"]["po_responsavel"]["nome"] == "Guilherme Bernardino", "o PO chega ao JSON")
|
||||
@@ -253,7 +242,7 @@ def po_e_objetivo():
|
||||
"a Seção 1 foi criada no corpo, antes da Seção 5")
|
||||
ck([e["ordem_servico"]["linha"] for e in p["entregaveis"]]
|
||||
== ["2587-L3", "2587-L3", "1090-L1", "2587-L3", "2587-L3"],
|
||||
"cada entregável saiu com a linha que a pessoa declarou")
|
||||
"cada entregável saiu com o padrão do SEU item na tabela (n3 é I-02; os demais, I-05)")
|
||||
|
||||
|
||||
def sem_datas():
|
||||
@@ -330,7 +319,7 @@ def writeback():
|
||||
|
||||
|
||||
def linha_de_os_no_formulario():
|
||||
print("\n9. Linha de OS: só o formato é conferido aqui; existência é do banco, na carga")
|
||||
print("\n9. Linha de OS: declarada, só o formato é conferido; apagada, a tabela preenche o padrão do item")
|
||||
sd_md = ("---\n"
|
||||
'sd: "P9·SD99"\nprojeto: "P9 · Teste"\ntitulo: "Sintética"\n'
|
||||
'po_responsavel: "Alguém"\nversao: "V1"\nestado: rascunho\n'
|
||||
@@ -341,28 +330,33 @@ def linha_de_os_no_formulario():
|
||||
reiniciar()
|
||||
pag = texto(enviar(Path("sintetica.md"), "sintetica.md", sd_md))
|
||||
ck("fora do formato" in pag and "1090-L1" in pag, "linha mal formada: pendência que mostra o formato")
|
||||
ck('<input name="e0__linha_os"' in pag and "<select" not in pag, "campo de texto, sem lista local")
|
||||
ck('<input name="e0__linha_os"' in pag and "<select" not in pag,
|
||||
"campo de texto: a pessoa corrige o que declarou, a tabela não se mete no declarado")
|
||||
ck(">tabela</span>" not in pag, "linha declarada (mesmo errada) não é trocada pela da tabela")
|
||||
c = campos_do_form(pag)
|
||||
c["e0__linha_os"] = "9999-L7"
|
||||
ck(estado_na_tela(texto(post("/revisar", c))) == "Pronta para exportar",
|
||||
"linha bem formada que o repositório não conhece PASSA — quem confere existência é o banco")
|
||||
"linha bem formada que a tabela não conhece PASSA — quem confere existência é o banco")
|
||||
p = json.loads(texto(post("/exportar")))
|
||||
ck(p["entregaveis"][0]["ordem_servico"]["linha"] == "9999-L7", "o que a pessoa declarou vai no JSON")
|
||||
|
||||
# Apagar a declaração é "não sei": a tabela entra e dá o padrão do item.
|
||||
reiniciar()
|
||||
c = campos_do_form(texto(enviar(Path("sintetica.md"), "sintetica.md", sd_md)))
|
||||
c["e0__linha_os"] = ""
|
||||
pag = texto(post("/revisar", c))
|
||||
ck("`linha_os` ausente" in pag and not baixou(post("/exportar")),
|
||||
"em branco não há fallback nenhum: continua pendente e não exporta")
|
||||
ck("linha_os" not in servidor.ATUAL["sessao"].dados["entregaveis"][0],
|
||||
"e a chave `linha_os` saiu do frontmatter, em vez de virar string vazia")
|
||||
ck(estado_na_tela(pag) == "Pronta para exportar" and ">tabela</span>" in pag,
|
||||
"em branco, a tabela preenche o padrão de I-02 e a tela marca a origem")
|
||||
e0 = servidor.ATUAL["sessao"].dados["entregaveis"][0]
|
||||
ck(e0.get("linha_os") == "1090-L1", "a linha da tabela foi escrita no frontmatter da sessão (para o Salvar SD.md)")
|
||||
p = json.loads(texto(post("/exportar")))
|
||||
ck(p["entregaveis"][0]["ordem_servico"]["linha"] == "1090-L1", "e é ela que vai no JSON")
|
||||
|
||||
|
||||
def editar_depois_de_exportar():
|
||||
print("\n10. Editar depois de exportar desfaz o rótulo 'exportada'")
|
||||
reiniciar()
|
||||
preencher_linhas(texto(enviar(SD31)))
|
||||
enviar(SD31) # pronta de saída: as linhas de OS vêm da tabela
|
||||
post("/exportar")
|
||||
ck(estado_na_tela(pagina()) == "JSON exportado", "exportada")
|
||||
post("/revisar", {"sd__titulo": "outro título"})
|
||||
|
||||
Reference in New Issue
Block a user