Skip to content

Commit 2fff18a

Browse files
Merge pull request #198 from LayoutParser/feat/contrato-linha-vazia-e-progresso
feat: contrato aditivo de linha vazia/degradacao posicional + fases de progresso
2 parents 8dfb8df + 56f3742 commit 2fff18a

18 files changed

Lines changed: 861 additions & 22 deletions

‎.claude/agent-memory/lp-pm/MEMORY.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
- [Mapeamento campo TXT<->XML 2026-08-16](project_mapeamento-campo-txt-xml-2026-08-16.md) — issues #137-#141 (guarda-chuva + Fases 0-3), plano de `@lp-architect` pro PBI #128/Epic #126 do front-end.
1010
- [Bug/gate issues 2026-08-20](project_bug-gate-issues-2026-08-20.md) — #171-#174, 4 TODOs de @lp-architect confirmados no código (NFe hardcoded, PDF, validação, métricas).
1111
- [Board-sync 2026-08-18](project_board-sync-2026-08-18.md) — 7 issues fechadas com evidência (#122,#33,#111,#113,#92,#93,#51); `Closes #N` na PR não garante fechamento automático, conferir sempre.
12+
- [Contrato linha vazia/progresso/degradação 2026-08-27](project_contrato-linha-vazia-progresso-degradacao-2026-08-27.md) — issues #194-#197; InformacoesParaEDI já resolvido em PR #191, não virou issue nova.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
name: project-contrato-linha-vazia-progresso-degradacao-2026-08-27
3+
description: Issues #194-#197 do doc de arquitetura contrato-linha-vazia-progresso-e-degradacao-posicional-2026-08-27; InformacoesParaEDI já resolvido em PR #191, sem issue nova.
4+
metadata:
5+
type: project
6+
---
7+
8+
Lote formalizado a partir de `docs/architecture/contrato-linha-vazia-progresso-e-degradacao-posicional-2026-08-27.md`
9+
(`@lp-architect`, 2026-08-27), que cobria 3 pedidos do dono + 1 achado já resolvido:
10+
11+
- **#194** — story: `IsDeclaredEmpty` em `LineInfo`. Dono `lp-parser-llm`.
12+
- **#195** — story: fases discretas em `LowCodeTransformationIndexEntry.Status` (`uploaded`→`layout_selected`→`parsing`→`transforming`→`completed`/`partial`). Dono `lp-backend-dev`.
13+
Complementa (não duplica) a **#99** (já existia — instrumentar/documentar o `transformationsTicket`
14+
existente para o "trava em 100%"); #99 é sobre medição/documentação do que já existe, #195 é sobre
15+
estender o enum de `Status` em si.
16+
- **#196** — bug: colapso posicional LINHA006 no `.mqseries` (todos os campos com
17+
`startPosition===endPosition`). Dono `lp-parser-llm`, **bloqueado em `correlationId`** que só o
18+
dono do projeto pode fornecer — severidade marcada "a validar" (sem confirmação de causa raiz
19+
nem frequência em produção, só hipótese fundamentada em código).
20+
- **#197** — story: contrato aditivo `PositionalAlignmentFailed` por linha (sinal genérico de
21+
degradação posicional, sem acoplar a `mapperName` — pedido explícito do dono). Relacionado a
22+
#196 mas não depende do `correlationId` bloqueante daquela.
23+
24+
**Achado importante nesta sessão:** o bug de `InformacoesParaEDI`/LINHA081 (Length=LengthField em
25+
fragmento bruto + falta de `OccurrenceCount`/`IsAggregatedOccurrence`), que o doc de arquitetura
26+
ainda descrevia como "não implementado", **já foi corrigido e mesclado** — commit `a330af2` na
27+
branch `fix/informacoesparaedi-length-e-occurrence-id`, PR **#191 (MERGED)**, validado por `@lp-qa`
28+
(PASS, 393/393 testes) conforme `.claude/agent-memory/lp-qa/informacoesparaedi-occurrencecount-fix-qa-gate.md`.
29+
Não havia issue prévia pra esse bug (nunca chegou a virar item de board), então **nenhuma issue foi
30+
criada** para ele — só registrado aqui como já resolvido. Reforça [[project-backlog-nao-e-prova-do-codigo]]:
31+
o doc de arquitetura (fonte da tarefa) estava desatualizado em relação ao código real; sempre
32+
verificar o estado atual antes de formalizar como pendência.
33+
34+
Todas as 4 issues adicionadas ao Project #2 (Status=Todo). Field-ids confirmados iguais aos já
35+
registrados em [[reference-gh-cli-setup]] (Tipo: story=`b1173f83`, bug=`fb117f1c`; Dono:
36+
lp-parser-llm=`2cab763a`, lp-backend-dev=`c290c76b`).
37+
38+
**Correção de referência:** `gh` neste ambiente (WSL/bash) está em `/usr/bin/gh` no PATH direto —
39+
o caminho absoluto Windows (`C:\Users\...\gh.exe`) documentado em [[reference-gh-cli-setup]] não
40+
existe neste shell. Atualizar a referência para citar ambos os caminhos possíveis conforme o
41+
ambiente (Windows/PowerShell vs. WSL/bash).

