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.
A condição em materialegislativa_detail.html exigia numero_protocolo
para mostrar o bloco com botão Assinar PDF Digitalmente, Ver PDF
Assinado, Todos em PDF e Todos em ZIP. Como o template de listagem
(materialegislativa_filter.html) já mostra a tag Pendente de
Assinatura baseado apenas em texto_original (sem exigir protocolo),
havia inconsistência: a matéria aparecia como pendente na lista mas
ao abrir o detalhe o autor não tinha botão para assinar.
Remove a exigência de numero_protocolo da condição externa. Os botões
internos preservam suas condições próprias (is_autor/can_edit_materia,
not ja_assinou, etc).