{
  "schemaVersion": 1,
  "produto": "GradUP",
  "tipo": "caso técnico",
  "verificado_em": "2026-11-02",
  "autor": "Jeremias Cardoso",
  "relacao_comercial": "produto do autor; LMS em implementação",
  "metodo": "Leitura de ExamSortition, ADR-0010, QA 10/07 e templates no commit f349c11 em 02/11/2026. Contagem estática de @Test. Suíte Maven NÃO reexecutada (decisão do dono nesta frente).",
  "contagem": {
    "regra": "Java main/test; @Test estático; resources html/sql/xml.",
    "categorias": {
      "java_main": {
        "arquivos": 437,
        "linhas_fisicas": 31783,
        "linhas_nao_brancas": 27923
      },
      "java_test": {
        "arquivos": 199,
        "linhas_fisicas": 33252,
        "linhas_nao_brancas": 28107
      },
      "resources": {
        "arquivos": 145,
        "linhas_fisicas": 14129
      }
    },
    "testes": {
      "anotacoes_test_estaticas": 1595,
      "ExamSortitionTest": 16,
      "execucao_citada": "QA 10/07/2026 commit 63ad21f: 722 run / 0 fail / 105 skip; não reexecutado em 02/11/2026"
    },
    "historico_git": {
      "commits": 163,
      "head": "f349c11",
      "data_do_commit": "2026-10-29"
    }
  },
  "limites": [
    "Suíte Maven não reexecutada em 02/11/2026.",
    "Entre tentativas, questões/enunciados podem repetir.",
    "Não afirma homologação NR específica nem clientes pagantes.",
    "Não confundir com Empresa_Gradup."
  ],
  "fontes": [
    {
      "repositorio": "Gradup",
      "commit": "f349c11",
      "data_do_commit": "2026-10-29"
    }
  ],
  "alegacoes": [
    {
      "id": "exam-sortition-escopo",
      "alegacao": "ExamSortition sorteia UMA tentativa; função pura.",
      "repositorio": "Gradup",
      "arquivo": "src/main/java/com/portal/assessment/domain/ExamSortition.java",
      "linhas": [
        11,
        14
      ],
      "trecho": "/**\n * Sorteio do conjunto de questoes de UMA tentativa de prova. FUNCAO PURA (sem Spring, sem I/O): recebe\n * o banco do curso, a configuracao (quantas aplicadas + mix de dificuldade) e a fonte de aleatoriedade,\n * e devolve o subconjunto sorteado. A orquestracao (ler banco, gravar tentativa) fica na aplicacao."
    },
    {
      "id": "sem-reposicao",
      "alegacao": "Sem reposição na mesma tentativa.",
      "repositorio": "Gradup",
      "arquivo": "src/main/java/com/portal/assessment/domain/ExamSortition.java",
      "linhas": [
        32,
        32
      ],
      "trecho": " *   <li><strong>Sem reposicao</strong>: uma questao nunca aparece duas vezes na mesma tentativa.</li>"
    },
    {
      "id": "sem-enunciado-repetido",
      "alegacao": "Sem enunciado repetido; fallback prova menor.",
      "repositorio": "Gradup",
      "arquivo": "src/main/java/com/portal/assessment/domain/ExamSortition.java",
      "linhas": [
        33,
        44
      ],
      "trecho": " *   <li><strong>Sem enunciado repetido</strong>: o sorteio nunca escolhe DUAS questoes com o mesmo\n *       enunciado normalizado ({@link Question#normalizeStatement}: sem acento, sem caixa, espacos\n *       colapsados). Sao questoes de ID distinto, mas de conteudo igual, que escaparam da dedup byte a\n *       byte da importacao por um acento/espaco/grau diferente; o guard filtra os candidatos por chave de\n *       enunciado. Cada enunciado ja escolhido descarta de vez os demais candidatos com a mesma chave, em\n *       qualquer faixa (a chave usada so cresce, entao o descarte e seguro). Cotas e fallback por faixa\n *       vizinha continuam valendo sobre os candidatos que sobram. <strong>Fallback honesto</strong>: se,\n *       depois de excluir as quase-duplicatas, nao houver enunciados unicos suficientes para {@code\n *       appliedCount}, a prova sai MENOR do que o pedido, e nunca com uma questao repetida. Melhor uma\n *       prova mais curta do que a mesma pergunta duas vezes; o instrutor e avisado na origem, na tela de\n *       revisao da importacao (\"Possivel duplicada\"). So o ramo de sorteio ({@code appliedCount} menor que\n *       o banco) aplica o guard; \"aplica todas\" devolve o banco integral do instrutor 1:1.</li>"
    },
    {
      "id": "adr-contexto",
      "alegacao": "Antes do ADR-0010: banco inteiro, ordem fixa, corte 70%.",
      "repositorio": "Gradup",
      "arquivo": "docs/decisoes/ADR-0010-prova-sorteada.md",
      "linhas": [
        9,
        9
      ],
      "trecho": "Até aqui a prova de um curso era o banco INTEIRO respondido em ordem fixa, com nota de corte constante (`QuizService.PASSING_PERCENT = 70`). O épico (spec 08/07) pede: banco de até 99 questões por curso ([ADR-0009](ADR-0009-teto-99-questoes-por-curso.md), já implementado), com o instrutor configurando por curso quantas questões são APLICADAS por tentativa, a NOTA DE CORTE e o MIX de dificuldade (ex.: 30/30/40), sorteio aleatório por tentativa quando o banco é maior que as aplicadas, e — requisito NR — o conjunto sorteado PERSISTIDO por tentativa, para o auditor saber exatamente qual prova cada aluno recebeu e para a correção validar contra aquele conjunto (anti-adulteração)."
    },
    {
      "id": "adr-sessao",
      "alegacao": "Conjunto no GET/sessão; SUBMIT persiste; abandono não consome.",
      "repositorio": "Gradup",
      "arquivo": "docs/decisoes/ADR-0010-prova-sorteada.md",
      "linhas": [
        19,
        19
      ],
      "trecho": "3. **Ponte GET→POST via SESSÃO HTTP:** o conjunto nasce no GET, vive na sessão e é persistido junto com a tentativa no SUBMIT; abandono não consome tentativa; F5 no GET mantém o conjunto; POST sem conjunto na sessão re-sorteia sem corrigir e sem consumir tentativa. `grade()` corrige contra o conjunto da sessão (por id), `total` = N sorteado; ids respondidos fora do conjunto são ignorados (defesa anti-injeção)."
    },
    {
      "id": "adr-reroll",
      "alegacao": "Perda de sessão permite re-sortear sem consumir tentativa.",
      "repositorio": "Gradup",
      "arquivo": "docs/decisoes/ADR-0010-prova-sorteada.md",
      "linhas": [
        31,
        31
      ],
      "trecho": "- **Re-rolagem do conjunto sem custo de tentativa:** se a sessão se perde (expiração, restart da aplicação, troca de navegador) antes do submit, o próximo GET sorteia um conjunto NOVO sem consumir tentativa — um aluno paciente pode \"re-rolar\" até receber um conjunto que prefira. Aceito porque a alternativa (tentativa criada no GET) quebraria a semântica de 3 tentativas, e o ganho do abuso é limitado (o banco tem no máximo 99 questões e o gabarito nunca vaza). **Gatilho de mitigação:** abuso real observado (padrão de aberturas repetidas sem submit) → persistir o conjunto pendente por aluno×curso fora da sessão, reusando-o até o submit."
    },
    {
      "id": "adr-corte",
      "alegacao": "Corte não retroativo.",
      "repositorio": "Gradup",
      "arquivo": "docs/decisoes/ADR-0010-prova-sorteada.md",
      "linhas": [
        25,
        25
      ],
      "trecho": "1. **Nota de corte NÃO é retroativa.** Tentativas já gravadas mantêm o `passed` da época; mudar o corte de um curso só afeta tentativas novas. Veredito imutável é o comportamento NR-correto e evita reprocessar histórico."
    },
    {
      "id": "adr-auditoria-ff",
      "alegacao": "Dados de auditoria persistidos; UI fast-follow.",
      "repositorio": "Gradup",
      "arquivo": "docs/decisoes/ADR-0010-prova-sorteada.md",
      "linhas": [
        26,
        26
      ],
      "trecho": "2. **Tela de auditoria por tentativa é fast-follow.** Os DADOS já são persistidos nesta entrega (`quiz_attempt_questions`: conjunto sorteado + resposta + acerto congelado por questão), satisfazendo o requisito de evidência NR; a UI de drill-down por tentativa no portal do auditor entra na próxima frente (registrado em [PONTOS-DE-ATENCAO.md](../PONTOS-DE-ATENCAO.md), seção 14)."
    },
    {
      "id": "qa-e1",
      "alegacao": "QA 10/07: mvn test 722/0/105.",
      "repositorio": "Gradup",
      "arquivo": "docs/qa-reports/qa-2026-07-10-1149.md",
      "linhas": [
        19,
        19
      ],
      "trecho": "| E1 | `mvn -o -B test` completo | `Tests run: 722, Failures: 0, Errors: 0, Skipped: 105` + `BUILD SUCCESS` literal | PASS |"
    },
    {
      "id": "qa-e10",
      "alegacao": "QA E10: enunciado não repete; ExamSortitionTest 16/16.",
      "repositorio": "Gradup",
      "arquivo": "docs/qa-reports/qa-2026-07-10-1149.md",
      "linhas": [
        28,
        28
      ],
      "trecho": "| E10 | Sorteio nunca repete enunciado | Guard de chave normalizada em `ExamSortition` (sem acento/caixa/espaços); `ExamSortitionTest` 16/16, incluindo `nuncaEscolheDuasQuestoesComOMesmoEnunciado`, `trataComoRepetidoEnunciadosQueSoDiferemEmAcentoCaixaEEspaco`, `quandoSoHaDuplicadasAProvaSaiMenorEmVezDeRepetir`, `deficitPorFaixaVizinhaTambemNaoRepeteEnunciado` | PASS |"
    },
    {
      "id": "alternativas-ordem",
      "alegacao": "Template aluno: optionA..E na ordem cadastrada.",
      "repositorio": "Gradup",
      "arquivo": "src/main/resources/templates/assessment/prova-aluno.html",
      "linhas": [
        82,
        106
      ],
      "trecho": "                        <span th:text=\"${q.optionA}\">Alternativa A</span>\n                    </label>\n                    <!-- B (ou Falso) -->\n                    <label style=\"display:flex; align-items:center; gap:14px; text-align:left; padding:16px 18px; border-radius:var(--radius-sm); cursor:pointer; font-size:var(--fs-body); color:var(--color-text); border:1.5px solid var(--color-border-strong); background:var(--color-surface);\">\n                        <input type=\"radio\" required th:field=\"*{answers[__${q.id}__]}\" value=\"2\" style=\"width:22px; height:22px; flex-shrink:0; accent-color:var(--color-brand);\"/>\n                        <span th:unless=\"${ehVF}\" aria-hidden=\"true\" style=\"font-family:var(--font-mono); font-weight:600; color:var(--color-text-soft);\">B</span>\n                        <span th:text=\"${q.optionB}\">Alternativa B</span>\n                    </label>\n                    <!-- C (só múltipla escolha) -->\n                    <label th:if=\"${q.optionC != null}\" style=\"display:flex; align-items:center; gap:14px; text-align:left; padding:16px 18px; border-radius:var(--radius-sm); cursor:pointer; font-size:var(--fs-body); color:var(--color-text); border:1.5px solid var(--color-border-strong); background:var(--color-surface);\">\n                        <input type=\"radio\" required th:field=\"*{answers[__${q.id}__]}\" value=\"3\" style=\"width:22px; height:22px; flex-shrink:0; accent-color:var(--color-brand);\"/>\n                        <span aria-hidden=\"true\" style=\"font-family:var(--font-mono); font-weight:600; color:var(--color-text-soft);\">C</span>\n                        <span th:text=\"${q.optionC}\">Alternativa C</span>\n                    </label>\n                    <!-- D (só múltipla escolha) -->\n                    <label th:if=\"${q.optionD != null}\" style=\"display:flex; align-items:center; gap:14px; text-align:left; padding:16px 18px; border-radius:var(--radius-sm); cursor:pointer; font-size:var(--fs-body); color:var(--color-text); border:1.5px solid var(--color-border-strong); background:var(--color-surface);\">\n                        <input type=\"radio\" required th:field=\"*{answers[__${q.id}__]}\" value=\"4\" style=\"width:22px; height:22px; flex-shrink:0; accent-color:var(--color-brand);\"/>\n                        <span aria-hidden=\"true\" style=\"font-family:var(--font-mono); font-weight:600; color:var(--color-text-soft);\">D</span>\n                        <span th:text=\"${q.optionD}\">Alternativa D</span>\n                    </label>\n                    <!-- E (só múltipla escolha, opcional) -->\n                    <label th:if=\"${q.optionE != null}\" style=\"display:flex; align-items:center; gap:14px; text-align:left; padding:16px 18px; border-radius:var(--radius-sm); cursor:pointer; font-size:var(--fs-body); color:var(--color-text); border:1.5px solid var(--color-border-strong); background:var(--color-surface);\">\n                        <input type=\"radio\" required th:field=\"*{answers[__${q.id}__]}\" value=\"5\" style=\"width:22px; height:22px; flex-shrink:0; accent-color:var(--color-brand);\"/>\n                        <span aria-hidden=\"true\" style=\"font-family:var(--font-mono); font-weight:600; color:var(--color-text-soft);\">E</span>\n                        <span th:text=\"${q.optionE}\">Alternativa E</span>"
    },
    {
      "id": "readme-stack",
      "alegacao": "Stack Java 21 / Spring Boot 3 / Thymeleaf / PostgreSQL.",
      "repositorio": "Gradup",
      "arquivo": "README.md",
      "linhas": [
        16,
        23
      ],
      "trecho": "| Camada | Tecnologia |\n|--------|------------|\n| Linguagem | Java 21 |\n| Backend | Spring Boot 3.x (monolito modular) |\n| UI | Thymeleaf + htmx + Alpine.js (renderização no servidor) |\n| Banco | PostgreSQL 15+ |\n| Build | Maven |\n| Mídia (vídeo) | Embed externo (YouTube/Vimeo) — ver [ADR-0004](docs/decisoes/ADR-0004-video-externo.md) |"
    }
  ]
}
