Passo 2 de 2 da conciliacao. O passo 1 (chore/conciliacao-3.1.2-na-dev) trouxe a
3.1.2 inteira para a dev, inclusive as telas "Assinar Despachos em Lote". Este
aplica a delegacao da C3 por cima **sem derrubar essas telas**, que e a decisao
tomada: o individual delega a composicao ao microservico, o lote continua
compondo localmente.
Por que as duas coisas convivem sem contradicao: o /sign/batch nao compoe a
pagina — seus parametros valem para o lote inteiro, e a composicao e por
documento (a URL de verificacao carrega o pk, o codigo sai do hash daquele PDF).
Mas ele **carimba** um PDF ja composto e posicionado. Entao o lote compoe cada
documento aqui e manda coordenadas explicitas. O artefato sai igual: a grade e o
algoritmo do codigo (sha256[:16].upper(), congelado) sao os mesmos dos dois lados.
Como foi feito, e por que assim: em vez de resolver o merge das duas linhagens no
braco (21 blocos de conflito, 708 linhas), o delta C2→C3 do views_assinatura foi
aplicado sobre a base da 3.1.2 — que e literalmente "portar a C3 para cima do que
existe". Sobraram 3 conflitos, todos de uma linha ou duas:
- assinatura de _assinar_pdf_com_pagina_auth: mantidos os parametros de A3
(assinatura_a3_bytes, cert_chain_bytes) que so a 3.1.2 tinha, e acrescentado o
hash_doc que a C3 usa no corpo — sem ele o codigo ja emitido nao voltaria ao
microservico da 2a assinatura em diante;
- imports do PyPDF4: mantida a uniao (PdfFileWriter e base64 seguem em uso no
caminho A3);
- bloco de nome/cargo: fica o da C3 (_nome_e_cargo_do_assinante, fonte unica),
preservando o tipo_cert_display que o resto da funcao usa.
_carregar_certificado nao voltou de proposito: a C3 ja o havia removido em
178865f5 e ele nao tem chamador na 3.1.2 — conferido no repositorio inteiro.
assinar_pdf_lote_via_api volta ao cliente, agora com chamador de verdade e com a
condicao de validade no docstring: so aceita PDF ja composto e posicionado. O
docstring do modulo passa a explicar as duas composicoes que convivem e quando
usar cada uma, em vez de dizer que lote nao serve.
Uma melhoria junto: as duas telas de lote duplicavam a composicao inline (ler
dimensoes, gerar codigo, montar URL, anexar pagina). Passam a chamar
_compor_pagina_auth_localmente, o mesmo helper do caminho individual local.
Eram tres copias da mesma composicao; agora e uma.
Verificado:
- nenhum marcador de conflito; sapl/materia compila;
- todas as funcoes da 3.1.2 seguem no arquivo (exceto a morta acima) e todas as
oito novas da C3 tambem;
- todo nome importado de views_assinatura e do cliente em urls.py e views.py
existe;
- analise estatica de nome livre nas duas telas de lote e no modulo inteiro: nada
fora de escopo depois do port.
Nao testado com o microservico ao lado: o teste ponta a ponta da C3 (primeira
assinatura anexa a pagina, segunda cai no bloco 1, mesmo retangulo nos dois
backends) precisa ser refeito aqui, agora incluindo uma rodada pelas telas de
lote. E o que falta antes do merge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Passo 1 de 2 da conciliacao das linhas do SAPL. A dev vinha da integracao
(hub, inventario, anexos, polls) e a 3.1.2 vinha do produto (assinatura, GED,
proposicao, Franco da Rocha). Nenhuma das duas continha a outra: a dev estava
33 commits a frente e 20 atras.
Traz os 20 commits que faltavam. Como a sp_importante_virou_urgente esta
inteiramente contida na 3.1.2 (zero commits exclusivos), ela vem junto e nao
precisa de merge proprio. As branches feature/add-agents-tracking e
feat/integracao-hub-app foram conferidas arquivo a arquivo e estao superadas:
o que adicionam (OnlyOffice, templates de documento, AGENTS.md, autor_nome no
poll) ja esta na dev por outro caminho.
O unico conflito real foi o docs/LOCALHOST_SETUP.md — dois roteiros escritos em
paralelo para a mesma tarefa. Resolvido a favor do roteiro da dev (mais novo,
validado em 13/08, com a arvore de decisao sobre migration), com tres ajustes:
- a nota sobre DEBUG foi corrigida. Ela dizia "DJANGO_DEBUG, nao DEBUG"; depois
deste merge o settings.py aceita as duas (linha 38, vindo da 3.1.2). Manter a
nota antiga seria documentar o codigo errado a partir de agora.
- "Problemas comuns" da 3.1.2 foi incorporado (alcance do banco remoto,
collectstatic com Gunicorn).
- as instrucoes que dependiam de docker/docker-compose-local.yml ficaram de
fora, com o motivo registrado no proprio documento: esse arquivo nao existe em
nenhuma das duas linhas.
Verificado no resultado do merge, e nao por suposicao:
- as quatro funcionalidades que so existiam na 3.1.2 chegaram inteiras — telas
de "Assinar Despachos em Lote" (docacessorio_assinar_lote, materia_assinar_lote),
preview PNG da pagina de assinatura (as tres funcoes), remocao de assinatura
(_pode_remover_assinatura) e o helper _carregar_certificado;
- nenhum marcador de conflito sobrou em .py, .md ou .html;
- sapl/materia e sapl/base compilam;
- todo nome importado de views_assinatura em urls.py e views.py existe — que era
o modo de falha mais perigoso aqui (urls.py importa no topo do modulo, entao
um nome faltando derruba o boot);
- a integracao da dev (sapl/integracao_hub) segue no lugar.
O passo 2 e a unificacao da assinatura: aplicar a delegacao da C3 por cima
mantendo as telas de lote com composicao local, decisao registrada em
docs-ia-projects#63.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Correcao da regra juridica (decisao do arquiteto, 20/08/2026): assinatura e
ato pessoal e indelegavel — o certificado ICP e do vereador; o assessor pode
DISPARAR o ato pelo app, mas NUNCA aparece como autor da assinatura. A versao
anterior resolvia o signatario por 'primeiro operador por id', o que gravaria
a assessora no lugar do vereador (no dado real de Franco, o autor CESINHA tem
operadores {cesinha, Juciana} e so cesinha e o titular).
Vinculo estrutural do titular = o Votante do Parlamentar que o Autor
representa (parlamentares_votante.user). Confirmado no banco: CESINHA
(parlamentar 2) tem votante {cesinha}, isolando o titular da assessora — o
Votante e o vinculo confiavel, casar username com o nome do autor seria
coincidencia fragil.
Duas camadas distintas:
- signed_by / assinado_por / nome = SEMPRE o vereador titular (autoria
juridica); resolvido por resolver_titular() e sua inversa no poll de
concluidas (signed_by -> autor via Votante, nao mais 'primeiro por id');
- operado_por = quem disparou o ato (rastro operacional, auditoria interna) —
novo campo em AssinaturaRecebida + chave no assinatura_info.
Titular indeterminavel (multi-operador sem Votante ou com Votante ambiguo) =
recusa 422 com mensagem clara, em vez de adivinhar — falha visivel e melhor
que atribuir a autoria errada.
NOTA PARA O APP: o evento DocumentoAssinado (origem amu) ainda nao carrega a
identidade do assessor logado; ate o app enviar 'operado_por', o rastro recai
sobre o proprio titular. Campo ja preparado no SAPL para quando o app enviar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gerada por makemigrations no container padrao de teste — duas tabelas novas
no app isolado, nenhuma alteracao em tabela do core.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cobre as regras que sustentam o contrato: retificacao zera o processo de
assinatura; pendencia por autor nao some para o coautor apos a primeira
assinatura; poll so devolve materia com PDF-alvo; POST idempotente por
chave e 409 em hash divergente. Usernames explicitos e curtos nos fixtures
(o auditing do SAPL grava em varchar(100) dentro do atomic).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A pendencia viaja como as outras fontes — poll, nao push (refinamento §3):
- assinaturas-pendentes: keyset por id da materia, SO materia com PDF-alvo
materializado (§5.1 — DOCX nao convertido nao sai do SAPL). Pendencia e
POR AUTOR (§2): autor da autoria cujo usuario (OperadorAutor) nao esta em
assinatura_info.signed_by — a primeira assinatura nao some com a
pendencia dos coautores.
- assinaturas-concluidas: cursor composto assinado_em|id (mesmo keyset das
fontes por data — empate de timestamp nao trava cursor), com hash do
assinado calculado do arquivo e autor_id resolvido de signed_by via
OperadorAutor (e por ele que o consumidor fecha a pendencia).
- GET documentos-assinatura/<id>/alvo|assinado: serve os bytes ao hub
(token + pode_integrar) — o Authorization nunca vaza ao consumidor final.
- POST assinaturas: idempotente por chave (padrao EventoRecebido), confere
o hash do alvo ANTES de gravar (divergencia = 409, retificacao no meio do
caminho) e grava pdf_assinado + assinatura_info no MESMO formato da
sprint — para a tela do SAPL, indistinguivel de assinatura local.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Management command para cron na VPS (refinamento §5.1): a conversao
DOCX->PDF acontece UMA vez, no SAPL (dono do OnlyOffice), fora do caminho
quente das requisicoes. Reusa _gerar_pdf_da_materia da sprint em vez de
duplicar — PDF copia os bytes, DOCX converte.
- varre materias protocoladas com texto_original; sem alvo gera, com alvo
em dia pula (idempotente), hash_origem divergente e RETIFICACAO
- retificacao ZERA o processo de assinatura (decisao do arquiteto,
19/08/2026): regenera o alvo e limpa pdf_assinado, assinatura_info,
assinado_em, assinado_por e codigo_autenticacao — mesmo efeito da rotina
remover-assinatura. Assinar texto retificado e impossivel por construcao.
- falha de conversao de uma materia loga e segue: OnlyOffice fora do ar
nao trava o ciclo, a pendencia aparece no ciclo seguinte
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
O invariante do documento unico (refinamento §5): o PDF que o SAPL assina,
o que o app exibe e o que aparece assinado sao o MESMO binario. Hoje o fork
viola isso por construcao — o PDF nasce no momento da assinatura, e dois
momentos de geracao podem produzir dois binarios.
- DocumentoParaAssinatura: o alvo persistido, OneToOne com a materia, com
sha256 do PDF e sha256 do texto_original usado na geracao (hash_origem) —
e ele que detecta retificacao (§5.1). Vive no app isolado, nao em
MateriaLegislativa: custo zero de rebase do fork, mesmo racional do
AnexoProposicao.
- AssinaturaRecebida: dedupe do POST de assinaturas, padrao do
EventoRecebido; guarda o hash respondido para a reentrega devolver a
mesma resposta, e marca a origem da gravacao (anti-eco §5.1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Correcao de semantica (decisao do arquiteto, 17/08/2026): o que o vereador anexa
no app e evidencia da demanda — foto da rua, video da indicacao — nao o
documento legislativo. O codigo gravava o anexo como texto_original, o que faria
um video virar "texto" da proposicao.
- AnexoProposicao (app isolado, custo zero de rebase): arquivo + nome + mime +
tamanho + sha256 DOS BYTES RECEBIDOS — o SAPL declara o que guardou, fechando
a cadeia app -> hub -> legislativo. So gravacao por ora; consulta vem depois.
- limite de 1 arquivo removido (premissa errada); foto e video convivem
- texto_original volta a ser exclusivo do fluxo proprio do SAPL (tipo_texto '')
De quebra, conserta flakiness pre-existente dos testes: o baker criava usuario
com username de 150 chars e o auditing do SAPL grava em varchar(100) — o INSERT
do AuditLog estourava DENTRO do atomic e envenenava a transacao inteira.
Username explicito nos fixtures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
O hub nao guarda referencia por tramitacao — ele guarda a referencia da MATERIA.
Devolvendo so o id, o hub comparava id de tramitacao com id de materia e contava
o acervo inteiro como lacuna: em 14/08 foram 500 "ausentes" que eram historico
que a v1 decidiu nao integrar.
A materia e o que diz se aquela tramitacao pertence a algo que o hub deveria ter
integrado. Sem ela a conferencia nao tem como distinguir lacuna de fora-de-escopo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implementa o lado SAPL do refinamento da reconciliacao.
INVENTARIO — GET /api/integracao/reconciliacao/?id_gt=&tramitacao_id_gt=
devolve apenas IDS de proposicoes e tramitacoes acima do corte, com a flag
`truncado` avisando quando a faixa encheu. E lista de conferencia, nao
segunda via dos dados: o hub compara com o que conhece e descobre o que
ficou de fora. Corte por ID porque Proposicao NAO tem campo de criacao — as
datas que existem sao de estado (envio, recebimento, devolucao) e nenhuma
responde "quando apareceu". Mesmo filtro dos polls (cancelado=False), senao
a reconciliacao apontaria canceladas como ausentes para sempre.
KEYSET (data, id) nos polls por data — corrige o travamento do refinamento
§1.1: com cursor so de data e >=, uma pagina inteira de registros com o
MESMO timestamp devolvia sempre o mesmo cursor, ele nao avancava e a fonte
relia a mesma pagina para sempre, sem erro e sem nunca progredir. Agora
id_gt desempata dentro do instante.
TESTES: 9 novos (inventario + o cenario exato do travamento). Corrigidos
tambem tres defeitos das fixtures existentes, que nunca haviam rodado:
Token.objects.create colidia com o token que sapl/api/signals.py:8 cria no
post_save; TipoProposicao exige content_type; e Tramitacao sem status
quebrava a asercao. Resultado: 25 de 29 passam.
Os 4 que faltam sao de recepcao e falham por problema interno do
sapl.rules em banco de teste novo (ContentType matching query does not
exist), NAO por este diff — na versao da dev, sem estas mudancas, o mesmo
arquivo da 10 erros. O caminho de recepcao esta provado em producao
(proposicao 1071, criada pela ida em 14/08).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dois achados do teste manual de integracao (13/08, ambiente real):
1. select_related('materia_gerada') estourava FieldError — o campo NAO
existe no modelo: o bloco `materia_gerada = models.ForeignKey(...)` do
materia/models.py esta DENTRO de uma docstring (codigo morto). O vinculo
real e a generic FK conteudo_gerado_related (content_type + object_id),
que aponta para MateriaLegislativa OU DocumentoAcessorio — so a materia
interessa ao contrato. Sem isso, ProtocoloGerado nunca sairia.
2. O commit do autor_nome (ebf0fac) ficou fora do merge do PR #4 e o campo
sumiu do poll. Sem ele o acervo perde o nome do autor nao mapeado, que e
a regra do refinamento do historico §3.2. Reaplicado.
Verificado no ambiente real: poll devolve as proposicoes do acervo com
autor_nome e rascunho corretos.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
O ProposicaoCadastrada/ProtocoloGerado do contrato pedem descricao (e sigla,
na materia) do tipo; id sozinho obrigaria o hub a segunda consulta.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Componente A da spec (POST /api/integracao/proposicoes/): TokenAuthentication
com permissao propria, criacao pelo ProposicaoForm com autor fixado na
instancia antes do save (a numeracao usa a sequencia do autor), proposicao
nasce rascunho, dedupe por EventoRecebido (reentrega devolve 200 com a
proposicao existente), throttle auto-contido.
Componente B pelo fallback nomeado na spec §4: o get_queryset do
_ProposicaoViewSet nao expoe rascunho ao usuario de integracao, entao os
cinco polls (cadastradas/enviadas/recebidas/devolvidas por cursor proprio e
tramitacoes insert-only) vivem no app isolado, com ordenacao estavel
(data, id) para o cursor do hub.
Toque no core: duas linhas (INSTALLED_APPS e include no urls) — custo de
rebase contra o Interlegis proximo de zero, principio 1 da spec.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Evita depender de alguém lembrar de marcar a caixa em Tabelas Auxiliares
depois do deploy: a migration de dados marca dispensa_protocolo no tipo de
proposição "Ofício" que já existir no banco.
A comparação normaliza acentos, espaços e caixa, porque a descrição é digitada
em cada Casa e aparece como "Ofício", "OFICIO" ou "oficio". Casas que não têm
esse tipo cadastrado não são afetadas.
A migration é reversível: o reverse desmarca os mesmos tipos.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ofícios são documentos de uso próprio do gabinete do vereador e não precisam
de validação da Casa, mas hoje caem na fila de Proposições Pendentes como
qualquer outro tipo — a fila é definida apenas por data_envio/data_recebimento/
data_devolucao, sem considerar o tipo. O protocolo vinha devolvendo esses
ofícios na mão.
Adiciona TipoProposicao.dispensa_protocolo, configurável em Tabelas Auxiliares
-> Tipo de Proposição, em vez de fixar a regra no tipo "Ofício". Para os tipos
marcados:
- As ações 'send' e 'send_setor' são recusadas, então data_envio nunca é
gravada e a proposição não entra na fila do Protocolo nem do Setor
Legislativo.
- O botão "Enviar Proposição" dá lugar a um selo explicativo no detalhe.
- A listagem do vereador ganha o status "Documento de Gabinete" e o filtro
correspondente; esses documentos saem da contagem de "em elaboração".
A visibilidade já é resolvida pelo container_field='autor__operadores' do
ProposicaoCrud: a proposição só é acessível ao gabinete do autor.
Proposições já existentes não são tocadas — as que estão enviadas, devolvidas
ou já incorporadas seguem com o status atual.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Customização para o Procurador Jurídico da Câmara de Franco da Rocha,
identificado pelo grupo "Operador de Norma Jurídica" (SGVP_GROUP_NORMA):
- Tela inicial exibe somente o card "Matérias Legislativas".
- Documento Acessório já abre com Tipo="Parecer Jurídico", Nome="Aprovado"
e Data=data atual. Os campos seguem editáveis.
Adiciona o helper sapl.rules.is_procurador_juridico, usado pelas duas
funcionalidades para manter o critério de grupo em um único lugar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md/AGENTS.md agora apontam para knowledge/AGENTS.md com regras
anti-alucinação explícitas (vale para Claude Code, Codex e Cursor):
não inventar regra/contrato, dizer quando não está documentado, citar o
doc governante, knowledge/ prevalece sobre doc local divergente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Documentacao oficial em knowledge/ (submodule de docs-ia-projects, branch
main). Path knowledge/ e nao docs/ porque tres repositorios ja tem docs/
proprio ainda nao migrado.
Clone: git clone --recurse-submodules
Atualizar: git -C knowledge pull origin main && git add knowledge
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adiciona pagina onde o Presidente da Mesa Diretora pode visualizar e
assinar em lote, com um unico certificado A1, TODOS os documentos
acessorios do tipo "Despacho" ainda pendentes de assinatura digital
(filtro frouxo: tipo__descricao__icontains "despacho").
Reuso integral do backend ja existente:
- docacessorio_assinar_lote() em views_assinatura.py ja aceita PKs de
documentos de multiplas materias num unico POST.
- Modal de 4 passos (selecao -> certificado -> previa -> progresso ->
resumo) extraido de documentoacessorio_list.html para partial
reusavel e incluido nos dois templates (refactor mecanico, HTML/JS
byte-a-byte identico).
Acesso: restrito ao grupo SGVP_GROUP_PRESIDENTE_MESA (ou superuser
para depuracao); demais usuarios recebem 403. Atalhos pra tela
aparecem so quando usuario esta no grupo:
- "Pesquisar Materia Legislativa" (botao na linha de filtro de
assinatura)
- "Materias Pendentes de Assinatura" (botao no bloco de acoes)
Arquivos:
- sapl/materia/views.py: ListView DespachosPendentesLoteView nova +
flag is_presidente_mesa nos contextos das views existentes.
- sapl/materia/urls.py: rota /materia/despachos-pendentes-lote.
- sapl/templates/materia/assinatura_doc_lote_modal.html (novo): partial
com modal completo.
- sapl/templates/materia/despachos_pendentes_lote_list.html (novo):
tela do presidente.
- sapl/templates/materia/documentoacessorio_list.html: substitui modal
embedded por include do partial.
- sapl/templates/materia/materialegislativa_filter.html: atalho.
- sapl/templates/materia/materias_pendentes_assinatura_list.html: atalho.