fix: campo CITE AS seleciona nota de rodapé errada e quebra espaçamento sem volume - #1350
Conversation
…to sem volume
extract_cite_as_part_one pegava a primeira <fn fn-type="other"> do
documento, sem checar o <label>. fn-type="other" e uma categoria
generica do JATS usada para qualquer tipo de nota (institucional,
declaracao de uso de IA, codigos JEL, politica de plagio, registro
ZooBank), entao o campo CITE AS do PDF - que deveria ser uma citacao
bibliografica - imprimia conteudo errado em 7 de 26 artigos do corpus
de teste, incluindo um caso (a29.xml) em que a nota correta existia no
mesmo fn-group mas nao era a primeira.
Passa a percorrer todas as <fn fn-type="other"> do documento e so usa
a que tiver label (ou, na ausencia dele, inicio do texto do <p>)
sinalizando citacao ("Como citar:", "CITE AS:", "How to cite this
article"); nada e retornado quando nenhuma nota corresponde. Tambem
troca part_one.text por extracao de texto completa (itertext), que
antes cortava a citacao no primeiro elemento filho (ex: um DOI dentro
de <ext-link> logo apos o texto).
Corrige tambem docx_cite_as_pipe: cite_as_part_two montava
"{volume}: {location}" sem checar se volume existia, deixando um ':'
solto quando o XML nao tem <volume> (ex: "Cadernos Pagu : e236720.").
Extrai _format_cite_as_part_two, que omite o segmento de volume e seu
separador nesse caso.
Issue: scieloorg#1349
There was a problem hiding this comment.
A correção resolve a seleção da primeira fn-type="other", mas o resultado ainda não produz uma referência bibliográfica cientificamente correta.
Quando existe uma nota editorial de “Como citar”, ela já contém a referência completa e deve ser usada integralmente, sem acrescentar novamente periódico, volume e localização. No estado atual, casos como a8 e a29 ficam duplicados ou concatenados sem espaço; em a3, o resultado começa com CITE AS: Como citar:.
Quando a nota não existe, como em a7 e a28, os PDFs originais mostram que o comportamento esperado não é deixar o campo vazio nem gerar apenas Periódico volume: localização. Deve ser construída uma referência completa a partir dos metadados — autores, ano, título, periódico, volume/número, páginas ou e-location e DOI — segundo um formato explicitamente definido.
Também considero frágil identificar a nota apenas por 'cit' in signal.lower(), pois isso admite falsos positivos e pode ignorar uma citação cujo label seja genérico, mas cujo p comece com “Como citar”.
Solicito ajustar o PR e a issue #1349 para definir e testar estes dois caminhos:
- nota editorial explícita: utilizar a citação completa uma única vez;
- ausência da nota: gerar uma referência bibliográfica completa e determinística a partir dos metadados.
Os testes devem validar o texto final do DOCX, incluindo pelo menos a3, a7, a8, a28 e a29, e não apenas extrator e renderer isoladamente.
Dois ajustes pedidos na revisao do PR scieloorg#1350: - docx_cite_as_pipe imprimia journal_title/volume/localizacao depois de cite_as_part_one incondicionalmente. Quando a nota "como citar" ja e uma citacao completa (o caso normal), isso duplicava conteudo (a8, a29) ou colava sem separador (a3). Agora, quando a nota existe, ela e usada sozinha; o fallback journal/volume/localizacao so entra quando nao ha nota (a7, a28 - ainda incompletos, tratamento fica para uma proposta de formato separada). - extract_cite_as_part_one trocou o teste 'cit' in signal.lower() por uma lista de frases-ancora (como citar, cite as, how to cite...), e passa a remover o prefixo da propria frase quando ela vem embutida no <p> por falta de <label> proprio (caso a3: "Como citar: Autor...") - senao duplicava com o "CITE AS: " que o chamador ja imprime. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V829P3TiVHWnnde6tumABq
|
Oi @pitangainnovare, avancei em duas partes dos três pontos levantados. Corrigido: duplicação da nota completa (a3, a8, a29)
Corrigido: heurística de detecção frágilTroquei Pendente de confirmação: referência completa quando não há nota (a7, a28)Toda a metadata necessária já está disponível e extraível (autores, ano, título, periódico abreviado, volume/número, páginas/e-location, DOI) - não é isso que falta. O que falta é uma decisão de formato: os exemplos reais mostram estilos diferentes por periódico (a8 usa Proposta (estilo Vancouver, comum em citação de artigo): Exemplo com os dados reais do a7 (hoje sai Antes de implementar (formatação de nomes com iniciais, truncamento com "et al." se muitos autores, etc.), gostaria de confirmar se esse formato serve ou se há um padrão SciELO já definido que eu não encontrei. Posso abrir isso como issue separada de #1349/#1350 se preferir tratar como funcionalidade nova em vez de bugfix. Suíte completa de |
…itorial Ponto 3 da revisao do PR scieloorg#1350: a maioria dos artigos do corpus nao tem uma nota "como citar este artigo" (fn-type=other so cobre o caso minoritario onde ela existe). Sem uma, o CITE AS ficava incompleto ("Periodico Volume: localizacao", sem autores/titulo/DOI). build_full_citation monta a citacao a partir da propria metadata do artigo (autores com iniciais estilo Vancouver, titulo, periodico abreviado, ano/volume/numero/localizacao, DOI), truncando em 6 autores + "et al." (regra ICMJE). docx.py usa extract_cite_as_part_one(...) or build_full_citation(...) - a nota editorial continua tendo prioridade quando existe. O parametro `style` (so 'vancouver' implementado) segue o mesmo padrao de layout_config.load_page_attributes: nao le config ainda, mas deixa o ponto de extensao pronto para um JSON de configuracao por periodico escolher o formato no futuro sem mudar a assinatura da funcao. Corrigido de quebra: extract_doi lanca AttributeError de proposito quando nao ha DOI (contrato ja testado); build_full_citation checa o no diretamente em vez de arriscar propagar essa excecao para artigos sem DOI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V829P3TiVHWnnde6tumABq
|
Implementei o ponto 3 (referência completa quando não há nota editorial) formato Vancouver como padrão, com o parâmetro Toda a metadata é extraída da própria XML (autores com iniciais estilo Vancouver - excluindo partículas minúsculas como "da"/"de"/"dos" - título, periódico abreviado, ano/volume/número/localização, DOI), truncando em 6 autores + "et al." (regra ICMJE). Exemplo real, a7.xml (antes incompleto): Antes: Depois: Validado contra as 26 amostras do corpus: a maioria não tinha nota editorial e ganhou uma citação completa; os que já tinham nota real (a8, a17, a29, a30) continuam intocados, já que a nota tem prioridade sobre o formato construído. De quebra corrigi um bug real que apareceu no caminho: Suíte completa de |
| # Only 'vancouver' exists today. Kept as a plain function parameter/dict | ||
| # dispatch (instead of hardcoding the format inline) so a future per-journal | ||
| # JSON config can plug in a different style by name without changing | ||
| # build_full_citation's contract - the same not-wired-yet-to-config pattern | ||
| # as layout_config.load_page_attributes. | ||
| CITATION_STYLE_VANCOUVER = 'vancouver' | ||
| _MAX_CITATION_AUTHORS_BEFORE_ET_AL = 6 | ||
|
|
||
|
|
||
| def _extract_citation_authors(xml_tree): | ||
| """ | ||
| Returns the article's own contributors as Vancouver-style "Surname IN" | ||
| strings (surname, then the initials of given names - lowercase | ||
| particles like "da"/"de"/"dos" excluded from initials, e.g. "Bárbara | ||
| Passos da Silva" -> "BPS"). | ||
|
|
||
| Args: | ||
| xml_tree (ElementTree): The XML tree to extract authors from. | ||
|
|
||
| Returns: | ||
| list: Author strings in document order; empty if there's no | ||
| <contrib-group> or no <contrib> has both <surname> and text. | ||
| """ | ||
| article_meta = xml_tree.find('./front/article-meta') | ||
| metadata_scope = article_meta if article_meta is not None else xml_tree | ||
| contrib_group = metadata_scope.find('.//contrib-group') | ||
| if contrib_group is None: | ||
| return [] | ||
|
|
||
| authors = [] | ||
| for contrib in contrib_group.findall('.//contrib'): | ||
| name = contrib.find('name') | ||
| if name is None: | ||
| continue | ||
| surname = name.find('surname') | ||
| if surname is None or not (surname.text or '').strip(): | ||
| continue | ||
|
|
||
| given_names = name.find('given-names') | ||
| initials = '' | ||
| if given_names is not None and given_names.text: | ||
| initials = ''.join( | ||
| word[0].upper() for word in given_names.text.split() if word[:1].isupper() | ||
| ) | ||
|
|
||
| author = surname.text.strip() | ||
| if initials: | ||
| author = f'{author} {initials}' | ||
| authors.append(author) | ||
|
|
||
| return authors | ||
|
|
||
|
|
||
| def _sentence(text): | ||
| """Appends a period unless text already ends with terminal punctuation.""" | ||
| return text if text.endswith(('.', '!', '?')) else f'{text}.' | ||
|
|
||
|
|
||
| def _format_vancouver_citation(xml_tree, footer_data): | ||
| """ | ||
| Builds a Vancouver-style citation: "Surname IN, Surname IN. Article | ||
| title. Journal abbrev. Year;Volume(Issue):location. https://doi.org/...". | ||
| Missing pieces (issue, DOI...) are simply omitted rather than leaving a | ||
| stray separator; returns '' when there are no authors to start from. | ||
| """ | ||
| authors = _extract_citation_authors(xml_tree) | ||
| if not authors: | ||
| return '' | ||
| if len(authors) > _MAX_CITATION_AUTHORS_BEFORE_ET_AL: | ||
| authors = authors[:_MAX_CITATION_AUTHORS_BEFORE_ET_AL] + ['et al'] | ||
| segments = [f"{', '.join(authors)}."] | ||
|
|
||
| title = extract_article_title(xml_tree) | ||
| if title: | ||
| segments.append(_sentence(title)) | ||
|
|
||
| abbrev_journal = xml_tree.find('.//abbrev-journal-title') | ||
| journal = ( | ||
| ''.join(abbrev_journal.itertext()).strip() | ||
| if abbrev_journal is not None | ||
| else extract_journal_title(xml_tree) | ||
| ) | ||
| if journal: | ||
| segments.append(_sentence(journal)) | ||
|
|
||
| year = footer_data.get('year') or '' | ||
| volume = footer_data.get('volume') or '' | ||
| issue = footer_data.get('issue') or '' | ||
| location = footer_data.get('location_label') or '' | ||
|
|
||
| vol_issue = f'{volume}({issue})' if volume and issue else volume | ||
| date_and_location = year | ||
| if vol_issue: | ||
| date_and_location = f'{date_and_location};{vol_issue}' if date_and_location else vol_issue | ||
| if location: | ||
| date_and_location = f'{date_and_location}:{location}' if date_and_location else location | ||
| if date_and_location: | ||
| segments.append(_sentence(date_and_location)) | ||
|
|
||
| # Not extract_doi(): that function raises AttributeError on a missing | ||
| # DOI by design (see test_extract_doi_missing_doi) - a DOI is just | ||
| # another optional piece of a citation, not something to blow up over. | ||
| doi_node = xml_tree.find('.//article-id[@pub-id-type="doi"]') | ||
| doi = doi_node.text if doi_node is not None else None | ||
| if doi: | ||
| segments.append(f'https://doi.org/{doi}') | ||
|
|
||
| return ' '.join(segments) | ||
|
|
||
|
|
||
| _CITATION_STYLE_FORMATTERS = { | ||
| CITATION_STYLE_VANCOUVER: _format_vancouver_citation, | ||
| } | ||
|
|
||
|
|
||
| def build_full_citation(xml_tree, footer_data, style=CITATION_STYLE_VANCOUVER): | ||
| """ | ||
| Builds a complete "how to cite this article" citation from the | ||
| article's own metadata (authors, title, journal, volume/issue/location, | ||
| DOI), for use as a fallback when `extract_cite_as_part_one` finds no | ||
| explicit editorial note (see issue #1349's review: some articles simply | ||
| don't carry one). | ||
|
|
||
| `style` selects the citation format. Only CITATION_STYLE_VANCOUVER | ||
| exists today; it's a plain parameter (not read from a config file) so a | ||
| future per-journal JSON config can choose it by name later without | ||
| changing this function's contract - not wired to the CLI/API yet. | ||
|
|
||
| Args: | ||
| xml_tree (ElementTree): The XML tree to build the citation from. | ||
| footer_data (dict): Output of `extract_footer_data` (year/volume/issue/location_label). | ||
| style (str, optional): Citation format identifier. Defaults to CITATION_STYLE_VANCOUVER. | ||
|
|
||
| Returns: | ||
| str: The complete citation, or '' when the style is unknown or the | ||
| article has no authors to build one from. | ||
| """ | ||
| formatter = _CITATION_STYLE_FORMATTERS.get(style) | ||
| if formatter is None: | ||
| return '' | ||
| return formatter(xml_tree, footer_data) | ||
|
|
There was a problem hiding this comment.
Em lugar de implementar uma lógica para montar a citação, podemos usar uma biblioteca chamada citeproc-py e citeproc-py-styles. Essas libs tem uma estrutura que permite receber os metadados e devolver a citação completa, no formato que bem desejar. Então bastaria nós usarmos os métodos para extrairmos autor, revista, volume, ano, etc, e passar nessa libs. Elas seriam as responsáveis por montar a full citation. Veja um exemplo:
pip install citeproc-py citeproc-py-styles bibtexparserIn [1]: from citeproc import (
...: Citation,
...: CitationItem,
...: CitationStylesBibliography,
...: CitationStylesStyle,
...: formatter,
...: )
...: from citeproc.source.json import CiteProcJSON
...: from citeproc_styles import get_style_filepath
...:
...: references = [{
...: "id": "ref1",
...: "type": "article-journal",
...: "title": "An example paper",
...: "author": [
...: {"family": "Damaceno", "given": "Rafael"}
...: ],
...: "issued": {"date-parts": [[2026]]},
...: "container-title": "Journal of Examples",
...: "volume": "10",
...: "page": "1-10"
...: }]
...:
...: source = CiteProcJSON(references)
...:
...: style = CitationStylesStyle(
...: get_style_filepath("apa")
...: )
...:
...: bibliography = CitationStylesBibliography(
...: style,
...: source,
...: formatter.plain
...: )
...:
...: citation = Citation([CitationItem("ref1")])
...: bibliography.register(citation)
...:
...: print(bibliography.cite(citation, lambda x: None))
...:
...: for ref in bibliography.bibliography():
...: print(str(ref))
...:
(Damaceno, 2026)
Damaceno, R. (2026). An example paper. Journal of Examples, 10, 1–10.
pitangainnovare
left a comment
There was a problem hiding this comment.
Acredito que o ideal é usar uma lib externa e já deixar pronta a estrutura para gerar citações em quaisquer estilos. Veja nos comentários como é mais tranquilo. Bastaria montar o objeto Reference com os metadados extraídos do XML.
O que esse PR faz?
Corrige a issue #1349: o campo CITE AS do rodapé da página 1 tinha dois problemas.
extract_cite_as_part_onepegava a primeira<fn fn-type="other">do documento, sem checar o<label>.fn-type="other"é uma categoria genérica do JATS usada para qualquer tipo de nota (institucional, declaração de uso de IA, códigos JEL, política de plágio, registro ZooBank), então o CITE AS - que deveria ser uma citação bibliográfica - imprimia conteúdo errado em 7 de 26 artigos do corpus de teste, incluindo um caso (a29.xml) em que a nota correta existia no mesmofn-groupmas não era a primeira.docx_cite_as_pipemontavacite_as_part_twocomof'{volume}: {location}'sem checar sevolumeexistia, deixando um:solto quando o XML não tem<volume>(ex: "Cadernos Pagu : e236720.").Atualização pós-revisão: a revisão apontou três pontos - duplicação da nota completa, heurística de detecção frágil, e ausência de referência completa quando não há nota editorial. Todos corrigidos - ver seções "Revisão, rodada 2" e "Revisão, rodada 3" abaixo.
Onde a revisão poderia começar?
packtools/sps/formats/pdf/pipeline/xml.py, funçãoextract_cite_as_part_one, epacktools/sps/formats/pdf/pipeline/docx.py,docx_cite_as_pipee o helper_format_cite_as_part_two.Como este poderia ser testado manualmente?
a11.xmldo corpus de teste, cuja primeira<fn fn-type="other">é uma nota institucional.CITE AS: Trabalho realizado na Universidade Federal de Pernambuco - UFPE - Recife (PE), Brasil.Audiology - Communication Research 28: e2725.CITE AS: Audiology - Communication Research 28: e2725.a18.xml(sem<volume>).CITE AS: Cadernos Pagu : e236720.Depois:CITE AS: Cadernos Pagu e236720.a8.xmloua29.xml(nota "como citar" já é uma citação completa).CITE AS: Sousa VR, Gomes MM, Couri MS (2024) On Cerodontha...Zoologia 41: e23038. https://doi.org/10.1590/S1984-4689.v41.e23038.tests/fixtures/pdf/a3.xml(nota sem<label>próprio, frase "Como citar:" embutida no<p>).CITE AS: Como citar: Souza CM, Iser BM...Depois:CITE AS: Souza CM, Iser BM...(prefixo removido).a7.xml(sem nenhuma nota "como citar").CITE AS: Acta Botanica Brasilica 40: e20250035.(sem autores/título/DOI). Depois:CITE AS: Oliveira BPS, Leite KRB, Amorim VO, Roque N. Leaf morphoanatomy supporting evolutionary relationships in a recent clade of Asteraceae. Acta Bot. Bras. 2026;40:e20250035. https://doi.org/10.1590/1677-941X-ABB-2025-0035.pytest tests/sps/formats/pdf/pipeline/test_xml.py tests/sps/formats/pdf/pipeline/test_docx.pycobre todos os casos com testes de regressão.Algum cenário de contexto que queira dar?
Achado revisando o PR #1326: o
@pitangainnovarenotou o espaçamento quebrado sem volume no CITE AS e sugeriu abrir uma issue separada, já que "o problema não é só esse". Investigando a fundo, a causa maior é a seleção da nota errada (fn-type="other"sendo tratado como sinônimo de "nota de citação", o que não é garantido pelo JATS).Validado contra as 26 amostras do corpus de teste: 6 casos que vazavam conteúdo errado (a10, a11, a12, a20, a24, a28) agora retornam vazio corretamente; a29 passou a capturar a nota real de citação em vez de uma URL do ZooBank; os 3 casos que já funcionavam (a8, a17, a30) continuam corretos e ganharam texto completo (antes truncado por usar
.textem vez de itertext).Revisão, rodada 2 (2026-09-10)
Dois pontos corrigidos:
docx_cite_as_pipeimprimiajournal_title/volume/localização depois decite_as_part_oneincondicionalmente. Quando a nota "como citar" já é uma citação completa (o caso normal), isso duplicava conteúdo ou colava sem separador. Agora, quando a nota existe, ela é usada sozinha; o fallback journal/volume/localização só entra quando não há nota (a7, a28 - ver ponto pendente abaixo).'cit' in signal.lower()por uma lista de frases-âncora (pt/en: "como citar", "cite as", "how to cite this article", "citação sugerida"...) via regex, e passei a remover o prefixo da própria frase quando ela vem embutida no<p>por falta de<label>próprio (caso do fixturea3.xml, sem essa remoção ficaria "CITE AS: Como citar: ...").Suíte completa de
tests/sps/formats/pdf/agora com 230 testes, todos passando.Revisão, rodada 3 (2026-09-10)
Ponto 3 da revisão implementado: referência completa quando não há nota editorial (a7, a28, e na prática a maioria do corpus - a maior parte dos artigos simplesmente não carrega uma nota "como citar este artigo").
Nova função
build_full_citationmonta a citação a partir da própria metadata do artigo (autores com iniciais estilo Vancouver, título, periódico abreviado, ano/volume/número/localização, DOI), truncando em 6 autores + "et al." (regra ICMJE).docx.pyagora usaextract_cite_as_part_one(...) or build_full_citation(...)- a nota editorial continua tendo prioridade quando existe.O formato é Vancouver por decisão do mantenedor, já antecipando um plano de configuração por periódico: o parâmetro
styledebuild_full_citation(só'vancouver'implementado, via dict de despacho) segue o mesmo padrão já usado emlayout_config.load_page_attributes- não lê um arquivo de configuração ainda, mas deixa o ponto de extensão pronto para um JSON de configuração escolher o estilo por periódico no futuro, sem mudar a assinatura da função.Validado contra as 26 amostras do corpus: a maioria não tinha nota e antes mostrava só
"CITE AS: Periódico Vol: local."incompleto - agora sai uma citação completa, ex. a7:Oliveira BPS, Leite KRB, Amorim VO, Roque N. Leaf morphoanatomy... Acta Bot. Bras. 2026;40:e20250035. https://doi.org/10.1590/1677-941X-ABB-2025-0035.Os que já tinham nota real (a8, a17, a29, a30) continuam usando a nota, intocados.Corrigido de quebra:
extract_doilançaAttributeErrorde propósito quando não há DOI (contrato já testado emtest_extract_doi_missing_doi);build_full_citationcheca o nó diretamente em vez de arriscar propagar essa exceção para artigos sem DOI.Suíte completa de
tests/sps/formats/pdf/agora com 239 testes, todos passando.Screenshots
a11.xml (nota institucional sendo usada como citação):
a18.xml (sem volume, espaçamento quebrado):
Quais são os tickets relevantes?
Issue #1349
Referências
Comentário original do @pitangainnovare: #1326 (review)
Segurança da informação (NSI.04)
Este PR manipula dados sensíveis ou pessoais (LGPD)?
Este PR altera autenticação, autorização, controle de acesso ou gerenciamento de sessão?
Este PR introduz, atualiza ou remove dependências de terceiros?
Este PR foi validado pelo pipeline de segurança (SonarQube / Trivy)?
Este PR concatena, monta ou executa comandos SQL, HTML ou JavaScript a partir de entrada externa?
Este PR expõe novos endpoints, telas ou serviços?
Algum segredo, senha, chave ou token está sendo adicionado ao código-fonte?
🤖 Generated with Claude Code
https://claude.ai/code/session_01V829P3TiVHWnnde6tumABq