‎.claude/agent-memory/lp-pm/reference_gh_cli_setup.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ metadata:
55
type: reference
66
---
77

8-
- `gh` CLI está em `C:\Users\elson.lopes\.local\bin\gh.exe` (caminho completo — não está no PATH). Usar sempre o caminho absoluto nas chamadas Bash.
8+
- `gh` CLI: no shell **PowerShell/Windows** está em `C:\Users\elson.lopes\.local\bin\gh.exe` (não está no PATH lá, usar caminho absoluto). No shell **WSL/bash** (ambiente mais comum destas sessões) `gh` já está no PATH direto (`/usr/bin/gh`, v2.45.0) — usar só `gh` sem prefixo de caminho. Confirmar o shell atual antes de assumir qual dos dois vale.
99
- Repo alvo: `LayoutParser/LayoutParserApi`. Autenticado, com escopo `read:project` já concedido.
1010
- Checar duplicata antes de criar: `gh issue list --repo LayoutParser/LayoutParserApi --search "<termos>" --state all`.
1111
- Dono já autorizou criação direta de issues (sem rascunho prévio) quando a fonte é um diagnóstico técnico bem documentado (ex.: memória de outro agente `@lp-*`). Formato de corpo: `## Contexto` (com link pro arquivo de origem em `.claude/agent-memory/<agente>/`), `## O que falta`, `## Critério de aceite` (checklist), `## Dono natural`, `## Severidade` (ou `## Por que agora` para stories). Ver issues #30-#40 como padrão de capricho.

