Curadoria do acervo¶
Esta página é para o arquivista que trabalha na SPA de curadoria. Ela diz, tela por tela, que decisão se toma ali, o que o clique escreve e se dá para desfazer — e nomeia o que não tem volta nenhuma.
A SPA é servida pela própria API (uma origem, sem CORS), e toda tela lê e escreve pelo cliente gerado
a partir do contrato OpenAPI. As telas são declaradas em apps/curator/src/router.tsx, e como cada
uma se chama em apps/curator/src/lib/screens.ts: um registro por tela guarda o rótulo que o menu,
o cartão de configurações e o título da própria página leem, então os três não podem discordar
(Um nome por tela, um verbo por ação, abaixo). O menu tem 16 entradas: as telas que ninguém
abre no meio da catalogação — o plano de arranjo, os catálogos, o painel de workers, o ledger de
execuções, os diagnósticos e as contas — são cartões de /configuracoes, não itens de menu
(Configurações, abaixo). Uma rota também não vem do menu: a ficha da descrição,
/acervo/$descriptionId, é alcançada pela lista e pela árvore. O roteador, por isso, declara 26
telas. O próprio menu pode ser recolhido a ícones, o que é uma escolha de apresentação e não de
permissão — O menu se recolhe a ícones, abaixo.
Vale ler duas páginas junto com esta: Modelo de dados para o que os ledgers e as tabelas significam, e Instalação e implantação para o esquema, as contas e a implantação em volta das telas.
A única regra¶
Toda escrita ou tem desfazer, ou avisa que não tem. O merge, a purga de stopwords e o expurgo de NER guardam um ledger; a edição da descrição guarda o antes e o depois. Quando uma tela não tem nem uma coisa nem outra, esta página diz em negrito.
O trabalho de leitura não declara permissão: ler é o que uma sessão autenticada é. As telas que decidem carregam a área em que escrevem, e a API recusa com 403 e uma frase quando a conta não a carrega.
O que não tem volta¶
Estas escritas não têm desfazer
As operações abaixo apagam ou dissolvem uma linha que nada mais reconstrói. A exclusão pede que você digite o código de referência. Leia a confirmação.
-
Excluir uma descrição —
DELETE /api/v1/documents/{description_id}. Definitivo. A API grava antes um retrato ISAD(G) da linha inteira no ledger de exclusões, e esse retrato é uma trilha, não uma lixeira:/acervo/excluidaso mostra e nada o restaura. O ledger de revisões não sobrevive (ele cai junto com a descrição), e é por isso que o retrato existe. Uma descrição com filhos não pode ser excluída — o serviço recusa e diz quantos estão abaixo; exclua ou mova os filhos primeiro. A tela pede que o arquivista digite o código de referência (ou o identificador, quando não há código) antes de o botão liberar. -
Excluir ou purgar entidades sem vínculo —
DELETE /api/v1/taxonomy/entities/{entity_id}ePOST /api/v1/taxonomy/entities/orphans/purge. A tela só oferece a exclusão numa linha cuja contagem de uso é zero, e a purga remove toda entidade que nenhuma descrição carrega, então nenhum vínculo se perde — mas a linha vai embora e não há ledger que a restaure.
O lote que tem desfazer¶
Materializar o arranjo a partir do plano (POST /api/v1/hierarchy/materialisation/apply) é
reversível, e o desfazer está na tela. Ele só roda depois de uma prévia
(POST /api/v1/hierarchy/materialisation/preview, calculada pelo mesmo planejador) e grava uma
entrada de ledger por execução (GET /api/v1/hierarchy/materialisation/log). O desfazer é
DELETE /api/v1/hierarchy/materialisation/log/{materialisation_id}, e ele restaura exatamente o que
a execução mudou:
- toda descrição que ele moveu volta à unidade superior e ao nível que tinha antes;
- os nós que a execução criou são apagados — o que só é possível restaurando primeiro, porque a
auto-referência é
RESTRICT; - os planos que apontavam para um nó apagado voltam a "não materializado", para a próxima aplicação não tentar ligar descrições a uma linha que não existe mais.
O desfazer recusa quando uma execução posterior ligou descrições que o ledger não conhece, porque
reverter ali desligaria trabalho que nunca fez parte desta decisão. Reverter uma execução concluída é
um fato novo e registrado: a entrada guarda undone_at e quem desfez.
Revisão, difusão e o que a IA não pode reescrever¶
O ciclo de revisão¶
review_status é o ciclo da validação humana sobre o trabalho da IA. Os cinco valores são
PENDING_AI, AI_APPROVED, NEEDS_REVIEW, HUMAN_APPROVED e REJECTED. HUMAN_APPROVED é o
escudo: o predicado de escrita do pipeline de IA o exclui, então nenhum worker de relatório reescreve
em silêncio uma descrição que o arquivista validou. Editar qualquer campo ISAD(G), ou as tags e
entidades da descrição, grava HUMAN_APPROVED por PATCH /api/v1/documents/{description_id} (ou
pelas rotas de vínculo), registra o antes e o depois em archive_document_revisions e tira a
descrição da fila da IA.
As duas exceções documentadas¶
worker_embeddingde propósito não usa o predicado de escrita da IA. O embedding é um índice derivado do texto, não conteúdo arquivístico: uma descriçãoHUMAN_APPROVEDcujo texto mudou precisa ser re-embedada, ou a busca semântica serviria um vetor velho. Ele escreve só a colunaembeddinge o próprio carimbo (o MD5 do texto efetivo), e pula descriçõesREJECTED.- Publicar não é travar.
is_publishedé escolhido pelo mesmoPATCHde qualquer campo ISAD(G), e não congela a descrição contra a IA. O predicado de escrita lê apenasreview_status. Reusar o status de revisão para a difusão teria feito uma correção de digitação equivaler a publicar, e teria travado toda descrição publicada contra o melhoramento da IA.
Os outros workers de IA ficam de fora de uma descrição HUMAN_APPROVED; o que uma pessoa decidiu não
é reescrito pelo pipeline.
Difusão não é revisão¶
Revisão é uma afirmação sobre o registro; publicação é uma decisão sobre o que expor. is_published
é coluna própria, editada pela caixa "Publicar na difusão" na aba Descrição da ficha, e o predicado
público é exatamente is_published. Nada é publicado por padrão, então a superfície de difusão nasce
vazia.
Publicar expõe a descrição pela superfície aberta, sem autenticação:
GET /api/v1/public/documentseGET /api/v1/public/documents/{description_id}.
Duas regras valem ali. O filtro published_only é definido no servidor e não é parâmetro do
cliente, então nenhuma query string pode pedir um registro não publicado; e um identificador não
publicado responde 404, nunca 403, porque um status distinto confirmaria que a descrição existe.
A projeção pública é mais estreita que a do curador, campo a campo. Ela carrega os campos ISAD(G)
original_title, final_title, document_date, reference_code, level, scope_content,
language_name e producers; o enriquecimento typology, as tags (nome), as entidades (nome e
tipo) e as categorias de assunto (nome e contagem); a miniatura — ainda a referência s3://
guardada, porque a rota pública que serve os bytes é a #46 (ADR 0017); o ramo (ancestors) e o
children_count. Ela deliberadamente não carrega o processo de curadoria nem a narrativa não
publicada: review_status, is_anomaly, anomaly_reasons, archivist_notes, provenance,
suggested_final_title, admin_bio_history, admin_archival_history e access_conditions. O
conjunto de facetas públicas também é mais estreito — anomaly_reason não é publicado.
Quem pode decidir o quê¶
A autorização é um nível declarado por operação, imposto por um único guard. As quatro permissões de escrita são áreas, não botões:
| Permissão | A área |
|---|---|
CURATE |
o registro e seus assuntos: os campos ISAD(G), vínculos, tags, entidades, conflitos e as taxonomias que os movem |
CATALOGUE |
os catálogos fechados: níveis, tipologias, vocabulário do acervo, categorias de assunto, regras de limpeza e trechos |
OPERATE |
os workers de IA: rodar um, mudar o padrão persistido dele e o painel de saúde |
ADMIN |
contas e sessões |
| Papel | Carrega | Lê |
|---|---|---|
VIEWER |
nada | tudo o que uma sessão autenticada alcança |
CURATOR |
CURATE, CATALOGUE |
o mesmo, mais as telas que decidem o registro e os catálogos |
ADMIN |
CURATE, CATALOGUE, OPERATE, ADMIN |
tudo, incluindo os workers e as contas |
Ler não é permissão: é o que uma sessão autenticada é, e uma leitura pura não declara nada.
Entrar, e esquecer a senha¶
O formulário de entrada não é uma rota: ele é desenhado no lugar da casca, então um link direto
sobrevive a ele — quem abre /acervo/lista sem sessão entra e cai em /acervo/lista. Uma senha
errada e um endereço que não existe respondem a mesma frase, de propósito: o formulário não é uma
enumeração de quem tem conta.
Não há recuperação por e-mail nesta instalação, e a tela não finge o contrário. Abaixo do botão, Esqueci minha senha abre uma explicação curta do caminho que existe: um administrador redefine a senha na tela de contas (Usuários, abaixo), a senha é temporária e a conta a troca no primeiro acesso, a redefinição encerra todas as sessões dessa conta e destrava um bloqueio — e, se o último administrador ativo for justamente quem não consegue entrar, o terminal do servidor é a volta (Operação é dona desse procedimento). Nada na tela promete uma mensagem, porque nenhuma mensagem é enviada.
A senha temporária é credencial para a troca e para nada mais. Enquanto a conta ainda a carrega,
a API recusa toda operação de /api/v1 — leituras inclusive — com PASSWORD_CHANGE_REQUIRED, e a
casca desenha a troca de senha forçada no lugar de toda tela. As três operações em que esse
formulário se apoia continuam alcançáveis: ler a sessão (GET /auth/me), encerrá-la
(POST /auth/logout) e trocar a senha (POST /auth/password, que limpa a marca). A recusa não
depende da tela: um script com a senha que foi impressa num terminal, ou digitada por um
administrador, recebe a mesma frase e nenhum acesso ao acervo. A troca abre o resto da API na mesma
sessão.
A interface fala duas línguas¶
A interface de curadoria é escrita em português e inglês, e a escolha é da conta, não da
instalação: Automático, Português e English ficam no rodapé da conta, no menu. Uma conta nova
começa em Automático, que é guardado como "nenhuma escolha" e segue o navegador — pt* abre em
português, en* em inglês e todo o resto em português, a língua da instalação. Depois de entrar, a
escolha da conta vence: uma estação compartilhada não pode entregar a um arquivista a língua do
anterior.
A língua não é uma preferência de cliente pintada sobre um servidor em português: toda requisição
leva Accept-Language, e a API compõe a sua message — a confirmação de uma escrita e a recusa de
outra — na língua pedida (ADR 0020 §4). A troca não recarrega a página; o shell re-renderiza com o
catálogo novo, e datas, números e plurais a acompanham via Intl.
O que não muda entre as duas línguas: os caminhos continuam em português (/acervo/lista), o que a
239 acompanha, e o conteúdo do registro nunca é traduzido.¶
O menu esconde; nunca concede¶
A casca filtra as entradas pela área em que a tela escreve
(apps/curator/src/lib/permissions.ts, um espelho do mapa de papéis do servidor), e um grupo vazio
não é renderizado. As telas que só leem não carregam permissão e ficam visíveis para todo papel:
Início, a lista do acervo, a árvore, o histórico de exclusões e o diagnóstico do arranjo. Um VIEWER
vê, portanto, o grupo "Acervo" e a entrada "Início", que fica sem cabeçalho — todos os outros grupos
existem para decidir, e ficam escondidos.
A mesma regra governa os cartões de /configuracoes, e é o catálogo de cartões
(apps/curator/src/lib/settings.ts) que a página e a entrada de menu leem: um cartão fica escondido
quando a conta não carrega a área em que a tela escreve, uma aba com todos os cartões escondidos não é
renderizada, e a entrada fica no menu enquanto ao menos um cartão for alcançável. Um VIEWER não
tem Configurações nenhuma, e uma URL direta até ela chega a uma página que diz por que está vazia em
vez de fingir que quebrou.
O espelho é de mão única de propósito. Ele encurta o menu; nunca faz a API aceitar uma requisição. Uma URL direta para uma tela em que a conta não pode trabalhar ainda chega à API, e a API responde 403 com uma frase — essa é a verdade, e a entrada escondida é uma cortesia.
Um nome por tela, um verbo por ação¶
A interface cresceu tela por tela, e o vocabulário cresceu com ela: o menu dizia Tags enquanto a página que ele abria dizia "Vocabulário de tags", o cartão de configurações dizia Usuários enquanto a página dizia "Contas", e duas telas diferentes se chamavam Diagnóstico. Dois arquivos agora são donos dessas palavras.
apps/curator/src/lib/screens.ts guarda um registro por tela — a rota, o rótulo e a dica que diz
o que ela decide. O rótulo é a mesma string nos três lugares que discordavam: a entrada de menu, o
cartão de configurações e o <h1> da própria página, que o PageHeader resolve a partir do id de
tela que a rota declara. Uma rota, por isso, não consegue passar um título próprio; a ficha da
descrição é a única exceção, e é o catálogo que a marca como a tela cujo título é o do próprio
registro.
Abaixo do título, o cabeçalho tem os mesmos quatro espaços em toda tela: o rótulo, uma linha dizendo o que a tela decide, o estado do que foi lido — uma contagem, um filtro, um total — e as ações. A contagem disputava o mesmo espaço com a linha explicativa, e é por isso que umas telas se explicavam e outras só relatavam totais; enquanto a leitura está em curso, o espaço do estado mostra um esqueleto da própria altura, para o cabeçalho não pular quando os números chegam.
apps/curator/src/lib/copy.ts guarda o nome do produto — reexportado de lib/attribution.ts, que é
dono dele para o aviso de licença — e um verbo por ação, com a palavra do meio da execução. Os
verbos que esta página usa são os desse arquivo:
| Ação | A palavra na tela |
|---|---|
| escrever uma linha editada | Salvar |
| trazer uma linha à existência | Criar (o gatilho ao lado diz + Nova…) |
| tirar um termo de uma lista — um veto, uma exclusão | Remover |
| destruir um registro, sempre atrás de uma prévia ou de uma confirmação digitada | Excluir |
a chave is_active de uma linha de catálogo — um nível, uma tipologia, uma categoria, uma regra de limpeza, um termo do acervo |
Aposentar / Reativar |
| desativar uma conta | Desativar / Reativar |
| disparar uma execução de worker | Rodar agora |
| unificar duas entradas de vocabulário | Mesclar |
| confirmar um plano calculado | Aplicar |
| calcular o que uma escrita destrutiva faria, sem escrever | Conferir impacto |
Aposentar e Desativar são duas palavras de propósito: uma linha de catálogo e uma conta saem do
sistema de maneiras diferentes, e uma palavra só para as duas diria que não. Banir um termo mantém o
verbo próprio (Banir / Desbanir), porque é outra marca e não exclui nada.
A tela tem a altura de uma janela¶
A casca tem exatamente uma viewport e a página nunca rola: o que rola é a coluna de conteúdo, então
a rail — e a entrada de Configurações no pé dela — fica onde está numa tela longa. O cabeçalho da
própria tela é fixo dentro dessa coluna, e é isso que mantém o título, o estado e as ações ao
alcance: "Propor níveis", "Rodar agora" e "Excluir órfãs" saíam da janela junto com o conteúdo. Medido
a 1440x900 na lista de entidades: o documento tem 900px numa janela de 900px, o conteúdo tem 3299px
numa coluna de 867px e o menu tem 734px numa caixa de 734px.
A coluna não tem largura máxima: ela preenche o que a janela der, então a mesma tela tem a mesma
largura em qualquer monitor. Antes disso cada tela carregava o próprio teto e eles tinham divergido —
max-w-5xl na maioria, max-w-4xl em cinco, max-w-6xl em uma — então duas telas lado a lado num
monitor largo paravam de crescer em pontos diferentes. Quem mantém o próprio teto é o conteúdo: um
parágrafo mantém a medida legível, um campo de texto mantém a largura de campo.
O menu se dobra numa seção aberta, mais a seção em que o arquivista está — essa fica aberta por
construção, porque uma entrada ativa escondida dentro de um grupo recolhido é um menu que esconde onde
você está. Clicar num título abre o grupo e fecha o que foi aberto à mão; navegar fecha esse, então o
menu nunca escorrega para três. O título não carrega ícone: o glifo é da entrada. Início fica no topo
sem título, como a entrada do pé, porque uma seção que junta uma linha é uma linha de cromo.
A identidade está na tela¶
A rail é o navy da identidade (#14202B), e a logomarca está nela nos dois estados: expandida é o
lockup completo — a ficha com a aba terracota e Scrinalia em PT Serif — e recolhida é só a marca
recortada. Recolhida, essa marca é também o controle que expande a rail: a marca é o que se clica
para trazer as palavras de volta, e é isso que faz valer a pena mantê-la em 64px.
Um filete de 2px de terracota fecha o cabeçalho da tela, e os dois cartões que vêm antes de uma sessão — o formulário de entrada e a configuração inicial — carregam o mesmo motivo no topo, onde antes estava o nome em texto simples. As duas fichas não são iguais: a configuração inicial centraliza o lockup sob a tagline, e o formulário de entrada põe um lockup maior à direita e não carrega tagline nenhuma — ali a ficha é só a marca.
O accent continua o azul que era, e a terracota é cor de marca e não de interação: a terracota é
oklch(… 31) e a cor de perigo é oklch(… 25), seis graus de distância, então um botão terracota e
"Excluir" seriam da mesma cor. O navy, por outro lado, compartilha a matiz do accent de propósito — e
é por isso que a rail e os botões parecem uma coisa só.
O menu se recolhe a ícones¶
Um controle no cabeçalho do próprio menu o dobra numa coluna de ícones de 64px e o devolve, e o
navegador lembra a escolha: a largura de uma coluna da tela é estado de apresentação, então mora no
localStorage e não na conta — a API não tem coluna para isso.
Recolhido, cada entrada mantém o ícone; o rótulo continua sendo o nome acessível do link e o title
carrega rótulo e dica, de modo que a dica fica escondida e nunca cortada. A entrada ativa mantém a cor
de destaque, os títulos de grupo viram filetes e o acordeão fica desligado — um título sem rótulo numa
coluna de 64px é um clique que não diz nada —, então as dezesseis entradas aparecem de uma vez. O
rodapé de sessão vira a inicial da conta mais dois botões de ícone.
O nome da atribuição sai do menu, mas não da página: a atribuição é renderizada no pé de toda tela
(AttributionFooter, ADR 0006), incluindo as três que vêm antes de uma sessão — a configuração inicial,
o formulário de entrada e a troca de senha forçada — e ela termina com a versão da instalação, sozinha à
direita. Os ícones vêm do lucide-react, um glifo por entrada.
Curadoria¶
| Tela | Decisão | Reversível? |
|---|---|---|
Início — / |
qual fila pendente abrir | sim (só leitura) |
Início — /¶
A lista de trabalho. Ela responde "o que precisa de mim hoje?" com um cartão por fila e a rota que
resolve a pendência, já filtrada. É uma leitura de predicados existentes
(GET /api/v1/curation/inbox): nada é escrito. Um cartão que a casca não reconhece como abrível
mostra "tela pendente" em vez de apontar para o vazio.
Acervo¶
| Tela | Decisão | Reversível? |
|---|---|---|
Nova Descrição — /acervo/nova |
digitar uma descrição ou importar um CSV (curador) | o registro entra no pipeline como PENDING_AI; a aprovação (ou rejeição) acontece no dossiê, e só ali |
Lista e busca — /acervo/lista |
como recortar o acervo | sim (só leitura) |
Árvore — /acervo/arvore |
onde a descrição fica no arranjo | sim, movendo; remover o nó significa excluir a descrição |
Histórico de exclusões — /acervo/excluidas |
nenhuma (o registro do que saiu) | sim (só leitura) |
Descrição — /acervo/$descriptionId |
o registro, seus assuntos e seu lugar | em geral sim; a exclusão não |
Nova Descrição — /acervo/nova¶
A porta de entrada do acervo: uma descrição digitada inteira, ou um CSV importado. As duas são abas
na URL — Digitar (o padrão) e Importar CSV (?modo=csv) — porque são duas formas de entrada
do mesmo trabalho, não dois trabalhos: as duas põem o registro no staging e enfileiram a mesma
transfer que a coleta usa, e a descrição do Acervo nasce PENDING_AI. A tela não é uma
aprovação: o dossiê é onde o registro é revisto e aprovado depois que a IA rodar. É uma escrita
CURATE, e é a primeira entrada do menu Acervo.
Digitar é o registro ISAD(G) inteiro: trinta controles nas sete áreas da norma, o nível e o idioma
como seletores, a data como texto com a gramática do idioma, o controle do objeto digital desabilitado
até a #198. O formulário gera um id manual-<uuid>, então um reenvio é o mesmo registro; um
description_id duplicado é recusado antes da gravação. Só o título é obrigatório — uma descrição pode
nascer incompleta e ser completada no dossiê.
Pontos de acesso são chips sobre o catálogo de tags, não uma string separada por vírgulas. A busca
enquanto digita escolhe uma grafia existente; digitar um termo e escolher Adicionar mantém a grafia
do arquivista. Um resumo abaixo do campo diz o que a transfer vai fazer com a string — criar uma
grafia nova, ligá-la a um sinônimo ou descartá-la por ser um termo bloqueado, ter menos de três ou mais
de cem caracteres — e aponta as entradas cuja grafia guardada difere da digitada. O resumo é POST
/api/v1/taxonomy/tags/preview, uma leitura: uma grafia desconhecida é NEW e só é criada pela
transfer no arquivamento. Nada é removido nem reescrito pela prévia: remover um termo é decisão do
arquivista. O campo continua opcional, e a string que ele envia é a mesma da coluna do CSV e da chave
mapeada do scraper.
Produtor(es) sugere nomes a partir do catálogo de entidades. Digitar no campo consulta GET
/api/v1/taxonomy/entities?term= — a mesma busca que o dossiê usa para associar uma entidade — e
escolher uma sugestão copia o nome para o texto, nunca o entity_id. A cópia é literal (o catálogo
guarda os nomes das entidades em minúsculas) e substitui só o trecho entre dois separadores, então as
vírgulas, os espaços e os outros nomes do arquivista continuam como estavam; o cursor fica depois do
nome copiado e o campo segue editável. É uma cópia e não um vínculo: uma mesclagem ou renomeação
posterior da entidade não alcança a descrição, e a ajuda embaixo do campo diz isso. A lista não é
filtrada por tipo de entidade, porque o NER classifica o mesmo órgão como ORG, LOC ou PER e
qualquer filtro esconderia um produtor real; o tipo vai como dica da sugestão. Um nome que o catálogo
não tem continua digitável, e o campo continua sendo a string que o contrato carrega. A relação, com os
filtros que ela destravaria, está na #242.
Importar CSV é a origem csv (#191, ADR 0019). O arquivo é interpretado no navegador (vírgula
ou ponto e vírgula, campos entre aspas, CRLF, UTF-8 com queda para Windows-1252); a tela mostra os
cabeçalhos detectados, uma prévia e as conferências locais, e envia só uma lista limitada de linhas
como JSON — nada vai para o storage de objetos e não existe rota multipart, então uma instalação cujo
storage não está configurado importa um CSV do mesmo jeito. O mapa é o da API (GET
/api/v1/ingestion/csv/spec), então um cabeçalho não pode ser aceito pela tela e recusado pelo
servidor; a API revalida cada linha com o contrato de staging, e a resposta por linha diz aceita,
duplicada (o description_id já existe) ou inválida, com o campo e a frase. Uma importação
parcial é normal. As linhas aceitas entram no staging como origin_code='csv' e uma transfer é
enfileirada depois delas.
O caminho antigo /sistema/ingestao/nova redireciona para cá, então um link compartilhado ainda abre
o formulário digitado.
Lista e busca — /acervo/lista¶
A busca facetada do acervo: o termo, uma de três ordens, tipologia, categoria de assunto, tipo de entidade, nível, um ramo do arranjo e um intervalo de datas. A URL é o estado, então uma lista filtrada é compartilhável e o botão voltar funciona. Nada é escrito.
As três ordens respondem a perguntas diferentes, e a tela diz qual é qual:
| Modo | Pelo que ordena | Quando escolher |
|---|---|---|
| Lexical (padrão) | ts_rank sobre o search_vector gerado, com reforço quando uma tag ou entidade casa |
O arquivista sabe o termo que o título usa |
| Semântica | Similaridade de cosseno entre o termo e o vetor da descrição | O arquivista sabe o assunto, mas não o vocabulário do fundo |
| Híbrida | As duas fundidas por Reciprocal Rank Fusion (RRF_K = 60, janela RRF_DEPTH = 100) |
O modo que não exige escolher, ao custo de carregar o modelo |
Os números por trás dessas frases ficam em docs/log.md, ao lado da bancada que os produziu
(testing/evaluation/retrieval_modes.py), e não nesta página: uma página que guarda um número que
deve mudar mente na próxima execução. O que a tela diz é que a evidência é um conjunto rotulado
pequeno e que serve de proxy, e que o modo lexical é preferível quando o termo é conhecido — o único
caso em que os dois braços diferem por margem larga.
Árvore — /acervo/arvore¶
O arranjo como navegação. Selecionar um nó mostra o ramo, os filhos e as descrições dentro dele, tudo
em leitura. A única escrita da tela é "Criar nó" (POST /api/v1/hierarchy/nodes, CURATE): declara
um Fundo, uma Seção ou uma Série que a origem não entregou e que o fatiador, por isso, não tem como
propor. A descrição nova nasce HUMAN_APPROVED, e a escada é validada contra o pai escolhido pela
mesma regra de um mover. O lugar dela é reversível movendo a descrição; remover o nó significa
excluir a descrição, o que é definitivo.
Arranjo não é assunto
Este é o eixo objetivo — onde a descrição fica na proveniência. Os selos de assunto vivem na lista e na ficha; misturá-los aqui ensinaria o arquivista a "consertar" um assunto arrastando uma descrição para outro ramo.
Histórico de exclusões — /acervo/excluidas¶
A trilha das exclusões, em leitura (GET /api/v1/documents/deletions), com busca e paginação no
servidor porque a trilha não tem teto. Ela não é uma lixeira: cada entrada é o retrato ISAD(G)
que a API gravou antes de excluir, e nada aqui restaura coisa alguma. Responde "fui eu que excluí
isto?" — código, título, nível, quem excluiu, quando, quantos filhos havia, e o retrato inteiro sob
demanda.
Descrição — /acervo/$descriptionId¶
A ficha, com quatro abas na URL. O título sugerido aparece ao lado do campo que ele propõe e
nunca é o valor guardado: ele é derivado na leitura a partir dos trechos de escopo TITLE, e só o
arquivista escreve final_title.
Descrição¶
O primeiro cartão é a miniatura, e ela vem da API, não do navegador: a leitura anuncia
thumbnail_url e a SPA a liga a um <img>, então o cliente nunca monta um caminho e nunca fala com o
bucket (ADR 0017). Os bytes são GET /api/v1/documents/{description_id}/thumbnail (AUTHENTICATED);
a rota responde 404 quando a origem não tinha imagem ou o objeto não está mais no bucket, e 503
quando o próprio storage está indisponível. Quatro das 101 descrições do acervo de referência não
têm imagem de origem, então "Sem imagem" é um estado normal, e uma URI guardada cujo objeto sumiu
mostra "Imagem indisponível" — nunca o ícone de imagem quebrada. A lista do acervo mostra a mesma
miniatura a 56 px, que é como se reconhece uma fotografia sem abrir o dossiê.
A decisão é o próprio registro, agrupado nas áreas da norma — 3.1 Identidade, 3.2 Contexto,
3.3 Conteúdo e estrutura, 3.4 Acesso e uso, 3.5 Materiais relacionados, 3.6 Notas e
3.7 Controle da descrição — mais o nível, a tipologia e o selo de difusão, com uma nota do porquê.
O registro ISAD(G) inteiro é editável: o acervo guarda todo campo que o transform de staging
interpretou, não o subconjunto que a transferência copiava. A escrita é PATCH
/api/v1/documents/{description_id} (CURATE). Ela registra o antes e o depois do que mudou em
archive_document_revisions, grava a autoria a partir da sessão e define HUMAN_APPROVED — o que tira
a descrição da fila da IA. Reversível: outra edição registra uma nova revisão; o histórico é só de
acréscimo e o valor anterior fica visível na aba Histórico. Publicar e retirar usam o mesmo comando e
são igualmente reversíveis.
Dois valores são exibidos e não editados, porque são evidência e não descrição: date_expression (a
data como a fonte escreveu — Década de 1980 ao lado do 1980-01-01 interpretado, que é o que o
parser conseguiu dela) e original_url (a página de onde o registro foi lido). O nível e a tipologia
são seletores sobre os seus catálogos; language_name é um seletor sobre GET /api/v1/languages, um
catálogo declarado em código porque um idioma não carrega regra que a instituição edite. O valor
guardado é o nome do catálogo: uma variante declarada (Português/Brasil, Português Brasil) é
dobrada na transferência, então a coleção inteira lê uma grafia — e o seletor mantém um valor que o
catálogo não conhece, marcado como tal, para que abrir o dossiê nunca apague um idioma que o
catálogo nunca viu. O pai declarado aparece em 3.1 mesmo enquanto está pendente (BR PRPMC —
aguardando), porque a origem pode entregar o filho antes do pai; o vínculo em si se faz na aba
Arranjo.
O cartão Objeto digital aparece com o controle desabilitado: o acervo guarda a descrição, não
o arquivo, e o objeto digital é a #198. A frase que diz isso é definida uma vez
(apps/curator/src/lib/copy.ts) e o formulário de entrada manual mostra a mesma.
Assuntos¶
A decisão é quais tags e entidades esta descrição carrega e em que categoria cada tag fica. As escritas
são POST/DELETE /api/v1/documents/{description_id}/tags[/{tag_id}] e o par de entidades
(CURATE), mais PATCH /api/v1/taxonomy/tags/{tag_id} para a categoria. Associar e desassociar são
reversíveis: a revisão guarda a lista inteira de nomes de cada lado, e chamar a mesma rota duas
vezes não escreve revisão nenhuma.
Cada entidade mostra a origem do seu vínculo: IA quando o extrator a escreveu, confirmada
quando uma pessoa a associou ou confirmou, e não rastreada para um vínculo escrito antes de a coluna
existir. Uma origem desconhecida nunca é exibida como se fosse do modelo. Confirmar associa uma
entidade que o extrator já propôs sem duplicá-la: o vínculo fica onde está e sua origem vira HUMAN —
o veredito positivo que o arquivista não conseguia registrar antes (#135). Confirmar um vínculo que já
existia não muda nome nenhum, então não escreve revisão: o registro da confirmação é a origem do
vínculo, e a descrição vira HUMAN_APPROVED, o que a tira da fila da IA.
A categoria é uma decisão sobre o vocabulário, não sobre esta descrição
Mudar a categoria de uma tag move a tag em todas as descrições que a carregam, e a tela diz isso ao lado do seletor. Escolher "sem categoria" devolve a tag ao classificador de assunto, que vai tentar arquivá-la de novo na próxima execução.
Arranjo¶
A decisão é onde esta descrição fica e em que nível. A escrita é
POST /api/v1/hierarchy/nodes/{description_id}/move (CURATE), e a rota declara para onde o nó vai:
new_parent_id=null significa "para a raiz", então não há como mudar só o nível — a tela sempre
envia a unidade superior que está mostrando. A API valida antes de escrever (um Item não pode ter
filhos, um Dossiê não pode ficar sem pai, mover para dentro da própria subárvore é recusado), e mover
uma unidade leva a subárvore inteira: o caminho é reescrito em uma instrução. A mudança fica no
histórico desta descrição e é reversível movendo a unidade de volta.
Histórico¶
As revisões desta descrição, em leitura, campo a campo com o valor antigo e o novo. No fim do histórico fica a exclusão, longe do cabeçalho de propósito — é a única ação que remove um registro para sempre, e a confirmação (digitar o código de referência) é o que separa "excluir" de "excluir esta descrição". Veja o que não tem volta acima.
Arranjo¶
O plano é alcançado por /configuracoes — decidir os degraus é trabalho de montagem, não curadoria do
dia a dia — e o diagnóstico ficou no menu, sob "Acervo", ao lado da árvore que ele lê. Sem o plano, o
grupo "Arranjo" teria uma linha só, então ele deixou de existir.
| Tela | Decisão | Reversível? |
|---|---|---|
Plano de arranjo — /arranjo/plano |
aprovar ou rejeitar cada degrau proposto, e materializar a árvore | sim — a decisão pode ser reaberta, e a materialização tem desfazer |
Diagnóstico do arranjo — /arranjo/diagnostico |
nenhuma (a evidência; o conserto é em outra tela) | sim (só leitura) |
Plano de arranjo — /arranjo/plano¶
A máquina lê os códigos de referência e propõe os degraus; a decisão é do arquivista. Em cada degrau a evidência vem primeiro — a contagem de descrições, os níveis declarados, as amostras — e o formulário por último:
- Propor níveis —
POST /api/v1/hierarchy/plans/suggest(CURATE). Escreve as perguntas. É idempotente por código e nunca reescreve um degrau que saiu deSUGGESTED, então a mesma pergunta não é feita de novo. - Aprovar um degrau —
PATCH /api/v1/hierarchy/plans/{plan_id}(CURATE) comAPPROVED, o nível escolhido, um título opcional, um código opcional em "fundir em" e uma nota. Aprovar exige o nível: é a decisão que o código não sabe tomar. - Rejeitar — a mesma rota com
REJECTED. Um degrau rejeitado deixa suas descrições órfãs de propósito: elas caem no degrau aprovado mais próximo acima. - Reabrir decisão — a mesma rota com
SUGGESTED. Reversível: a decisão volta para a fila e a próxima proposta pode atualizar a evidência dela. A decisão nunca é sobrescrita por uma proposta nova.
Os degraus aparecem como a árvore que os códigos implicam, não como uma lista recuada. A caixa à
esquerda de uma linha recolhe aquele ramo — a linha passa a dizer quantos degraus e quantas decisões
ele esconde —, enquanto o chevron do cartão abre o corpo da decisão; recolher o ramo não reinicia os
cartões que estão dentro dele: expandir devolve cada um como estava. Os filtros de status, aviso e
código mantêm os pais e o ramo de cada
degrau que casa, e um pai de um degrau que casa não pode ser recolhido. Recolher os decididos
fecha os ramos que não têm nenhum degrau ainda SUGGESTED. A URL guarda só o topo de cada ramo
recolhido, então o colega abre a mesma vista.
O painel lateral materializa a árvore: ele sempre oferece "Conferir impacto" primeiro, e o botão de aplicar fica desabilitado até a prévia existir. Só os degraus aprovados são materializados. A prévia e a aplicação compartilham um planejador, então o número aprovado é o número escrito. O que o desfazer restaura está descrito em o lote que tem desfazer.
Diagnóstico do arranjo — /arranjo/diagnostico¶
O diagnóstico estrutural, uma seção por problema, cada uma com sua contagem e sua evidência (uma
seção com zero continua visível, porque saber que a checagem rodou importa). Ele não oferece
correção silenciosa: cada linha leva à tela onde o conserto é uma decisão registrada — o plano, a
árvore ou a aba de arranjo da descrição. As contagens se sobrepõem de propósito (um Dossiê na raiz é
ao mesmo tempo ORPHAN e DOSSIER_WITHOUT_PARENT), por isso não há um total geral.
Catálogos¶
Os três são alcançados por /configuracoes, sob Arranjo e catálogos: são o vocabulário com que o
trabalho é escrito, não o trabalho — e é por isso que o menu não os carrega mais.
Os dois catálogos fechados que o arquivista mantém e o vocabulário do acervo. Nenhum deles apaga: uma
linha se aposenta com is_active=false, porque as chaves estrangeiras são SET NULL e remover uma
linha desclassificaria toda descrição que aponta para ela, além de excluir o registro de que ela
existiu.
| Tela | Decisão | Reversível? |
|---|---|---|
Níveis de descrição — /arranjo/niveis |
a escada contra a qual o arranjo é escrito | sim (aposentar e reativar) |
Tipologias — /arranjo/tipologias |
a forma diplomática que o classificador propõe, e as descrições que ele recusou | sim (aposentar e reativar; arquivar uma descrição é uma revisão do documento) |
Vocabulário do acervo — /vocabulario |
o que os códigos e termos deste acervo significam | sim (aposentar e reativar) |
Níveis de descrição — /arranjo/niveis¶
O catálogo de níveis. Criar um degrau é POST /api/v1/hierarchy/levels e editar é
PATCH /api/v1/hierarchy/levels/{level_id} (CATALOGUE): nome, descrição, grafias aceitas, se exige
unidade superior e se pode ter filhos. Duas regras que a tela declara em vez de contornar:
- não há exclusão.
is_active=falseé o caminho, e o peso de um degrau aposentado continua visível; - o ordinal não é editável. Mudá-lo renumeraria a árvore contra a qual o passado foi decidido; um ordinal errado é um degrau novo, não um renome.
A assimetria é deliberada: um nível desconhecido que chega da origem entra como não classificado e a descrição segue, enquanto o arquivista não pode gravar um nível que não existe.
Tipologias — /arranjo/tipologias¶
O catálogo das tipologias documentais — a forma diplomática (ata, ofício, planta), nem arranjo nem
assunto. POST /api/v1/typologies e PATCH /api/v1/typologies/{typology_id} (CATALOGUE) mantêm o
nome e a descrição de contexto. O classificador recebe só o nome: acrescentar o contexto faz o
modelo perder o vínculo e colapsar o acervo numa única tipologia (medido), então o contexto é
documentação para quem lê. Aposentar uma tipologia é a única alavanca que alcança o modelo — ela
deixa de ser proposta sem desclassificar nenhuma descrição — e é por isso que o peso das aposentadas
continua na tela.
A tela também lista as descrições que o classificador recusou. GET /api/v1/typologies/refusals
(AUTHENTICATED) devolve uma página das descrições cuja melhor pontuação ficou abaixo do limiar, da
mais próxima do limiar para a mais distante, com o palpite da execução e a pontuação que ela viu. A
lista lê o mesmo predicado da contagem do classificador e deixa de fora a descrição que uma pessoa já
arquivou, mesmo que a linha da fila continue no banco. Escolher uma tipologia ali é
PATCH /api/v1/documents/{id} (CURATE): a escrita do dossiê, que grava a revisão e marca a
descrição como HUMAN_APPROVED. Dá para voltar atrás pelo dossiê, e o antes/depois fica na trilha.
Uma lista vazia quer dizer que nenhuma recusa está aberta, não que o catálogo cobre o acervo — uma
descrição que nenhuma execução alcançou não tem linha, e arquivar uma a tira da lista sem apagar a
evidência. Se nenhuma tipologia serve, crie ou reative uma no catálogo da mesma tela; ainda não existe
veredito durável para "esta descrição não tem tipologia".
Vocabulário do acervo — /vocabulario¶
O que este acervo declara, em oposição ao que a língua ou o software fixam. Duas listas:
- nomes de arranjo —
POST/PATCH /api/v1/vocabulary/arrangement-terms(CATALOGUE): um token (SMU) ou o código inteiro (BR PRADAP) mapeado para o nome que a proposta de arranjo sugere; o código inteiro tem precedência sobre o último token; - termos do acervo —
POST/PATCH /api/v1/vocabulary/collection-terms(CATALOGUE): as grafias que o acervo carrega e que não são assunto, tipadas para o guarda reconhecer um lugar ou um nome de pessoa. O que o nome é fica no eixo de entidades (LOC,PER), não num estado da tag: o eixo de assunto simplesmente para de adivinhar sobre ele.
O que é propriedade da língua portuguesa — rua, não identificado, 303 anos, um ano solto — fica no perfil de idioma e não aparece aqui: mudá-lo seria mudar o significado que o software dá à palavra. Aposentar um termo não o exclui, e um termo aposentado volta a ser tratado como assunto na próxima execução do classificador.
Assuntos¶
| Tela | Decisão | Reversível? |
|---|---|---|
Tags — /assuntos/tags |
o peso, as duplicatas, a fila de merges e a decisão de assunto de cada tag | o merge sim (ledger); a decisão de assunto sim (dá para voltar atrás) |
Categorias — /assuntos/categorias |
as categorias de assunto que o classificador lê, e o que está dentro de cada uma | sim (aposentar e reativar) |
Descobrir Categorias — /assuntos/descobrir |
se um tema proposto merece uma categoria | sim (a proposta não escreve nada) |
Não é assunto — /assuntos/excecoes |
quais termos saem do eixo de assunto e têm a grafia banida de voltar a ser tag | sim (o banimento não apaga nada, e o desfazer levanta o banimento que esta decisão plantou) |
Grafias bloqueadas — /assuntos/grafias |
quais grafias a extração não pode recriar, e a exclusão das tags que já existem com elas | o bloqueio sim (desbanir); a exclusão sim (uma linha de ledger por tag, restaurando a tag, os vínculos e as grafias — nunca o bloqueio) |
Tags — /assuntos/tags¶
Uma rota, quatro perguntas, uma conversa só: o arquivista vê que alvenarias pesa pouco, encontra a
quase-duplicata dela e decide se a absorve. Três das abas são o jeito de olhar; a quarta,
Classificar, é onde a decisão de assunto se toma, e é onde os dois cartões de assunto do Início
caem. A aba ativa está na URL.
As grafias bloqueadas eram a quinta aba, e são Grafias bloqueadas — /assuntos/grafias desde a
232: tela própria, ao lado de Não é assunto, porque a descrição desta página nunca dizia que a aba¶
guardava a única escrita que apaga uma tag. ?aba=stopwords continua respondendo com a tela nova.
Relevância¶
Só leitura: GET /api/v1/taxonomy/tags/relevance/{method}. A contagem mostra o que domina o acervo;
o TF-IDF mostra o que é específico, punindo o que aparece em toda parte. As duas listas respondem
perguntas diferentes de propósito.
Similaridade¶
Pares por similaridade de trigrama, mostrados crus, com a pontuação e os dois identificadores. A
leitura é GET /api/v1/taxonomy/tags/similar. A similaridade não diz qual grafia é a boa:
'alameda cabral' e 'al. alameda cabral' têm 1,000.
A decisão é unificar: POST /api/v1/taxonomy/tags/merge (CURATE), depois de uma prévia
(POST /api/v1/taxonomy/tags/merge/preview, uma leitura que calcula o mesmo plano que a escrita
executa). O painel abre sempre com o impacto na frente: documentos atualizados, vínculos reescritos,
tags absorvidas, grafias registradas e reapontadas, e o aviso que precisa ser lido antes do clique —
category_would_be_lost, quando a canônica não tem categoria e uma absorvida tem, o que faria o merge
excluir uma classificação de assunto.
O merge é reversível: a escrita é registrada por tag absorvida no ledger de merges antes de
qualquer coisa mudar, e o desfazer (DELETE /api/v1/taxonomy/tags/merge-log/{merge_id}, CURATE)
restaura a linha, os vínculos, a classificação e o estado das grafias. O desfazer apaga apenas os
vínculos que o merge criou, então não solta um documento que já carregava a canônica. As linhas também
podem ser marcadas entre si e unificadas como um conjunto, que é como um merge atravessa vários pares
(carlos de carvalho aparece em mais de uma linha).
Uma seleção marcada tem um segundo destino, e ele é o outro tipo de escrita: Banir N marcadas
registra as grafias em POST /api/v1/taxonomy/tags/stopwords (CURATE), no eixo escolhido ao lado do
botão. Banir não apaga nada — as tags continuam no acervo, vinculadas e alcançáveis pela busca, e o
passo que as exclui é a exclusão das tags em Grafias bloqueadas — /assuntos/grafias, atrás da
própria prévia e com um ledger que restaura cada tag. A marcação sobrevive ao banimento: a mesclagem
para a qual ela foi feita continua possível.
Propostas de merge¶
Os conjuntos da máquina, com a decisão separada da escrita:
- Propor clusters —
POST /api/v1/taxonomy/tags/merge-proposals/suggest(CURATE) registra a pergunta. Um veredito já tomado nunca é sobrescrito. - Conferir impacto —
POST /api/v1/taxonomy/tags/merge/previewcom o id da proposta, o mesmo planejador. - Aprovar / Rejeitar —
PATCH /api/v1/taxonomy/tags/merge-proposals/{proposal_id}(CURATE). Aprovar registra a intenção; não absorve nada. O status vai deSUGGESTEDparaAPPROVED/REJECTEDe depoisAPPLIED. - Aplicar em lote —
POST /api/v1/taxonomy/tags/merge/batch(CURATE) absorve os conjuntos selecionados e gravaAPPLIEDno mesmo savepoint. A tela recusa um lote acima de 200 clusters (a API também recusa). Um conjunto cujos membros já saíram é reportado emskipped, nunca como falha. - Editar um conjunto — marca para fora as tags que não pertencem, escolhe a canônica e aplica a seleção revisada pelo mesmo planejador; a proposta da máquina é então fechada como rejeitada, porque a pergunta foi respondida de outro jeito.
O ledger abaixo lista só merges aplicados, cada um com o nome absorvido, a canônica, a contagem de documentos, a autoria e o horário. O desfazer dele é a restauração exata descrita acima, e o "desfazer" da tela é a mesma rota. Um merge feito aqui também passa por uma proposta, então o ledger é a história da escrita e o status é a da decisão.
Classificar¶
Onde o assunto de uma tag se decide, e a aba que os cartões do Início abrem. A leitura é
GET /api/v1/taxonomy/tags/catalog (AUTHENTICATED), filtrada por subject_state e opcionalmente
por macro_category_id, paginada e ordenada por peso — a tag que mais descrições carrega é a decisão
que vale mais primeiro. Ela existe porque o type-ahead não responde a essa pergunta: ele exige um
termo e devolve um topo por peso, então o cartão que prometia "76 tags sem categoria de assunto" não
tinha leitura que marcasse quais linhas eram.
O estado é uma partição, e ALL é a ausência de filtro, não um estado:
subject_state |
O que significa |
|---|---|
PENDING |
Sem categoria e sem veredito: ninguém decidiu. É o que o cartão do Início conta |
NOT_SUBJECT |
Uma pessoa declarou que a tag não é assunto; o classificador para de adivinhar sobre ela e a grafia fica banida de voltar a ser tag |
LOW_CONFIDENCE |
O classificador rodou, não teve confiança para arquivar a tag e a deixou sem decisão |
A linha carrega a evidência (peso e a confiança do classificador) e exatamente um de dois destinos,
por PATCH /api/v1/taxonomy/tags/{tag_id} (CURATE):
- uma categoria — a categoria em que a tag entra. A decisão é global: muda o selo de todas as descrições que carregam a tag, e a tela diz isso antes do clique;
- não é assunto —
macro_category_id=null, que é a mesma escrita que a tela Não é assunto faz. A tag continua no acervo e alcançável pela busca, e só a coluna seria uma decisão que a próxima execução sobrescreve; por isso o termo entra emdomain_subject_exclusionse a grafia é banida no escopoTAG, para a próxima ingestão não recriá-la. O caminho de volta éDELETE /api/v1/taxonomy/tags/subject-exclusions, e ele levanta só o banimento que esta decisão plantou.
Um produtor, um lugar ou uma pessoa não é um terceiro destino. O que o nome é pertence ao eixo
de entidades, onde ele carrega um tipo (LOC, PER, ORG) que a busca e a projeção pública leem;
uma grafia que é tag e entidade ao mesmo tempo se resolve em Conflitos — /entidades/conflitos,
que bane a grafia perdedora e guarda um ledger. Declarar o termo não-assunto é o que o tira da fila de
assunto; a entidade é onde ele mantém o significado. A medição por trás do estado removido continua
valendo — alfa alcança 2.376 descrições e cidade 1.865, e forçá-las para dentro de uma categoria é
causa medida do defeito de classificação —, mas a resposta agora é a entidade e a exclusão, não um
quarto estado da tag.
As mesmas duas ações valem para uma seleção, uma requisição por tag, e o lote informa quantas tags
escreveu e para onde. Nada nesta aba exclui: banir uma grafia e excluir as tags ficam em Grafias
bloqueadas — /assuntos/grafias, cada um no seu passo, e a exclusão guarda um ledger com o desfazer
de cada tag.
Grafias bloqueadas — /assuntos/grafias¶
As grafias que um bloqueio mantém fora do vocabulário, e a única escrita da taxonomia que apaga uma tag. Era a quarta aba de Tags até a #232, e virou tela própria porque a página em que ela morava descrevia relevância, duplicatas e mesclagem, e nunca dizia que a aba guardava a única escrita destrutiva do eixo de assunto.
Não é uma segunda vista de Não é assunto, e a tela diz isso na primeira linha: o veredito é sobre o
que o termo significa — e é o que faz a classificação de assunto parar de adivinhar —, enquanto o
bloqueio é sobre a grafia — e é o que impede a próxima ingestão de recriar a tag. Confirmar um
veredito planta um bloqueio no escopo TAG, e é por isso que a mesma grafia aparece nas duas listas: a
lista diz de onde veio cada bloqueio, e o que veio de um veredito leva de volta à tela que decidiu. As
duas telas se apontam, e o gate testing/unit/curator/test_curator_copy.py falha se um dos links ou o
redirecionamento de ?aba=stopwords desaparecer.
A tela separa três coisas:
- Banir —
POST /api/v1/taxonomy/tags/stopwords(CURATE) registra que um termo não vale a pena num eixo:TAG(o eixo de assunto),ENTITY(NER) ouALL. Uma palavra tem exatamente um escopo — banir de novo move — e banir não apaga nada. Um banimento digitado aqui é gravado comsource=HUMAN; o que a tela Não é assunto planta éEXCLUSION, e o que uma resolução de conflito planta éCONFLICT. A lista mostra o escopo em todo termo e a origem como selo quando ela não éHUMAN— um termo digitado aqui é o padrão, e o selo marca os que chegaram como consequência de outra decisão. - Desbanir —
DELETE /api/v1/taxonomy/tags/stopwords(CURATE). Reversível. - Casamento —
PATCH /api/v1/taxonomy/tags/stopwords(CURATE) muda como um banimento existente casa, sem banir de novo: banir de novo sobrescreveria a origem que um conflito ou a tela Não é assunto registraram.TOKENbloqueia a palavra e qualquer nome que a contenha como palavra inteira — o span fundidoIPTU do Exemplo— eEXACTbloqueia só a grafia, que é o que uma palavra genérica que inicia um nome próprio precisa:alamedabloqueiaalameda, nuncaAlameda Cabral. Só os eixosENTITY/ALLleem isso. Toda entrada dedomain_ner_exclusionscasa pela grafia exata pelo mesmo motivo: o conflito era sobrecasa, não sobrecasa da memória(#160). - Excluir as tags —
POST /api/v1/taxonomy/tags/stopwords/purge(CURATE), depois dePOST /api/v1/taxonomy/tags/stopwords/purge/preview. Esta é a escrita que apaga as tags. A prévia lista as tags que morreriam, com a contagem de documentos, a categoria e de onde veio o banimento (HUMAN,CONFLICTouEXCLUSION), para um termo banido com o acervo em mente não se confundir com um digitado à mão; o botão de aplicar fica desabilitado até a prévia existir.
Cada tag apagada é gravada num ledger (archive_stopword_purge_log) antes de sumir: o retrato da
linha, os documentos que a carregavam e as grafias que apontavam para ela. A resposta da exclusão traz
os purge_ids, a tela os mostra em Registro das exclusões, e DELETE
/api/v1/taxonomy/tags/stopwords/purge-log/{purge_id} (CURATE) restaura a tag, os vínculos e as
grafias; um identificador desconhecido é 404 e uma segunda tentativa é 409. O desfazer não
levanta o bloqueio — o termo continua bloqueado, e a próxima exclusão lista a tag restaurada de novo.
O escopo protege o outro eixo
A exclusão lê apenas TAG/ALL. Um termo bloqueado no eixo ENTITY é um veto de NER e nunca
pode fazer a exclusão de assunto apagar uma tag que o curador manteve — os dois são guardados em
lugares diferentes de propósito.
Categorias — /assuntos/categorias¶
As categorias que o classificador de assunto lê. POST /api/v1/taxonomy/macro-categories e
PATCH /api/v1/taxonomy/macro-categories/{category_id} (CATALOGUE) as criam, renomeiam, descrevem,
aposentam e reativam; uma categoria aposentada mantém o peso visível. Duas notas honestas da tela: o
rótulo do classificador é um ajuste do curador, não uma melhoria — medido em 44 tags rotuladas à
mão, uma frase em vez do nome nu leva a 0,000 com 65% das tags numa única categoria — e uma categoria nova
só passa a valer quando o classificador rodar de novo, porque o carimbo do worker é o hash do
conjunto de rótulos, o que devolve as tags à fila sozinho.
Cada cartão abre em duas abas. Descrições lista as descrições que a categoria contém, dez por vez,
pela busca do acervo (GET /api/v1/documents?macro_category_id=, AUTHENTICATED); o selo do cartão
conta descrições distintas pela mesma junção, então o número do cartão e o total da lista são uma
definição contada duas vezes. Uma categoria sem nenhuma diz isso — as aposentadas não são um defeito.
Editar é o formulário: nome, rótulo do classificador e descrição.
O botão da linha abre uma prévia somente leitura de uma descrição — código de referência, título,
data, nível e tipologia, caminho no arranjo, as tags com as suas categorias e a situação de revisão —
sobre a lista, para reconhecer a descrição sem uma ida ao dossiê. O painel não escreve nada: o
Editar dele sai para /acervo/{description_id}?aba=descricao. A categoria aberta, a página dela e
a descrição em prévia vivem na URL, então o botão voltar fecha o painel e a página sobrevive a ele.
Descobrir Categorias — /assuntos/descobrir¶
Roda o motor de agrupamento de verdade (POST /api/v1/taxonomy/tags/suggest-macro, só leitura:
AUTHENTICATED) sobre as tags ou os documentos para achar um tema que o vocabulário ainda não cobre.
É um clique deliberado e não um carregamento de página: o motor leva segundos e pode rodar num
subprocesso. Os clusters são propostas — nada entra no vocabulário antes de o arquivista
cadastrar, o que é POST /api/v1/taxonomy/macro-categories (CATALOGUE). Descartar uma proposta é
local à tela. Um corpus pequeno demais responde com uma sugestão vazia e uma mensagem, não com um
erro.
Lendo os documentos, a tela oferece as colunas que o clustering lê: GET
/api/v1/taxonomy/tags/suggest-macro/columns publica o texto da IA e o subconjunto que a execução lê
quando o corpo não escolhe nenhuma, e a tela desenha um checkbox por coluna a partir dessa leitura —
ela não digita os cinco nomes. final_title está no corpus e fora do padrão: uma coluna pode
entrar no que a IA lê sem mudar em silêncio o que o clustering lê. O corpo só leva
columns_to_extract depois que uma coluna se move, e a rota recusa um nome desconhecido com 400
que o nomeia (antes do #174 o erro de digitação chegava a um getattr e respondia 500), recusa
uma lista vazia, e recusa a lista com source_type=tags, onde ela seria ignorada.
Não é assunto — /assuntos/excecoes¶
A metade curada do "isto não é assunto de jeito nenhum". O guarda determinístico pega o que tem forma
— uma data, um placeholder, um logradouro, um número solto — e, na medição real, pegou 1 de 4 dos
não-assuntos rotulados à mão; o resto é julgamento semântico que nenhuma regra resolve (pessoas,
vista aérea, capanema). A tela mostra os candidatos do guarda com a evidência deles (peso, se o
termo é um lugar, se a mesma grafia também é entidade, a categoria que o bloquearia e se a grafia já
está banida) e oferece registrá-los com source=RULE ou um a um.
Confirmar planeja e aplica numa só chamada: a escrita abaixo consome o mesmo planejador que a rota de
prévia usa, então as duas não podem discordar. A prévia é POST
/api/v1/taxonomy/tags/subject-exclusions/preview (AUTHENTICATED); ela recebe os termos e nada mais,
porque um plano não tem motivo a declarar nem proveniência a informar, e responde applicable e
blocked. O campo Vetar termos a consulta enquanto a digitação assenta (250 ms) e mostra a
recusa antes do clique: um termo cuja tag já está numa categoria é nomeado com a categoria
enquanto o arquivista ainda pode mudar o termo, e a resposta da escrita só confirma. O campo grava
todos os termos que se aplicam, então uma recusa parcial não desabilita o botão — só uma lista em que
todos os termos estão bloqueados desabilita, porque aí a escrita não registraria nada.
A escrita é POST /api/v1/taxonomy/tags/subject-exclusions (CURATE), e ela faz duas coisas de uma
vez: registra o termo em domain_subject_exclusions, para o classificador de assunto parar de
adivinhar sobre ele, e bane a grafia no escopo TAG, para a próxima ingestão não recriar a tag
que acabou de ser decidida. O banimento não apaga nada: a tag continua no acervo, continua
vinculada e continua alcançável pela busca. Removê-la é um ato separado — a exclusão das tags em
Grafias bloqueadas — /assuntos/grafias, atrás da própria prévia e com um ledger que restaura
cada tag — e a prévia diz quais banimentos foram plantados aqui (EXCLUSION) e quais à mão
(HUMAN).
A tela e Grafias bloqueadas são dois objetos, não dois filtros, e cada uma aponta para a outra:
esta decide o que o termo significa e é o que para o classificador; aquela veta a grafia e é o
que impede a extração de recriar a tag. Um veredito planta um bloqueio TAG, então a mesma grafia
aparece nas duas listas — a lista de bloqueios diz qual veio de um veredito e leva de volta para cá.
Duas respostas que a tela precisa mostrar:
- um termo cuja tag já está numa categoria não é aplicado: ele volta em
blockedcom a categoria que precisa ser esvaziada primeiro, porque "em que categoria esta tag está?" é uma pergunta sobre a tag, respondida no catálogo de tags, e uma afirmação sobre um termo não pode desarquivar um veredito humano. O código éSUBJECT_EXCLUSIONS_BLOCKEDquando nada foi aplicado e algo foi recusado,SUBJECT_EXCLUSIONS_PARTIALquando alguns foram aplicados e alguns recusados, ecreated/banned/blockedlevam as três respostas; - a rota deliberadamente não propõe nada para a metade semântica: os sinais disponíveis mentem, e um modelo que não sabe se abster responderia com confiança e errado.
A reversão é DELETE (CURATE, o "restaurar" da tela). Ela responde removed e unbanned, e o
levantamento é exato: só um banimento que esta decisão plantou sai com ela. Uma grafia que uma pessoa
baniu na tela Grafias bloqueadas, ou que uma resolução de conflito plantou antes, sobrevive.
Entidades¶
| Tela | Decisão | Reversível? |
|---|---|---|
Entidades — /entidades/lista |
o tipo de um nome, a grafia de um nome, e se dois nomes são um | o tipo sim; a renomeação sim (uma linha no ledger); o merge sim (uma linha por entidade absorvida) |
Remover de Entidades — /entidades/excecoes |
quais grafias são assunto, não nome próprio, e o expurgo retroativo que as segue | ambos sim: a linha do veto pelo seu próprio DELETE, o expurgo pelo seu ledger (archive_ner_purge_log) |
Conflitos — /entidades/conflitos |
se uma colisão pertence ao eixo de assunto ou aos nomes, um par de cada vez ou uma categoria inteira de uma vez | sim (uma linha no ledger por par, com o desfazer) |
Entidades — /entidades/lista¶
O vocabulário de nomes próprios, por peso ou por similaridade. Cinco decisões:
- Reclassificar o tipo —
PATCH /api/v1/taxonomy/entities/{entity_id}/reclassify(CURATE), só entreORG,PEReLOC. Isso não é renomear um rótulo: o serviço grava também o sinônimo de ancoragem, então o extrator devolve aquela grafia com o tipo novo em toda execução futura. - Renomear —
PATCH /api/v1/taxonomy/entities/{entity_id}(CURATE): corrige a grafia de um nome, e não é um merge. A grafia anterior continua ancorada como sinônimo a menos que o arquivista desliguekeep_old_synonym, que é o que faz uma extração futura da grafia antiga continuar resolvendo para esta entidade — e um erro de digitação é exatamente o caso em que ela não deveria. A escrita deixa uma linhaRENAMEno mesmo ledger que o merge usa, então o "desfazer" da tela restaura o nome e remove a grafia que ele plantou. Uma grafia que outra entidade já carrega é um 409 que nomeia o gêmeo e aponta para o merge, nunca uma sobrescrita silenciosa e nunca um conflito estrutural cru. - Excluir uma entidade —
DELETE /api/v1/taxonomy/entities/{entity_id}(CURATE), oferecido só numa linha que nenhuma descrição carrega, onde excluir não perde vínculo. Sem desfazer. - Purgar órfãs —
POST /api/v1/taxonomy/entities/orphans/purge(CURATE): a mesma decisão em lote. Sem desfazer. - Mesclar —
POST /api/v1/taxonomy/entities/merge(CURATE): as entidades absorvidas deixam de existir, seus vínculos passam para a canônica, suas grafias viram sinônimos, e um nome novo opcional para a canônica faz o antigo virar sinônimo também. A decisão é precedida da prévia (POST /api/v1/taxonomy/entities/merge/preview, uma leitura): ela devolve quantas descrições passam a carregar a canônica, quantos vínculos são reescritos, quais entidades são absorvidas e os avisos a olhar, calculada pelo mesmo planejador que a escrita executa. Entidades não têm catálogo de propostas, mas têm ledger: cada absorvida é registrada antes de qualquer mudança, e o desfazer (DELETE /api/v1/taxonomy/entities/merge-log/{merge_id}, o "desfazer" da tela) restaura a linha, seus vínculos, suas grafias e o nome que a canônica perdeu.GET /api/v1/taxonomy/entities/merge-logé a trilha, buscável pelas duas grafias de cada entrada. A lista de pares é evidência crua, com os dois identificadores, porque duas linhas podem ler nomes quase iguais e a canônica é uma escolha que o arquivista precisa conseguir distinguir.
O merge tem três portas e um único painel atrás de todas: a seleção na Relevância, um par na
Similaridade e o cartão Mesclar pelo nome acima das abas. O cartão existe porque as outras duas
são guiadas por listas que o arquivista não controla — a relevância é ordenada por peso e limitada,
então dois nomes carregados por números muito diferentes de descrições nunca ficam na mesma página, e
a similaridade é trigrama, a única medida que falha no par de que ele tem mais certeza (smu e smcs
secretaria municipal de comunicação social: 68 descrições contra 1, e 0.045 de similaridade). O cartão
recebe os nomes pelo type-ahead que a aba Assuntos do dossiê já usa, então herda o piso de dois
caracteres da API em vez de inventar um; mostra o peso de cada nome, para a canônica ser escolhida com
os números à vista; e a lista é dele — não consome as caixas marcadas na Relevância, e com menos de
dois nomes nada é escrito.
Ler é a outra metade desta tela, e o cartão se abre nela. Um nome se expande nas descrições que o
carregam — a busca do acervo (GET /api/v1/documents) com entity_id, paginada de dez em dez
dentro do cartão, de modo que o selo acima e o total da lista são um número contado duas vezes;
testing/integration/api/controller/test_entity_documents.py falha quando os dois deixam de
concordar. Clicar numa linha abre a prévia somente-leitura (?descricao=) que a tela de categorias já
usa, e nada nela escreve. O "Editar" dela cai na aba Assuntos do dossiê, que é onde as entidades
de uma descrição são adicionadas e removidas. Um cartão fica aberto por vez, e ?aberta=, ?offset=
e ?descricao= carregam esse estado na URL — então o botão voltar fecha o painel e um colega pode
receber a mesma vista exata.
A segunda aba do cartão é a renomeação, e o botão Renomear ao lado do nome é quem a abre. Ele é o recurso da própria linha e nunca mescla: o merge parte de dois ou mais nomes e só renomeia a canônica como efeito colateral.
Remover de Entidades — /entidades/excecoes¶
O veto "esta grafia é assunto, não nome próprio". Vetar não apaga nada. A escrita é POST
/api/v1/taxonomy/entities/ner-exclusions (CURATE), com um motivo guardado para auditoria, e a
reversão é DELETE (o "remover veto" da tela), que reabre o termo para as próximas extrações. Um
comportamento que a tela declara porque não é visível na lista: o bloqueio é por limite de token,
não por nome exato (o spaCy funde tokens vizinhos, então um iptu vetado não barraria "IPTU do
Batel" numa comparação exata).
A metade retroativa é um ato separado, Expurgar do passado:
POST /api/v1/taxonomy/entities/ner-exclusions/purge (CURATE), depois de
POST /api/v1/taxonomy/entities/ner-exclusions/purge/preview. O impacto lista toda entidade já
extraída de uma grafia vetada, com as descrições que a carregam, e a escrita consome o mesmo plano.
Cada entidade apagada entra num ledger (archive_ner_purge_log) antes de sair: o snapshot da
linha, os documentos que a carregavam, as grafias que apontavam para ela e o excluded_term que a
justificou. A resposta carrega os purge_ids, a tela os mostra em Histórico de expurgos, e
DELETE /api/v1/taxonomy/entities/ner-exclusions/purge-log/{purge_id} restaura a entidade, os seus
vínculos e as suas grafias — sem levantar o veto, então o próximo expurgo lista a entidade
restaurada de novo. Use-o para reverter a perda; use "remover veto" para reabrir o termo.
A tela de conflitos resolve o mesmo choque com o mesmo par de escritas, e lá também é reversível na
mesma forma: uma linha de ledger por par (archive_conflict_resolution_log), com a linha perdedora, os
seus vínculos e as suas grafias restauradas pelo desfazer. Chegar à operação por qualquer das telas
restaura as mesmas coisas.
Esta é a outra metade da governança bidirecional: ela registra o curador ou o juiz dizendo "isto pertence ao eixo de assunto", e é guardada separada das stopwords do eixo de assunto de propósito, para um veto aqui nunca fazer a purga de assunto excluir uma tag que o curador manteve.
Conflitos — /entidades/conflitos¶
A mesma grafia nos dois eixos: "isto é assunto ou nome próprio?". Três leituras respondem a três perguntas, e ler só a primeira era o que escondia o trabalho do juiz:
- Pendentes — a varredura ao vivo de trigrama (
GET /api/v1/taxonomy/conflicts/cross-domain), anotada com o veredito do juiz onde existe e filtrada porpair_kind: "grafias diferentes" (uma pergunta de grafia) e "nomes idênticos" (uma pergunta estrutural) não são a mesma lista, e juntas nenhuma das duas fica visível. A mesma leitura aceitaentity_type(ORG/PER/LOC), porque "rua é entidade" é uma decisão sobre uma categoria de colisões e não sobre um par. - Decididos pelo juiz — a fila de revisão (
GET /api/v1/taxonomy/conflicts/judged). Uma auto-resolução apaga a linha perdedora, então a maior parte das decisões do juiz não pode aparecer na varredura ao vivo. - Resoluções — o ledger (
GET /api/v1/taxonomy/conflicts/resolutions), com o desfazer.
A decisão é precedida pela prévia (POST /api/v1/taxonomy/conflicts/resolve/preview, uma leitura):
ela devolve os dois vereditos com o que cada um transfere, apaga e bloqueia, calculados pelo mesmo
planejador que a escrita executa. Os dois vereditos são guardados em lugares diferentes de
propósito:
- a tag vence — os vínculos da entidade passam para a tag, a linha da entidade é apagada e o
termo é registrado em
domain_ner_exclusions, carregando o motivo, a origem e a tag que o justifica; - a entidade vence — os vínculos da tag passam para a entidade, a linha da tag é apagada e o nome
da tag entra nas stopwords de escopo
TAG.
Colapsar os dois deixaria um veto de NER fazer a purga de assunto excluir a tag vencedora.
Resolver é POST /api/v1/taxonomy/conflicts/resolve (CURATE) e o desfazer é
DELETE /api/v1/taxonomy/conflicts/resolutions/{resolution_id} (CURATE). O desfazer é exato porque
o ledger grava created_link_ids e ban_created: as duas escritas usam ON CONFLICT DO NOTHING,
então reverter uma resolução posterior não pode levantar um bloqueio que uma anterior plantou, nem
excluir um vínculo que já existia antes. O ledger restaura a linha perdedora a partir do retrato dela.
Um par já resolvido pode ser decidido de novo — a segunda resolução é independente, e cada uma tem o
próprio desfazer.
Um par resolvido sai da varredura ao vivo
A resolução apaga a linha perdedora, então o join de trigrama não pode devolvê-la e a lista ao
vivo não tem o que anotar. É por isso que o trabalho sobrevive na fila do juiz e no ledger, e é
por isso que as abas Decididos e Resoluções existem.
Decidir uma categoria de uma vez¶
Uma colisão é decidida um par de cada vez, mas a decisão que o arquivista está de fato tomando é
categórica — "rua é entidade" — e um acervo pode ter dezenas da mesma grafia nos dois eixos. O lote,
POST /api/v1/taxonomy/conflicts/resolve/batch (CURATE), aplica um veredito à seleção inteira
que a lista mostra: os mesmos filtros pair_kind e entity_type, o mesmo threshold, e nada além.
A unidade da decisão é a seleção; a unidade da escrita continua sendo o par. Cada par é planejado pelo
mesmo planejador que a prévia de um par usa e escrito dentro do seu próprio savepoint, então um par que
falha não desfaz os outros, e cada par fica com a sua linha no ledger e o seu resolution_id — o
desfazer continua sendo por par mesmo quando o clique foi categórico. Um par cujo perdedor um par
anterior do mesmo lote já removeu é relatado em skipped com o motivo, nunca como falha: é
trabalho terminado, e chamá-lo de falha é o que já transformou um lote limpo numa parede de linhas
vermelhas.
O limit limita uma requisição, porque um clique que escreve milhares de linhas no ledger é um clique
que ninguém revisou. O remaining é a mesma leitura que a lista faz, feita de novo com os mesmos
filtros, então o número antes do clique e o número depois dele são uma só medição — a tela não pode
afirmar que uma categoria acabou quando a página seguinte discorda.
Qualidade¶
| Tela | Decisão | Reversível? |
|---|---|---|
Trechos — /qualidade/trechos |
de qual consumidor um trecho repetido sai | aposentar sim; remover do catálogo é definitivo |
Regras — /qualidade/regras |
se uma regra reescreve o texto ou só sinaliza | sim: aposentar tem caminho de volta, e a regra aposentada continua na tela |
Anomalias — /qualidade/anomalias |
nenhuma (a correção é a revisão da ficha) | sim (só leitura) |
Trechos — /qualidade/trechos¶
O catálogo de trechos repetidos. É o maior ganho medido do projeto: 53% do acervo compartilhava o
mesmo bloco de scope_content, e um prefixo de título repetido dominava os embeddings e degradava a
busca. A decisão que importa é o escopo: de qual consumidor o trecho sai — EMBEDDING (o vetor),
NER (os nomes) ou TITLE (o título sugerido). O mesmo bloco é retirado dos consumidores que ele
prejudica e mantido onde ajuda: aprovar tudo o que a máquina sugeriu piorou o ranking (Hit@10 0,562 →
0,500), e é por isso que não existe botão de "aprovar todos" nesta tela.
- Procurar —
POST /api/v1/quality/text-templates/suggest(CATALOGUE) registra os candidatos como sugestões inativas: nada entra no texto da IA antes de o arquivista aprovar um escopo. - Escrever um trecho —
POST /api/v1/quality/text-templates(CATALOGUE). Ele nasce aprovado e aplicado e devolve as descrições afetadas à fila da IA; o botão de cadastrar só libera depois de o impacto ser visto. - Aprovar / rejeitar / aposentar / salvar escopo / remover —
PATCH/DELETE /api/v1/quality/text-templates/{template_id}(CATALOGUE). Remover tira o trecho do catálogo e devolve as descrições afetadas à fila da IA.
Regras — /qualidade/regras¶
O único lugar onde uma expressão regular pode reescrever o acervo. rule_kind é a diferença entre
limpar e destruir, e a tela torna a escolha explícita:
REWRITEsubstitui cada ocorrência na coluna alvo na próxima execução — o worker de limpeza a filtra explicitamente, então uma regra de validação nunca reescreve;VALIDATEeLLM_CHECKsó sinalizam; o sinal vira um motivo de anomalia na descrição.
As escritas são POST /api/v1/quality/cleaning-rules (CATALOGUE) — salva só depois da prévia,
quando a regra reescreve — PATCH /api/v1/quality/cleaning-rules/{rule_id}/deactivate e
PATCH /api/v1/quality/cleaning-rules/{rule_id}/activate, as duas CATALOGUE. Uma regra nunca é
apagada, e o caminho de volta é uma rota, não uma regra nova: a listagem aceita
include_inactive=true, que é o que coloca as regras aposentadas no fim da tela com o botão que as
devolve à fila. Reativar preserva o id e o histórico que recadastrar a regra teria perdido.
Aposentar é a metade segura do par — é o que se procura quando uma regra está errada, então a saída
dela precisa existir.
Uma regra REWRITE ativa é a coisa mais barulhenta da tela, e a prévia é o que a torna visível: uma
regra de teste já ficou ativa e carimbou o acervo inteiro sem casar com nada.
Uma regra LLM_CHECK também nomeia uma engine e um preset, e os dois são escolhidos do
registro em vez de digitados de memória: os dois selects são alimentados por
GET /api/v1/quality/cleaning-rules/engines, que é o catálogo do próprio eixo de qualidade de título
— os mesmos nomes contra os quais get_engine valida, então um modelo que se aposenta deixa de ser
oferecido aqui na mesma mudança que o aposenta. O par é conferido quando a regra é gravada, então uma
regra que não pode rodar não pode ser salva (a recusa é um 422 nomeando as opções), e engine vazia
continua legal: significa o padrão do eixo. Uma regra gravada antes dessa conferência existir degrada
em vez de derrubar a execução — o validador registra o motivo, valida de forma determinística, e a
identidade de produtor dele é a de "sem modelo", então a fila e o contador do painel descrevem a
execução que de fato aconteceu.
Anomalias — /qualidade/anomalias¶
O que o validador de qualidade marcou, em leitura (GET /api/v1/documents filtrado por
NEEDS_REVIEW, opcionalmente por motivo de anomalia). As contagens por motivo são a faceta da própria
busca sobre o conjunto filtrado inteiro, e clicar numa delas estreita a lista. Uma fila vazia tem duas
causas muito diferentes e a tela as distingue: o acervo está limpo, ou nenhuma regra que sinaliza
(uma VALIDATE ou LLM_CHECK) está ativa — uma regra REWRITE muda o texto e nunca sinaliza. A
correção é a revisão humana da ficha, na descrição.
Sistema¶
A máquina, não o acervo. Tudo aqui pertence a quem opera a instalação, e as três telas são
alcançadas por /configuracoes, sob Operação: o menu ficou com o que o arquivista consulta
catalogando, e não é isto. As duas formas de entrada do acervo moram no Acervo (/acervo/nova), e a
página de origens é do operador; as rotas de leitura continuam autenticadas, então um curador que
segue uma URL direta ainda a vê só para leitura.
| Tela | Decisão | Reversível? |
|---|---|---|
Origens — /sistema/ingestao |
qual origem adicionar, as URLs e a agenda, se ela está ativa e se enfileira uma execução (operador) | a configuração, a agenda e a pausa são reversíveis; a tela não escreve descrição |
Workers e execuções — /sistema/workers |
se roda um worker agora (Painel); se para uma execução ativa (Execuções) | sim para a configuração da própria execução; os efeitos de uma execução seguem o que ela escreveu. Sem desfazer para uma execução cancelada: ela continua cancelada, e uma nova execução é uma nova linha no ledger |
Saúde do sistema — /sistema/diagnostico |
nenhuma (banco, modelos, storage, processo) | sim (só leitura) |
Origens — /sistema/ingestao¶
A etapa à frente do Acervo, origem por origem (ADR 0016). Origens lista as fontes configuradas: cada cartão tem a fila que a próxima execução leria, os números de processados e falhas, a última execução e a ativa, a agenda e a configuração efetiva. O operador adiciona uma definição pelo seletor, edita a configuração do adaptador (URLs de listagem e detalhe da coleta de referência, a espera de cortesia), salva um cron de cinco campos num fuso IANA, Desativa/Ativa e Enfileira execução. Pausar mantém a linha, a agenda e as execuções; só a execução é recusada. Uma instalação nova começa sem fonte configurada e mostra Nenhuma origem configurada; o fluxo de adicionar ativa a definição escolhida. A execução é o mesmo ledger dos workers de IA; um período que dispara com a origem já rodando é pulado, não enfileirado.
Entradas mostra as linhas manual e CSV só para leitura, com o status e um link para o Acervo:
elas são as formas de entrada, não fontes, e as duas escritas que ficavam aqui estão em Nova
descrição (#205). O cartão é OPERATE; as rotas de leitura continuam autenticadas, então um
curador que segue uma URL direta vê a página só para leitura, com os controles escondidos. As rotas
genéricas de worker recusam ingestion — uma execução dela precisa da origem, e as rotas da origem
são o seletor.
Workers e execuções — /sistema/workers¶
Uma tela com duas abas na URL (#207), e ela responde ao ciclo de conferir a execução que antes
precisava de duas telas: a aba Painel enfileira e mostra a fila, e a aba Execuções é o ledger
do que rodou. A última execução de um worker é um link para o ledger filtrado por esse worker, com a
linha exata realçada; o caminho antigo /sistema/execucoes redireciona para lá com os filtros
preservados. Só a aba ativa é montada, então só a leitura dela é consultada em ciclo.
O catálogo dos dez workers, na ordem do pipeline, com a engine, o preset e a configuração efetiva na
linha recolhida — "com qual modelo isto está rodando?" é a pergunta que a tela existe para responder,
e aqui a resposta é só leitura. O que a instalação está configurada a fazer é uma tela própria
(Configuração dos Workers, abaixo), porque são perguntas diferentes: esta é o que a máquina está
fazendo. A linha carrega a fila — pendentes, processados, falhas, última execução — e a única escrita
é OPERATE. Expandida, ela abre quatro fatos: a configuração efetiva, a fila, o livro do
worker (Carimbos (descrições) quando o livro é um carimbo, Processados caso contrário) e a última
execução. A fila e o livro ficam separados de propósito, e guides/operate.pt.md é quem diz o que
cada um dos dois conta:
- Rodar agora —
POST /api/v1/system/workers/{worker_name}/runs. A execução usa a configuração efetiva, ajustada só para esta vez; nada ajustado aqui vira padrão. Toda execução deixa uma linha no ledger de execuções, e um worker com execução em andamento desabilita o botão — a garantia é o índice único parcial no banco, e a tela só evita oferecer o que o banco recusaria.
A linha ingestion é a exceção ao botão de execução: a execução dela pertence a uma origem, então a linha leva a /sistema/ingestao (a tela da origem) e a rota genérica a recusa.
Os campos do painel de execução vêm do contrato, não de uma caixa de texto: declared_options nomeia
as opções que o worker aceita e, para uma lista de colunas, as colunas que o texto da IA tem
(original_title, final_title, scope_content, admin_bio_history, provenance), então a tela
renderiza um checkbox, um número ou as próprias colunas. O único bloco livre é Repasses ao engine
(avançado): um repasse cru ao construtor da engine, por execução e nunca persistido, com as chaves que
o eixo já repassa (model, device, host, ...) como dica. É a exceção nomeada da #120 — existe para
quem conhece a biblioteca, e a engine valida o valor.
O painel em si não oferece desfazer para uma execução. A reversibilidade de uma execução é a
reversibilidade do que ela escreveu — um desfazer de merge, de materialização, de conflito — e um
worker que carimba a própria fila não refaz o trabalho só porque foi rodado de novo. O painel marca
quatro workers como fora do bloqueio de revisão humana — a transferência staging → archive, o
juiz de conflito assunto x entidade, a categorização de assunto (tags) e o worker_embedding — e só
o worker_embedding reescreve uma descrição: ele re-gera o vetor de um texto mudado mesmo numa
descrição aprovada, a exceção documentada. Os outros três trabalham sobre unidades que não são
descrições (um registro de staging, um par, uma tag).
Execuções — a aba do ledger¶
Desde a #207 o ledger é a aba Execuções de /sistema/workers, e /sistema/execucoes redireciona
para ela com a busca preservada. É leitura exceto pela parada: uma linha por execução, da linha de
comando e do painel, com a configuração já resolvida que a execução usou (então ele continua
legível depois que
um preset muda no código), as opções efetivas do worker (as colunas que ele leu, o limiar, o teto —
resolvidas quando a execução começou, então "quais colunas a execução #N leu?" tem resposta), a
duração, o desfecho e quem pediu. Uma linha anterior à coluna de opções diz "opções não registradas
nesta execução". Acima dele, as falhas dos últimos 30 dias
agrupadas por causa (GET /api/v1/system/failures), que respondem à pergunta que o ledger não
responde: quarenta linhas dizendo a mesma frase são uma causa. Clicar num grupo filtra o ledger
abaixo em vez de abrir uma segunda lista, e a referência de uma falha é o que se procura no log.
A única escrita é Cancelar execução… numa linha ativa (POST /api/v1/system/runs/{run_id}/cancel,
OPERATE). Uma execução na fila que nenhum runner reivindicou é cancelada na hora; uma em andamento só
recebe o pedido, e a linha continua dizendo "em execução" com uma nota até o worker chegar ao limite do
lote — o lote em curso é comitado, não interrompido, porque uma passada do modelo não pode ser cortada
no meio. Um segundo clique é o mesmo pedido, e uma execução que já terminou responde 409.
Saúde do sistema — /sistema/diagnostico¶
Quatro verificações independentes (GET /api/v1/system/health, OPERATE): o banco com suas
contagens, o servidor Ollama com os modelos que os presets exigem e quais deles faltam, o storage das
miniaturas e a configuração efetiva do processo. Cada verificação responde por si — um banco fora do
ar não pode esconder que o Ollama está bem — e nenhum valor de segredo é mostrado: o cartão do
processo informa que um segredo existe, nunca qual é. Nada aqui escreve.
Configurações¶
A landing que reúne o que a instalação é, fora do caminho do que o arquivista faz todo dia. Ela mesma não escreve nada.
| Tela | Decisão | Reversível? |
|---|---|---|
Configurações — /configuracoes |
qual tela de montagem abrir | sim (só leitura) |
Configuração dos Workers — /configuracoes/workers |
o padrão persistido de cada worker, e o histórico dele | sim (remover a linha) |
Usuários — /configuracoes/usuarios |
quem existe, o que pode fazer e onde está conectado | sim (desativar e reativar) |
Configurações — /configuracoes¶
Uma página de cartões, e cada cartão abre uma tela que já existia com a rota que sempre teve: o que mudou foi como se chega até ela. Três abas, na ordem da escada de permissões que elas percorrem:
| Aba | Cartões |
|---|---|
| Arranjo e catálogos | Plano de arranjo (CURATE), Níveis de descrição, Tipologias e Vocabulário do acervo (CATALOGUE) |
| Operação | Workers e execuções, Configuração dos Workers e Saúde do sistema (OPERATE) |
| Acesso | Usuários (ADMIN) |
A aba fica na URL (?aba=), então dá para mandar alguém direto aos "cartões dos workers". Uma aba que
a conta não pode preencher não é renderizada, e um ?aba= que a nomeie cai na primeira aba que a
conta pode preencher: um link válido para quem mandou ainda aterrissa em algo honesto para quem lê.
Cada cartão carrega a área em que a sua tela escreve, e a entrada que abre esta página segue as mesmas
áreas — a regra está em O menu esconde; nunca concede, acima.
Configuração dos Workers — /configuracoes/workers¶
O que a instalação está configurada a fazer, separado do que a máquina está fazendo (issue #53). Um
cartão por worker, na ordem do pipeline, cada um com a linha persistida de archive_worker_settings e
suas revisões — as duas escritas são OPERATE:
- Salvar padrão —
PUT /api/v1/system/workers/{worker_name}/settingsgrava o padrão. É parcial de propósito: um campo vazio continua seguindo o código, e a precedência não muda —argumento explícito > a linha de archive_worker_settings > o default da assinatura. O formulário renderiza um controle por opção declarada (uma lista de colunas como as colunas que o texto da IA tem);limitnão aparece, porque um teto pertence a uma execução, econfigcontinua recusado como opção, porque é um dataclass do runner e não um valor JSON.engine_name,presetedb_batch_sizetêm campo próprio e são recusados dentro das opções, que seriam uma segunda definição do mesmo campo. - Conferir impacto —
POST /api/v1/system/workers/{worker_name}/queue/previewresponde quantas unidades a configuração que o formulário mostra deixaria na fila, e não escreve nada. Existe porque a fila é chaveada pela identidade do produtor: salvar outra engine, outro preset ou outro limiar relê as unidades afetadas sozinho, e esse custo precisa ser legível antes de ser pago. O número some assim que um campo muda, então ele nunca descreve uma configuração que o formulário não mostra mais. - Voltar ao padrão do código —
DELETE /api/v1/system/workers/{worker_name}/settingsremove a linha. Toda escrita deixa uma revisão (GET /api/v1/system/workers/{worker_name}/settings/revisions), listada no cartão, então o padrão é reversível e quem o alterou fica nomeado.
Um worker cuja engine vive na regra LLM_CHECK ativa (quality-validator) não oferece a escolha de
engine/preset aqui: um segundo lugar para escolher a mesma engine seria uma segunda fonte de verdade,
então o cartão diz onde ela vive e oferece só os parâmetros de execução. A tela lê
GET /api/v1/system/workers/settings, que não é o GET /api/v1/system/workers do painel — o painel
conta nove filas, e esta tela não mostra nenhuma delas.
Uma linha gravada antes da validação de opções não é um erro novo: a leitura ignora a opção que não consegue mais honrar e explica isso na nota do cartão ("Padrão salvo ignorado — …"), então a tela responde com o default do código em vez de derrubar o contador da fila. Salvar o formulário de novo escreve só os campos que ele mostra, o que cura a linha.
Usuários — /configuracoes/usuarios¶
As contas da instalação, a superfície ADMIN. As escritas são POST /api/v1/users (criar),
PATCH /api/v1/users/{user_id} (nome, papel, ativa), POST /api/v1/users/{user_id}/password
(redefinir) e as rotas de sessão GET/DELETE /api/v1/users/{user_id}/sessions[/{session_id}] —
todas ADMIN. Cinco fatos que a API impõe e a tela declara:
- a senha criada aqui é temporária. O administrador a digitou, então a conta a troca no primeiro acesso, exatamente como no bootstrap do CLI;
- desativar encerra todas as sessões da conta na hora, que é o que torna o sinalizador real;
- redefinir a senha encerra todas as sessões e destrava um bloqueio por tentativas falhas. A ação fica na própria linha da conta (redefinir senha), sem precisar expandir a ficha, e a confirmação nomeia as duas consequências;
- redefinir a própria conta desconecta você. A redefinição administrativa não tem o "mantenha esta sessão" do Trocar senha, então o administrador que aponta para a própria linha encerra a sessão em que está; a tela avisa antes do clique e aponta o Trocar senha no rodapé da sessão, que a mantém;
- a última conta de administrador ativa não pode ser desativada nem rebaixada (
LastAdminError, 409). É o único beco sem saída que não tem volta pela tela; a entrada é o CLI no host.
Não há exclusão. Uma conta é desativada, porque os ledgers carregam o nome e o id dela, e uma decisão cujo autor não existe mais é um registro pior do que uma conta fechada. Revogar uma sessão que pertence a outra conta responde 404 e não 200, porque só o id não pode enumerar os acessos de outras pessoas; a linha que é a própria sessão do arquivista fica marcada como "esta sessão".
A conta desta tela nunca é a primeira. Uma instalação sem conta nenhuma é criada pela tela de primeiro acesso, que ela responde à primeira visita, ou pela CLI na máquina, e as duas se fecham para sempre quando uma conta existe — inclusive uma desativada (ADR 0011). Depois disso, esta é a única superfície que cria contas.