08 · prova
Capacidade e confiabilidade não são a mesma métrica.
pass@k mede se um caso passou ao menos uma vez em k execuções. pass^k mede se passou todas as vezes. O gate de capacidade exige pass@k ≥ 0.90; o gate de regressão exige pass^k = 1.0; --gate both exige os dois. Uma suíte que passa no primeiro e reprova no segundo é instável, e o relatório diz em quanto.
python -m hpp eval run examples/reliable-coding/benchmark-suite.json -k 3 --gate both
python -m hpp benchmark -k 3 --json
Medido: HPP benchmark: pass@k=1.00 · pass^k=1.00 · gate=PASS sobre dez controles (contrato do manifesto, enforcement de política, waves do WorkGraph, colisão de lanes, frescura de monitor, proveniência de contexto, piso de roteamento, gate evento/evidência, determinismo do grafo, atestação de evidência), três execuções cada. O relatório JSON carrega o sha256 da suíte e a plataforma em que rodou. Exit 0 quando o gate passa, 1 quando não.
Uma decisão tomada fora é medida, nunca confiada por padrão
Novo na 2.6.0
O harness não chama modelo. Uma decisão pequena tomada fora dele — por uma regra, uma pessoa, um modelo local ou um modelo de decisão tipada hospedado — pode ser registrada como hpp.decision/v1: sempre advisory; com raise-only ela pode elevar um valor declarado e nunca baixá-lo; abstention e instrument-failure são desfechos que não mudam nada. hpp decide eval roda o decisor que você nomeia como comando contra casos rotulados e reporta cobertura, acerto seletivo, erros confiantes, abstenções e falhas separadamente, com uma curva de cobertura por limiar de confiança. A versão 1 mede só perguntas de escolha.
python -m hpp decide validate decision.json
python -m hpp decide eval examples/typed-decisions/gotcha-family-suite.json --decider-command '["python", "examples/typed-decisions/baseline_decider.py"]'
decide eval SUITE — --decider-command recebe o argv do decisor como array JSON de strings, executado sem shell; omita-o para reexecutar os registros guardados nos casos da suíte, onde um caso sem registro conta como falha de instrumento. --timeout (padrão 10.0) é o número de segundos por caso antes de esse caso contar como falha de instrumento. O gate: --min-selective-accuracy (padrão 0.9), --max-confident-errors (padrão 0; erro confiante é uma resposta errada com confiança igual ou acima do confidence_floor da suíte, 0.6 quando a suíte não define um) e --max-failures (padrão 0). Exit 0 o gate passou · 1 o gate reprovou, ou nada foi decidido e não há acerto a medir · 2 registro, suíte ou argv inválido.
decide validate RECORD — imprime valid e o valor efetivo sobre o qual um consumidor pode agir, com a sua ação (raised, kept, advised ou none). Exit 0 válido · 2 recusado, nomeando a primeira regra quebrada.
Medido num checkout de main: o decisor baseline na suíte de exemplo → 15 casos, 12 decididos, 3 abstenções, 0 falhas de instrumento, cobertura 0.8, acerto seletivo 1.0, every threshold held, exit 0. A mesma suíte sem --decider-command não guarda registros → 15 falhas de instrumento, no decision was made, exit 1. Um registro com "authority": "binding" → hpp: authority must be 'advisory': …, exit 2. O pacote 2.5.8 não tem o comando decide: ali, hpp decide é uma escolha inválida, exit 2.
Como funciona de ponta a ponta — declarar um conselheiro como o Jev, fixar a versão, registrar, validar e medir — está no capítulo 09. O adaptador e os riscos dele estão em examples/typed-decisions.
Pacotes de evidência: o exit code de um critério, medido fora do modelo
Novo na 2.6.0
O hpp não dirige navegador e não chama modelo. O hpp evidence run roda o comando-critério que você declara — uma spec ponta a ponta, uma suíte pytest, qualquer script — como argv sem shell, mede o exit code, faz o hash de cada arquivo de artefato que você declarou e escreve .hpp/evidence/<id>-<UTC>.json (hpp.evidence/v1): o comando, o commit-base, o exit code, o veredito, stdout e stderr como contagem de bytes e sha256 (nunca o texto), os artefatos e um auto-hash. O veredito só é passed quando o comando saiu com 0 e cada padrão declarado casou com um arquivo que esta execução escreveu (um arquivo intocado desde antes da execução sai como unchanged e não conta); fora isso é failed, missing-artifacts, timeout ou could-not-start. O hpp evidence verify re-deriva um registro a partir dos arquivos no disco. O registro tem auto-hash, e só é assinado com --sign-key. O auto-hash torna visível um registro editado; ele não é assinatura — quem pode escrever o arquivo pode reescrevê-lo, e ao hash junto —, então um checker que não pode confiar no maker roda o comando de novo a partir da sua própria lane em vez de se apoiar só no verify, ou pede uma assinatura e a confere com --allowed-signers (assinatura opcional, abaixo).
python -m hpp evidence run --id smoke-page --artifact out/report.html --artifact out/smoke.log -- python examples/evidence/smoke_page.py
python -m hpp evidence verify <record_path>
evidence run --id ID [--artifact GLOB]... -- COMMAND — --artifact é um glob relativo ao workspace, repetível; --timeout (padrão 600.0) é o número de segundos antes de a execução contar como timeout; --out é o diretório do registro, relativo ao workspace (padrão .hpp/evidence); --record-event acrescenta evidence_recorded ao log de eventos quando, e só quando, o pacote passou, e exige ao menos um --artifact. Um argv com cara de segredo, um caminho de artefato ou de --out fora do workspace, um id inválido ou um timeout não positivo são recusados antes de qualquer execução. Exit 0 passou · 1 não passou · 2 recusado, ou o evento não pôde ser acrescentado.
evidence run ... --sign-key KEY --signer NAME (novo na 2.10.0) — assina o registro; --signer é obrigatório com --sign-key, e os dois são conferidos antes de o comando rodar.
evidence verify RECORD — o record_path que o run imprimiu; --allowed-signers FILE (novo na 2.10.0) exige também uma assinatura que esse arquivo aceite. Exit 0 valid, registro íntegro de uma execução que passou · 1 not-evidence, registro íntegro de uma execução que não passou · 2 blocked, o registro foi editado ou se contradiz, ou um artefato mudou ou sumiu, ou — com --allowed-signers — a assinatura falta, não é aceita ou cobre bytes alterados.
Medido num checkout de main, numa cópia da árvore para que out/ e .hpp/ fossem escritos ali: o demo → passed, exit 0, e o verify do seu registro → valid, exit 0. Com --break depois do script, os dois artefatos foram escritos e o veredito foi failed, exit 1; o verify desse registro → not-evidence, exit 1, e o de uma cópia editada para dizer passed → blocked, the record was edited after it was written, exit 2. O primeiro registro, verificado de novo depois que a segunda execução reescreveu out/ → blocked, 2 artifact(s) changed since the run, exit 2: runners ponta a ponta que limpam o diretório de saída no início de cada execução fazem o mesmo, então dê aos artefatos que precisam continuar verificáveis um diretório por execução. --record-event sem work_started no log → exit 2, evento não acrescentado; depois de event append --type work_started ele levou o loop a evidenced. O pacote 2.5.8 não tem o comando evidence: ali, hpp evidence é uma escolha inválida, exit 2.
Novo na 2.7.0: hpp evidence mutate --id ID (--mutants FILE | --generate FILE)... -- COMMAND pergunta se o critério perceberia código quebrado. Ele roda o comando numa cópia do workspace, limpa (tem de passar, senão o veredito é no-control, exit 2, e nenhum mutante roda), depois uma vez por mutante (tem de falhar). --generate muta um arquivo Python operador a operador (==↔!=, <↔<=, >↔>=, and↔or, True↔False), nunca dentro de strings ou comentários; --mutants lê {id, file, find, replace} declarados. Exit 0 sensitive · 1 blind-spots, incomplete ou no-mutants · 2 no-control ou recusado. Medido em examples/criterion-sensitivity: a checagem forte mata 3 de 3 mutantes, exit 0; a fraca, igualmente verde, deixa passar 3 de 3 e os nomeia, exit 1.
Veja examples/evidence para o mesmo padrão em volta de um runner ponta a ponta de verdade.
Régua de recuperação: o recuperador, medido separado da resposta
Novo na 2.6.0
O harness não roda índice e não chama modelo. Um recuperador é um comando que você declara: ele lê {"query": …, "k": …} como JSON no stdin e imprime ids ranqueados, o melhor primeiro. O hpp retrieval eval pontua o top k de cada resposta contra os ids que uma suíte hpp.retrieval-suite/v1 marca como relevantes — hit@k, recall@k, precision@k, MRR e nDCG@k — e conta à parte as falhas de instrumento (não iniciou, timeout, exit diferente de zero, saída que não é JSON, id duplicado): elas nunca são pontuadas, e as médias cobrem só os casos medidos. Sem nenhum caso medido as métricas são nulas e o gate diz por quê, nunca 0%. Se uma boa resposta pode ser gerada a partir do que foi recuperado é outra pergunta, que não se faz aqui.
python -m hpp retrieval eval examples/retrieval/suite.json
python -m hpp retrieval eval examples/retrieval/suite.json --retriever-command '["python", "examples/retrieval/keyword_retriever.py"]'
retrieval eval SUITE — --retriever-command recebe o argv do recuperador como array JSON de strings, executado sem shell; omita-o para reexecutar os resultados guardados nos casos da suíte. -k substitui o corte da suíte. --timeout (padrão 10.0) é o número de segundos por caso antes de esse caso contar como falha de instrumento. O gate: ao menos um caso medido, --min-recall (padrão 0.8) para o recall@k médio e --max-failures (padrão 0). Exit 0 o gate passou · 1 o gate reprovou · 2 suíte ou argumento recusado.
Medido num checkout de main: os dois comandos → 7 casos medidos, 0 falhas de instrumento, recall@3 médio 0.786, hit@3 0.857, MRR 0.786, nDCG@3 0.749, mean recall@k 0.786 < 0.8, exit 1 — o baseline por palavra-chave erra por completo a pergunta parafraseada, um 0 medido, e o gate padrão reprova; com --min-recall 0.75 → every threshold held, exit 0. Um recuperador que sai com 3 em todo caso → 7 falhas de instrumento, métricas nulas, no case was measured (7 instrument failures); there is no recall to measure, exit 1. --retriever-command '"python"' → hpp: --retriever-command must be a JSON array of strings (argv, no shell), exit 2. A suíte é sintética e o baseline foi escrito junto com ela: estes números provam que a régua funciona, não que algum recuperador é bom. O pacote 2.5.8 não tem o comando retrieval: ali ele é uma escolha inválida, exit 2.
Veja examples/retrieval para o contrato do recuperador e o formato da suíte.
Checagem de citações: todo marcador resolve, ou o exit code diz qual não resolve
Novo na 2.6.0
Uma resposta escrita a partir de fontes marca cada afirmação com o id da fonte em que se apoia — [ID:<id>] por padrão. O hpp cite check lê a resposta e a lista de itens de contexto a partir da qual ela foi escrita e reporta, sem modelo: um marcador cujo id não está no contexto (UNKNOWN_ID), um marcador que nomeia um intervalo ou uma lista (RANGE) e um marcador vazio (EMPTY_MARKER) bloqueiam; mais de --max-per-sentence marcadores numa frase (TOO_MANY) e uma frase com número, percentual, valor monetário ou data e nenhum marcador (UNCITED_CLAIM) avisam. Ela prova que cada marcador resolve, não que a fonte citada sustenta a frase: esse julgamento precisa de um leitor. O separador de frases e o detector de números são heurísticas, listadas na docstring de hpp/citations.py.
python -m hpp cite check --text examples/citations/answer.md --context examples/citations/context.json
cite check --text FILE --context FILE — --context é uma lista JSON de itens com id (e o seu text), ou de ids soltos; --max-per-sentence (padrão 4); --marker recebe uma regex com exatamente um grupo de captura, o id. Texto vazio, texto que é só código, texto ou contexto com cara de segredo e uma regex inutilizável são recusados. Exit 0 limpo · 1 aviso (TOO_MANY, UNCITED_CLAIM) · 2 bloqueio (UNKNOWN_ID, RANGE, EMPTY_MARKER) ou entrada recusada.
Medido num checkout de main: a resposta de exemplo → veredito ok, 7 frases, 5 marcadores, 4 dos 5 ids de contexto citados, exit 0. Em cópias dela: [ID:glossary] trocado por [ID:glossary-v2] → UNKNOWN_ID, exit 2; por [ID:runbook-7,sla-2026] → RANGE, exit 2; o marcador removido da frase com 4 engineers → UNCITED_CLAIM, exit 1. E o limite, medido: 42 minutes trocado por 90 minutes, ainda citado à fonte que diz 42 → veredito ok, exit 0. O pacote 2.5.8 não tem o comando cite: ali ele é uma escolha inválida, exit 2.
Veja examples/citations para outra sintaxe de marcador e o formato do relatório.
House Session: um painel de decisores pinados, contado, parado e selado
Novo na 2.7.0
O harness não chama modelo: os assentos e o juiz respondem fora dele, nas próprias lanes. hpp deliberate confere o que eles disseram. Um painel (hpp.panel/v1) não começa a menos que todo assento nomeie um modelo pinado (um alias como -latest é recusado), os participantes venham de pelo menos duas famílias de modelo, exatamente um juiz sente numa lane que nenhum participante usa, e os papéis sirvam ao session_type — um review exige dois examinadores e um assento dissidente. Cada turno (hpp.turn/v1) lista os turnos que o assento tinha lido: um turno da rodada 1 que leu algum é recusado, então a rodada cega é verificável. Um assento sem turno numa rodada não é julgado, nunca voto contra, e bloqueia o veredito. A sessão para por regra — not-judged, grounded-convergence, paused-budget, no-new-evidence, max-rounds — e escalate leva a uma pessoa a dissidência fundamentada que sobreviveu. record sela o painel, os turnos, a contagem, a parada, o hpp.decision/v1 do juiz, o veredito e a dissidência que perdeu em hpp.deliberation/v2 (a forma selada desde a 2.8.0; um registro v1 só é verificado); verify confere o selo e re-deriva contagem, parada e veredito a partir dos turnos. O selo não é assinatura: uma reescrita coerente de um turno ou do registro do juiz, selada de novo, verifica, então guarde o texto verbatim e a resposta bruta do juiz, cujos hashes o registro carrega. A sessão termina na primeira rodada em que uma regra vale; turnos de rodadas posteriores são recusados, e o registro do juiz precisa vir do provedor e do modelo pinado do assento do juiz. A decisão humana é escrita depois, fora do selo.
Novo na 2.8.0: um painel pode declarar evidence.ids — plan --context FILE os preenche a partir do contexto que os assentos receberam, --evidence RECORD... acrescenta cada registro hpp.evidence/v1 que verifica. Aí um fact só fundamenta uma posição por uma referência que resolve; qualquer outra referência é unsupported, listada por rodada e nunca contada como evidência nova. Quando o veredito deixa de pé uma dissidência fundamentada, o record exige --rationale, um hpp.rationale/v1 do assento do juiz com um steelman por posição dissidente e ao menos um would_change_if. O registro é hpp.deliberation/v2; um registro v1 selado pela 2.7.0 continua verificando.
Novo na 2.8.0, no módulo de lanes: house_session.py families diz se o host consegue sentar duas famílias de modelo (uma só é DEFERRED, nunca um painel de uma família), e house_session.py seat roda o comando de um assento no seu próprio worktree, tirando a impressão digital do worktree antes e depois — um assento que escreveu não tem turno (exit 2), um assento que falhou não é julgado (exit 1). O /deliberate no Claude Code e a skill house-session no Claude Code e no Codex percorrem a sessão inteira.
python -m hpp deliberate plan examples/house-session/review/panel.json
python -m hpp deliberate stop --panel examples/house-session/review/panel.json --turns examples/house-session/review/turns.json
python -m hpp deliberate record --panel examples/house-session/review/panel.json --turns examples/house-session/review/turns.json --judge examples/house-session/review/judge.json --rationale examples/house-session/review/rationale.json --out out/record.json
python -m hpp deliberate verify out/record.json
deliberate plan FILE [--context FILE] [--evidence RECORD...] [--out FILE] · deliberate validate FILE [--panel FILE] — validate lê um painel, um turno (com o painel a que pertence) ou um registro, que ele verifica. Exit 0 válido · 2 recusado, nomeando a primeira regra quebrada.
deliberate tally --panel FILE --turns FILE · deliberate stop --panel FILE --turns FILE — --turns é uma lista JSON de hpp.turn/v1. Exit 0 relatado · 2 recusado.
deliberate record --panel FILE --turns FILE [--judge FILE] [--rationale FILE] --out FILE — recusa sessão que não parou, veredito sem juiz, veredito sobre dissidência fundamentada sem steelman, e juiz que respondeu outra pergunta, outro estado ou de outro modelo que não o do assento do juiz. Exit 0 selado com a resposta do juiz · 1 selado, veredito bloqueado ou juiz falhou · 2 recusado.
deliberate verify RECORD — exit 0 íntegro · 2 quebrado: editado depois de selado, ou não se re-deriva dos próprios turnos e do juiz.
Novo na 2.9.0: uma sessão selada alimenta os comandos que agem sobre um veredito, e só pode tornar o resultado mais estrito. hpp route --deliberation PLAN-RECORD --maker-family F deixa uma sessão plan subir o risco do pedido, nunca baixar. hpp attest create ... --deliberation GATE-RECORD --maker-family F é o release gate: uma sessão release-gate cujos fatos se apoiam só em registros hpp.evidence/v1; a attestation grava o veredito mais estrito e o hash do registro, e um painel que não decidiu manda para revisão. lane_board.py select --deliberation DESIGN-RECORD decide uma competição, com o juiz como revisor de registro. Um juiz da família de modelo do maker é recusado em todos.
Medido a partir de um checkout da main: o exemplo de revisão → stop max-rounds, escalate: true; record → veredito high, dissidência do assento c em low, concordância cega 0,33, concordância final 0,67, 591 caracteres de 6000, exit 0; verify → intact, exit 0, e numa cópia com o veredito editado → record_sha256 does not match, exit 2. record sem --judge → a verdict needs the judge's decision record, exit 2. O painel reproduzido como um decisor só na suíte de decisão tipada → 15 sessões, 12 decididas, 3 abstenções, 0 falhas de instrumento, exit 0. As sessões são sintéticas: provam o contrato, não que um painel vence um agente sozinho.
Veja examples/house-session para os papéis que cada tipo de sessão exige e a regra de parada completa.
A atestação amarra uma aprovação a bytes
hpp attest create registra um veredito junto com o hash da spec, o commit-base, um digest do snapshot completo do repositório (rastreado e não rastreado), o maker, o checker e a sessão. Maker e checker têm de ser diferentes, ou o registro é recusado. hpp attest verify re-deriva cada amarração e bloqueia quando qualquer uma se moveu.
python -m hpp attest create --repo . --spec spec.md --output att.json --maker a --checker b --session s1 --verdict approved
python -m hpp attest verify att.json --repo .
Medido num repositório de rascunho: --maker a --checker a → hpp: maker and checker must be different non-empty actors, exit 2. Com atores diferentes o registro sai approved e o verify devolve valid, exit 0; depois de uma linha da spec mudar, o verify devolve blocked com mismatches: spec_sha256, snapshot_digest, exit 2. A atestação exige git.
Renderizado de uma execução real (scripts/render_terminal_svg.py --animate --session attest): num repositório cujo único commit guarda spec.md e app.py, um byte acrescentado ao app.py depois da aprovação faz o verify devolver blocked só com mismatches: snapshot_digest, exit 2, enquanto o base_commit não se move: uma edição não commitada move o snapshot, não o commit.
Assinatura opcional: quem deu o veredito, conferido offline
Novo na 2.10.0
Um auto-hash mostra que um registro mudou; não diz quem o escreveu. Os dois registros podem carregar uma assinatura SSH. attest create ... --sign-key KEY [--signer NAME] assina a atestação — o signatário padrão é o checker — e evidence run ... --sign-key KEY --signer NAME assina o registro de evidência. O harness nunca lê a chave: ele roda ssh-keygen -Y sign, namespace hpp, sobre os bytes canônicos do registro — a própria assinatura nunca entre eles — e guarda um bloco signature — scheme (sshsig), namespace, signer, o sha256 dos bytes assinados e a signature no formato de texto do OpenSSH. Sem ssh-keygen no PATH, o comando recusa (exit 2) e nada é escrito.
attest verify e evidence verify reportam a assinatura no seu próprio bloco signature. Sem --allowed-signers ela é not-signed ou not-verified (presente, nunca afirmada), e o veredito é decidido exatamente como antes. Com --allowed-signers FILE — o arquivo allowed-signers do ssh-keygen em que quem verifica confia — ela só é verified quando o ssh-keygen -Y verify aceita a assinatura exatamente destes bytes por uma chave que o arquivo lista para o signatário nomeado; qualquer outra coisa é refused, e o registro fica blocked, exit 2: sem assinatura, assinado por outra chave, ou alterado depois de assinado, por mais coerente que o hash tenha sido recalculado.
python -m hpp attest create --repo . --spec spec.md --output att.json --maker a --checker b --session s1 --verdict approved --sign-key ~/.ssh/id_ed25519
python -m hpp attest verify att.json --repo . --allowed-signers allowed_signers
python -m hpp evidence run --id smoke --artifact out.txt --sign-key ~/.ssh/id_ed25519 --signer b -- python check.py
python -m hpp evidence verify <record_path> --allowed-signers allowed_signers
Medido na árvore-fonte da 2.10.1 num repositório de rascunho, com uma chave ed25519 e um arquivo allowed-signers que a lista para o checker, a atestação assinada: verify sem o arquivo → valid, assinatura not-verified, exit 0; com o arquivo → valid, assinatura verified, exit 0; com um arquivo que lista outra chave → blocked, mismatches: signature, exit 2. Uma atestação sem assinatura verificada com o arquivo → blocked, the record is not signed, exit 2. evidence run --sign-key sem --signer → hpp: signing needs a signer: pass --signer with the principal your allowed-signers file lists, exit 2; com ele → passed, e o evidence verify responde not-verified, verified e blocked exatamente como a atestação.
Veja a matriz de prova e o contrato do benchmark.