‎.claude/agent-memory/lp-qa/MEMORY.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@
55
- [Fine-tuning POC Fase 1 dataset QA](finetuning-poc-fase1-dataset-qa.md) — 39 pares filtrados (NFe 31/MDFe 6/CTe 2); 11/11 amostras OK; CTe com amostra fina (só 2); dataset OK p/ Fase 2 RAG.
66
- [AI metrics Gap 3 QA gate](ai-metrics-gap3-qa-gate.md) — 6 bloqueios FECHADOS (9e48650) + hardening CONCERNS (e6df0b7); em aberto: duas pontes ativas contam cada geração 2x (54 vira 108, aprovação 100% vira 50%).
77
- [Técnica: matriz de mutação](tecnica-matriz-de-mutacao.md) — julgue suíte reintroduzindo bugs numa cópia via `git archive` no scratchpad; nunca mutar a árvore compartilhada.
8+
- [InformacoesParaEDI OccurrenceCount fix QA gate](informacoesparaedi-occurrencecount-fix-qa-gate.md) — PASS a330af2 validado c/ amostra real; baseline 704 era stale, correto é 705 (pré-existente, não regressão).
89
- [Cypress alpha emissão normal spec](cypress-alpha-emissao-normal-spec.md) — spec escrita em LayoutParserCypress; ambos pathways (TCL/XSL e LowCode) bloqueados no dev workstation por arquivos que só existem em `C:\inetpub\wwwroot\layoutparser\` de produção.
10+
- [PR #198 LineInfo signals QA gate](pr198-linhainfo-signals-qa-gate.md) — PASS; achado: IsDeclaredEmpty inalcançável na prática (matcher exige prefixo não-espaço); incidente de commits concorrentes no mesmo checkout.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
name: informacoesparaedi-occurrencecount-fix-qa-gate
3+
description: QA gate do fix Bug A (Length real) + OccurrenceCount/IsAggregatedOccurrence em ParsedField (commit a330af2) — PASS validado contra amostra real, não short-circuit
4+
metadata:
5+
type: project
6+
---
7+
8+
Fix de `InformacoesParaEDI`/LINHA081 (Lia, commit `a330af2`, worktree
9+
`agent-a08c990d4efceeb06`) validado com a amostra real (`.claude/tmp/26072026/`, copiada de
10+
`.claude/temp/teste/`) — os 4 testes de `PositionalFormatRegressionTests` rodaram de verdade
11+
(sem short-circuit) e passaram, mais suíte completa (382/382).
12+
13+
**Achado durante a validação:** a contagem de baseline do MQSeries de controle estava errada em
14+
704 — o valor real e correto é **705**. Confirmado isolando a causa: rodei o mesmo teste contra o
15+
worktree PAI (950cdf9, commit anterior ao fix da Lia) com a mesma amostra real e ele também
16+
produzia 705, não 704. Ou seja, o "704" nunca tinha sido validado contra dado real (herdado de um
17+
período em que o teste sempre fazia short-circuit) — não é regressão introduzida pelo fix do Bug
18+
A/OccurrenceCount. Corrigi o assert para 705 e recapturei `MqBaselineSha256` =
19+
`453e9a184e253d1b310f7814282ebfddb9ca5a99f25acc65ecae741060c8ecfd` (script: adicionar
20+
temporariamente `File.WriteAllText` do hash completo, rodar, copiar valor, reverter o
21+
write temporário — `Assert.Equal` do xUnit trunca strings longas na mensagem de falha).
22+
23+
**Why:** confiar apenas no assert de contagem sem isolar a causa (fix novo vs. comportamento
24+
pré-existente) teria devolvido um falso "achado" pra Lia corrigir algo que já estava certo.
25+
26+
**How to apply:** ao validar hash/contagem baseline "STALE" documentado como pendente de
27+
recaptura, sempre isolar se o valor divergente é efeito do fix em revisão ou já preexistia —
28+
usar `git worktree add <commit-pai>` num scratchpad e rodar o mesmo teste contra a mesma amostra
29+
real é o jeito mais direto de provar isso. Ver também [[tecnica-matriz-de-mutacao]] para a mesma
30+
lógica de isolamento (copiar/rodar fora da árvore compartilhada).
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
name: pr198-linhainfo-signals-qa-gate
3+
description: QA gate do PR #198 (IsDeclaredEmpty/PositionalAlignmentFailed/status "failed") — PASS com achado de design em IsDeclaredEmpty
4+
metadata:
5+
type: project
6+
---
7+
8+
PR #198 (`feat/contrato-linha-vazia-e-progresso` → `develop`), revisado em 2026-08-27.
9+
`dotnet build` limpo; `dotnet test` 385/389 (as 4 falhas são as pré-existentes de path
10+
Windows×Linux, ver [[unified-logging-parse-bug-and-log-dir-incident]] linha de raciocínio
11+
similar — não são regressão deste PR).
12+
13+
**Achado de design (não é bug de implementação — o código bate com a spec):**
14+
`LineInfo.IsDeclaredEmpty` é calculado como `string.IsNullOrWhiteSpace(currentLine)` sobre a
15+
linha FÍSICA INTEIRA (Sequencia + InitialValue + campos), exatamente como o
16+
`docs/architecture/contrato-linha-vazia-progresso-e-degradacao-posicional-2026-08-27.md` §1
17+
especifica. Só que todo matcher em `IsLineValidForConfig` (`Services/Implementations/
18+
LayoutParserService .cs`) exige um prefixo NÃO-espaço pra casar a linha com uma config de layout
19+
(sequência numérica de 6 dígitos, `HEADER`, `EDI_`/`ZRSDM_`, ou `999999`) — então uma linha
20+
"identificada" (`matchingLineConfig != null`) NUNCA pode ser 100% whitespace, e uma linha 100%
21+
whitespace nunca é identificada (cai em `unidentifiedLines`, `lineInfos` fica vazio pra ela).
22+
Resultado: `IsDeclaredEmpty=true` é, na prática, inalcançável para MQSeries/IDOC — mesmo quando
23+
o CAMPO de dado real está 100% em branco, porque o Sequencia/InitialValue não-espaço no início
24+
da linha já derruba o `IsNullOrWhiteSpace`. Confirmado com 2 testes que reproduzem exatamente
25+
esse cenário em `tests/LayoutParserApi.Tests/Parsing/LineInfoAdditiveSignalsTests.cs`
26+
(`ACHADO_dado_totalmente_em_branco_no_campo_nao_liga_IsDeclaredEmpty` e
27+
`ACHADO_linha_totalmente_em_branco_nao_gera_LineInfo_nenhum`).
28+
29+
**Why:** se a intenção de produto é "avisar quando o DADO da linha está vazio" (uso plausível:
30+
o front sinalizar ao usuário que uma linha declarada no layout veio sem conteúdo), o sinal como
31+
implementado não entrega isso — ele só dispara pra um cenário que as regras de matching atuais
32+
tornam impossível. Vale a pena `@lp-architect`/`@lp-parser-llm` revisitarem se o cálculo deveria
33+
comparar o(s) campo(s) de DADO (não-Sequencia/InitialValue) em vez da linha bruta inteira.
34+
35+
**How to apply:** ao revisar qualquer sinal aditivo que dependa de "linha bruta" vs. "campo
36+
extraído", extrair um caso de teste síntetico ANTES de aprovar — spec e código podem concordar
37+
entre si e ainda assim não produzirem o comportamento que o produto quer. Não é suficiente
38+
verificar "código bate com spec"; é preciso perguntar se a spec cobre os matchers já existentes.
39+
40+
**Cobertura de teste adicionada nesta sessão** (5 testes de linha + 2 de status "failed" no
41+
índice low-code): `tests/LayoutParserApi.Tests/Parsing/LineInfoAdditiveSignalsTests.cs` (novo
42+
arquivo) e 2 métodos em `tests/LayoutParserApi.Tests/Transformation/
43+
LowCodeTransformationStoreTests.cs`. `PositionalAlignmentFailed` tem reprodução sintética via
44+
dois `FieldElement` com `LengthField=0` consecutivos (mesmo `Start`) — não depende do
45+
correlationId pendente do caso real LINHA006.
46+
47+
**Incidente de processo (não é bug de produto):** durante esta sessão, dois agentes concorrentes
48+
(backend-dev corrigindo `Controllers/ParseController.cs` para expor `LineInfos` no payload de
49+
Upload, e outro fechando a spec doc) rodaram commit no MESMO checkout de working directory
50+
(sem worktree isolado para esta branch), e absorveram sem querer meus arquivos de teste
51+
untracked/modificados nos commits deles (`3ffe2ec`/`abea2b5`) via `git add`/`commit -a` amplo.
52+
Nada foi perdido — o conteúdo está correto na árvore — mas as mensagens de commit não mencionam
53+
os testes de QA que vieram junto. Nenhum push aconteceu ainda (branch local `ahead 2` do
54+
`origin`), então dá pra reescrever a atribuição antes do push se `@lp-devops` achar que vale a
55+
pena; senão, é só ruído de changelog.
56+
57+
**Veredito: PASS.** Build limpo, testes verdes (incluindo os 7 novos), gaps de cobertura
58+
fechados. O achado de `IsDeclaredEmpty` é recomendação de follow-up, não bloqueador — o contrato
59+
aditivo não quebra nada existente, só entrega menos valor de sinal do que a spec pretendia.

‎Controllers/ParseController.cs‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,17 @@ public async Task<IActionResult> Upload(IFormFile layoutFile, IFormFile txtFile,
253253
var autoResult = await transformTask;
254254
syncCts.Dispose();
255255

256-
transformationsStatus = autoResult.Applicable ? "completed" : "not_applicable";
256+
// ✅ "failed" (contrato aditivo 2026-08-27, spec §2): existe candidato mas
257+
// NENHUM teve sucesso — distinto de "completed" (ao menos um candidato OK),
258+
// sem exigir que o front varra o array pra descobrir que deu tudo errado.
259+
var todosFalharam = autoResult.Applicable
260+
&& autoResult.Candidates.Count > 0
261+
&& autoResult.Candidates.All(c => !c.Success);
262+
263+
transformationsStatus = !autoResult.Applicable
264+
? "not_applicable"
265+
: todosFalharam ? "failed" : "completed";
266+
257267
if (autoResult.Applicable)
258268
{
259269
transformations = AplicarTetoDeXmlInline(autoResult.Candidates);
@@ -303,10 +313,11 @@ public async Task<IActionResult> Upload(IFormFile layoutFile, IFormFile txtFile,
303313
summary = result.Summary,
304314
documentStructure = documentStructure,
305315
lineValidations = lineValidations, // Validações e posições calculadas (apenas para layouts configurados)
316+
lineInfos = result.LineInfos, // ✅ Contrato aditivo 2026-08-27: sinais por linha (IsDeclaredEmpty, PositionalAlignmentFailed)
306317
validationErrors = result.ValidationErrors, // ✅ Erros de validação de tamanho de linha
307318
validationWarning = !string.IsNullOrEmpty(result.ErrorMessage) ? result.ErrorMessage : null, // ✅ Aviso se houver erros
308319
transformations, // array de candidatos low-code (mapper/target/xml/sucesso-ou-erro) quando concluído a tempo
309-
transformationsStatus, // "not_applicable" | "completed" | "processing" | "error"
320+
transformationsStatus, // "not_applicable" | "completed" | "failed" | "processing" | "error" (contrato aditivo 2026-08-27: "failed" é novo — ver GetTransformations)
310321
transformationsReason, // opcional: no_mapper | type_not_positional | empty_input | timeout_sync | structural_error
311322
transformationsTicket // consulta do resultado: GET /api/parse/transformations/{ticket}
312323
});
@@ -339,9 +350,22 @@ public async Task<IActionResult> Upload(IFormFile layoutFile, IFormFile txtFile,
339350
/// consome, para não criar um terceiro dialeto (spec §3.3).</para>
340351
/// </summary>
341352
/// <param name="ticket">"{sha256}.{layoutGuid}" — devolvido pelo upload em <c>transformationsTicket</c>.</param>
342-
/// <response code="200">Manifesto encontrado (status "processing" ou "completed").</response>
353+
/// <response code="200">Manifesto encontrado (status "processing" | "completed" | "failed").</response>
343354
/// <response code="400">Ticket fora do formato.</response>
344355
/// <response code="404">Nenhuma execução registrada para este ticket.</response>
356+
/// <remarks>
357+
/// Contrato aditivo (2026-08-27, ver
358+
/// <c>docs/architecture/contrato-linha-vazia-progresso-e-degradacao-posicional-2026-08-27.md</c>
359+
/// §2): o vocabulário completo de fases é <c>"uploaded"</c> → <c>"layout_selected"</c> →
360+
/// <c>"parsing"</c> → <c>"transforming"</c> → <c>"completed"</c>/<c>"failed"</c>, mas as 3
361+
/// primeiras são <b>client-side only</b> — este endpoint só existe (índice só é gravado) a
362+
/// partir de depois que o documento já foi parseado, então a API nunca as emite. O que
363+
/// este endpoint efetivamente retorna em <c>status</c> é <c>"processing"</c> (valor de fio
364+
/// inalterado — equivale à fase "transforming"), <c>"completed"</c> (≥1 candidato com
365+
/// sucesso) ou <c>"failed"</c> (novo: existe candidato, mas nenhum teve sucesso — antes
366+
/// isso vinha como "completed" com <c>success=false</c> em todos os itens de
367+
/// <c>candidates</c>, obrigando o front a inferir o fracasso varrendo o array).
368+
/// </remarks>
345369
[HttpGet("transformations/{ticket}")]
346370
public async Task<IActionResult> GetTransformations(string ticket)
347371
{
@@ -359,7 +383,7 @@ public async Task<IActionResult> GetTransformations(string ticket)
359383
{
360384
success = true,
361385
ticket,
362-
status = entrada.Status, // "processing" | "completed"
386+
status = entrada.Status, // "processing" | "completed" | "failed" (contrato aditivo 2026-08-27)
363387
partial = entrada.Partial, // true = execução interrompida no teto síncrono; pode faltar candidato
364388
candidates = entrada.Candidates.Select(c => new
365389
{

‎Models/Entities/LineInfo.cs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,19 @@ public class LineInfo
88
public int StartPosition { get; set; }
99
public int Length { get; set; }
1010
public string Content { get; set; } = string.Empty;
11+
12+
/// <summary>
13+
/// Aditivo: true quando a linha foi identificada no layout (matchingLineConfig != null)
14+
/// mas o conteúdo bruto da linha é vazio/whitespace. Ortogonal ao Status por campo —
15+
/// não substitui nem altera nenhuma sinalização existente.
16+
/// </summary>
17+
public bool IsDeclaredEmpty { get; set; }
18+
19+
/// <summary>
20+
/// Aditivo: true quando ≥2 campos consecutivos desta ocorrência de linha resolveram para
21+
/// a mesma posição inicial (fieldStart colapsado) — sintoma observável de degradação
22+
/// posicional (ex.: LINHA006), não a causa raiz nem o mapeador de origem.
23+
/// </summary>
24+
public bool PositionalAlignmentFailed { get; set; }
1125
}
1226
}

0 commit comments

Comments
 (0)