Skip to content

Add <xref> element validations per SPS 1.10 specification#1136

Open
Copilot wants to merge 2 commits intomasterfrom
copilot/create-validations-for-xref-element
Open

Add <xref> element validations per SPS 1.10 specification#1136
Copilot wants to merge 2 commits intomasterfrom
copilot/create-validations-for-xref-element

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 19, 2026

O que esse PR faz?

Implementa 7 das 10 regras de validação para o elemento <xref> conforme SPS 1.10 e Critérios SciELO Brasil (70% de conformidade).

P0 – Críticas:

  • validate_rid_presence (CRITICAL) — @rid obrigatório e não-vazio
  • validate_ref_type_presence (CRITICAL) — @ref-type obrigatório e não-vazio
  • validate_ref_type_value (ERROR) — @ref-type deve ser um dos 16 valores permitidos
  • validate_bibr_presence (ERROR) — ao menos um <xref ref-type="bibr"> no documento
  • validate_rid_has_corresponding_id (ERROR) — todo @rid deve ter @id correspondente

P1 – Importantes:

  • validate_transcript_xref (WARNING) — <sec sec-type="transcript"> precisa de <xref ref-type="sec">
  • validate_aff_self_closing (INFO) — <xref ref-type="aff"> sem texto deve usar formato self-closing

Corrige bugs no método existente validate_attrib_name_and_value_has_corresponding_xref() que usava self.xref_xml (inexistente) e o builtin id em vez da variável do loop.

Onde a revisão poderia começar?

packtools/sps/validation/article_xref.py — contém todas as novas validações e as correções nos métodos existentes.

Como este poderia ser testado manualmente?

from lxml import etree
from packtools.sps.validation.article_xref import ArticleXrefValidation

xml = etree.fromstring('''
<article article-type="research-article" xml:lang="pt">
  <body>
    <p><xref ref-type="bibr" rid="B1">1</xref></p>
    <p><xref ref-type="image" rid="f1">Figure 1</xref></p>
    <p><xref ref-type="fig">Figure 2</xref></p>
  </body>
  <back>
    <ref-list><ref id="B1"><mixed-citation>Ref</mixed-citation></ref></ref-list>
  </back>
</article>
''')

v = ArticleXrefValidation(xml)
for r in v.validate_rid_presence():
    print(r["response"])      # OK, OK, CRITICAL (missing rid)
for r in v.validate_ref_type_value():
    print(r["response"])      # OK, ERROR ("image" invalid), skipped (empty)
for r in v.validate_bibr_presence():
    print(r["response"])      # OK

74 testes unitários: python -m pytest tests/sps/validation/test_article_xref.py -v

Algum cenário de contexto que queira dar?

Validações parciais para <xref> já existiam (validate_xref_rid_has_corresponding_element_id, validate_element_id_has_corresponding_xref_rid, validate_attrib_name_and_value_has_corresponding_xref). Este PR complementa com as regras faltantes e corrige bugs nos métodos existentes. Os testes existentes foram reescritos para o formato atual com suporte a i18n (msg_text, msg_params, adv_text, adv_params).

Arquivos alterados:

  • packtools/sps/validation_rules/xref_rules.json — novo arquivo de configuração de regras
  • packtools/sps/models/v2/article_xref.pyall_xrefs(), all_ids(), transcript_sections() em XMLCrossReference
  • packtools/sps/validation/article_xref.py — 7 novos métodos + correção de bugs
  • packtools/sps/validation/xml_validations.py — orquestrador atualizado
  • tests/sps/validation/test_article_xref.py — 74 testes em 14 classes

Screenshots

N/A

Quais são tickets relevantes?

Referências

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • api.crossref.org
    • Triggering command: /usr/bin/python python -m pytest tests/sps/validation/ -v --ignore=tests/sps/validation/test_footnotes.py (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>Criar validações para o elemento </issue_title>
<issue_description>## Objetivo

Implementar validações para o elemento <xref> conforme a especificação SPS 1.10 e Critérios SciELO Brasil, aumentando a conformidade de X% para 70% (7 de 10 regras).

Nota: Algumas validações para <xref> podem já estar parcialmente implementadas no repositório. Este Issue visa reavaliar, complementar e garantir cobertura completa das regras SPS 1.10 e Critérios SciELO Brasil.


Contexto

O elemento <xref> é usado para referência cruzada relacionando informações no texto. Para SciELO Brasil, é obrigatória a presença de pelo menos uma <xref> com @ref-type="bibr" (referência bibliográfica) no documento. Validações corretas garantem presença de atributos obrigatórios, valores válidos, e correspondência entre @rid e @id.

Conformidade atual: X de 10 regras implementadas (X%)
Meta após implementação: 7 de 10 regras (70%)


Documentação SPS

Referência oficial: https://docs.google.com/document/d/1GTv4Inc2LS_AXY-ToHT3HmO66UT0VAHWJNOIqzBNSgA/edit?tab=t.0#heading=h.xref

Regras principais conforme SPS 1.10 e Critérios SciELO Brasil:

  1. Ocorrência:

    • <xref> pode aparecer zero ou mais vezes em: <article-title>, <attrib>, <contrib>, <p>, <td>, <th>, <trans-title>, <sec>, <verse-line>
  2. Obrigatoriedade (Critério SciELO Brasil):

    • <xref> com @ref-type="bibr" deve ocorrer pelo menos uma vez no documento
  3. Atributos obrigatórios:

    • @rid - Contém identificador do elemento referenciado (obrigatório)
    • @ref-type - Especifica tipo de referência cruzada (obrigatório)
  4. Valores permitidos para @ref-type:

    • aff - Afiliação
    • app - Apêndice
    • author-notes - Notas relacionadas ao autor
    • bibr - Referência bibliográfica
    • bio - Bibliografia do autor
    • boxed-text - Caixa de texto
    • contrib - Autoria
    • corresp - Autor correspondente
    • disp-formula - Fórmula/Equação
    • fig - Figura ou grupo de figuras
    • fn - Nota
    • list - Lista ou item da lista
    • sec - Seção
    • supplementary-material - Material suplementar
    • table - Tabela ou grupo de tabelas
    • table-fn - Nota de rodapé de tabelas
  5. Correspondência @rid e @id:

    • Todo @rid obrigatoriamente deve ter @id correspondente no XML
    • Um @id pode ou não ter @rid correspondente
  6. Regra especial para transcrição:

    • <xref ref-type="sec" @rid> é obrigatório quando existe <sec sec-type="transcript">
  7. Regra especial para afiliação:

    • Para afiliação sem identificação de etiqueta no PDF: usar <xref ref-type="aff" rid="aff1"/> (self-closing)
  8. Regra de <sup>:

    • <sup> não pode abarcar <xref> quando não há caracteres textuais
    • Neste caso <sup> deve estar dentro de <xref>
  9. Menção obrigatória:

    • Para valores exceto aff, deve ocorrer menção ou etiqueta correspondente no texto

Regras a Implementar

P0 – Críticas (implementar obrigatoriamente)

# Regra Nível Descrição
1 Validar presença de @rid CRITICAL O atributo @rid é obrigatório em <xref>
2 Validar presença de @ref-type CRITICAL O atributo @ref-type é obrigatório em <xref>
3 Validar valores permitidos de @ref-type ERROR O valor de @ref-type deve estar na lista de valores permitidos
4 Validar presença de pelo menos um @ref-type="bibr" ERROR Documento deve conter pelo menos uma <xref> com @ref-type="bibr" (Critério SciELO Brasil)
5 Validar correspondência @rid e @id ERROR Todo @rid em <xref> deve ter @id correspondente no documento

P1 – Importantes (implementar se possível)

# Regra Nível Descrição
6 Validar presença de <xref> para transcrição WARNING Quando há <sec sec-type="transcript">, deve haver <xref ref-type="sec"> referenciando-a
7 Validar formato de afiliação sem label INFO Para @ref-type="aff" sem conteúdo textual, recomenda-se usar elemento self-closing <xref ... />

P2 – Futuras (fora do escopo deste Issue)

# Regra Motivo de exclusão
8 Validar que <sup> não abarca <xref> sem texto Alta complexidade - requer análise de estrutura e conteúdo textual
9 Validar presença de menção/etiqueta no texto Alta complexidade - requer análise semântica do texto
10 Validar formatação consistente de citações Baixa prioridade - formato livre permitido

Arquivos a Criar/Modificar

Avaliar existentes (podem ter validações parciais):

  • packtools/sps/models/xref.py ou similar – Verificar se modelo existe
  • packtools/sps/validation/xref.py – Verificar validações existentes
  • `packtools/sps/validation/rules/xref_rul...

📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

…pe values, bibr presence, rid-id correspondence, transcript xref, aff self-closing

Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement validations for the <xref> element Add <xref> element validations per SPS 1.10 specification Mar 19, 2026
Copilot AI requested a review from robertatakenaka March 19, 2026 12:05
Copy link
Copy Markdown
Collaborator

@Rossi-Luciano Rossi-Luciano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Evidência de revisão -- article_xref.py

Data: 2026-04-24
Revisor: Luciano
Artefatos analisados:

  • article_xref.py (validation) -- módulo de validação (ArticleXrefValidation)
  • article_xref.py (model, models/v2/) -- XMLCrossReference, Xref, Element
  • xref_rules.json -- parâmetros de configuração
  • article_xref.xml -- XML artificial gerado para cobertura de testes
  • article_xref-2026-24-04-114300-errors.csv -- relatório de erros gerado pelo packtools

Metodologia

Análise cruzada entre os dois módulos de mesmo nome (article_xref.py de validation
e de models/v2), o JSON de regras, o XML artificial construído com o princípio do
defeito isolado, e o relatório CSV. Para cada caso documentado no XML verificou-se:
se o método de validação deveria disparar o erro (leitura do código), se o CSV contém
a entrada correspondente, se o nível de severidade está correto, e se casos válidos
estão corretamente ausentes do CSV. O model foi necessário para confirmar três pontos:
(1) distinção entre all_ids() e elems_by_id("*") usados em R5 e R6, (2) cálculo
de has_text_content para R10, e (3) como elems_by_id(attribs=[...]) filtra por
atributo para R8.


Regras validadas (10 métodos)

# Método Nível configurado
R1 validate_rid_presence CRITICAL
R2 validate_ref_type_presence CRITICAL
R3 validate_ref_type_value ERROR
R4 validate_bibr_presence ERROR
R5 validate_rid_has_corresponding_id ERROR
R6 validate_xref_rid_has_corresponding_element_id ERROR
R7 validate_element_id_has_corresponding_xref_rid ERROR
R8 validate_attrib_name_and_value_has_corresponding_xref CRITICAL
R9 validate_transcript_xref WARNING
R10 validate_aff_self_closing INFO

Casos de teste no XML artificial

Caso Defeito introduzido Nível esperado Isolamento
P1 Nenhum -- caso ouro completo OK (ausente do CSV) aff1, f01, B01 com xrefs corretas
C-R1 <xref ref-type="fig"> sem @Rid CRITICAL R6 não dispara: elements_by_id[None] é não-vazio
C-R2 <xref rid="B01"> sem @ref-type CRITICAL rid="B01" existe; R5/R6 passam
C-R3 <xref ref-type="invalid-type" rid="B01"> ERROR rid="B01" existe; R5/R6 passam
C-R4* não testável neste XML ERROR requer ausência total de xref ref-type="bibr"
C-R5/R6 <xref ref-type="fig" rid="fig-inexistente"> ERROR + ERROR ambas as regras disparam para a mesma xref (cascata esperada)
C-R7 <fig id="f-orphan"> sem xref correspondente ERROR nenhuma xref com rid="f-orphan" no documento
C-R8 + C-R9a <sec sec-type="transcript" id="s-trans1"> sem xref algum CRITICAL + WARNING cascata: R8 verifica existência de xref, R9 verifica ref-type="sec"
C-R9b <sec sec-type="transcript" id="s-trans2"> + <xref ref-type="fn" rid="s-trans2"> WARNING R8 PASS (xref existe); R9 WARNING (fn != sec)
C-R10 <xref ref-type="aff" rid="aff1"></xref> (vazio) INFO has_text_content=False; aff1 ainda tem xref com texto (P1) portanto R7 passa

Verificação cruzada com o CSV

Todas as 10 entradas esperadas do módulo article_xref.py (validation) aparecem
no CSV sob o subject id and rid, com os níveis de severidade corretos:

Linha CSV Caso Nível Verificação
id and rid C-R1 CRITICAL PASS -- "Provide a valid @Rid attribute for xref"
id and rid C-R2 CRITICAL PASS -- "Provide a valid @ref-type attribute for xref"
id and rid C-R3 ERROR PASS -- "Replace invalid-type with one of the allowed values"
id and rid C-R5 ERROR PASS -- "@Rid=fig-inexistente has no corresponding @id in the document"
id and rid C-R6 ERROR PASS -- "Found xref rid=fig-inexistente, but not found the corresponding fig"
id and rid C-R7 ERROR PASS -- "Found fig id=f-orphan, but no corresponding xref was found"
id and rid C-R8 CRITICAL PASS -- "Found sec sec-type=transcript id=s-trans1, but no corresponding xref"
id and rid C-R9a WARNING PASS -- "Add xref ref-type=sec rid=s-trans1 to reference the transcript section"
id and rid C-R9b WARNING PASS -- "Add xref ref-type=sec rid=s-trans2 to reference the transcript section"
id and rid C-R10 INFO PASS -- "For ref-type=aff without text content, use self-closing"

R4 (validate_bibr_presence) não aparece no CSV (is_valid=True; xref ref-type="bibr"
presente para B01). O caso ouro P1 não gera nenhuma entrada. Não foram identificados
falsos positivos nem falsos negativos originados do módulo.


Ruído identificado (outros módulos) -- 38 entradas

Ruído estrutural (XML artificial mínimo sem abstract, history, subject etc.):
abstract ausente, history dates ausentes (received, rev-request, rev-recd, accepted, pub),
subj-group heading ausente, disp-formula/inline-formula/table-wrap ausentes, app ausente,
open science statement ausente.

Ruído de ambiente: rendition PDF ausente, DOI não registrado no Crossref, issue não
verificada no Core.

Ruído da estrutura de afiliação (aff): a afiliação de teste é mínima (apenas institution
text); o validador de aff exige label, institution content-type original/orgname/orgdiv1,
country com código ISO, addr-line com state e city (9 entradas).

Ruído de contrib: o contrib no XML não tem <n> nem role, gerando 3x CRITICAL.

Novos tipos de ruído identificados nesta sessão (incorporados ao padrão):

  1. <graphic xlink:href="...">: arquivos f01.jpg e f-orphan.jpg geram 2x CRITICAL
    "file not present in package". Para próximos XMLs: omitir <graphic> ou omitir
    o atributo xlink:href.
  2. <graphic> sem @id: 2x CRITICAL "Add id= to graphic". Adicionar @id a todos os
    elementos graphic.
  3. <graphic> sem <alt-text>: 2x WARNING accessibility. Adicionar alt-text ou
    omitir graphic.

Conclusão

O módulo article_xref.py (validation) está correto. Todas as 10 entradas
esperadas dispararam com os níveis de severidade corretos, sem falsos positivos nem
falsos negativos originados do módulo. R4 passou corretamente (bibr presente). A
regra R4 requer XML separado para teste do caso negativo (ausência total de bibr xrefs).

@robertatakenaka robertatakenaka marked this pull request as ready for review April 28, 2026 16:59
Copilot AI review requested due to automatic review settings April 28, 2026 16:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds SPS 1.10 + SciELO Brasil validations for <xref> and wires them into the XML validation orchestrator, including a new rules JSON and refactoring/bugfixes to existing cross-reference checks.

Changes:

  • Introduces 7 new <xref> validations (presence, allowed values, bibr occurrence, rid↔id correspondence, transcript/aff special rules).
  • Extends the cross-reference model (XMLCrossReference) with helpers to list all xrefs/ids and transcript sections.
  • Rewrites/expands unit tests for <xref> validations and updates the orchestrator to run the new checks.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packtools/sps/validation/article_xref.py Adds new <xref> validation methods; fixes existing cross-ref validators; centralizes parent metadata.
packtools/sps/models/v2/article_xref.py Adds all_xrefs(), all_ids(), and transcript_sections() to support new validations.
packtools/sps/validation/xml_validations.py Updates orchestrator to merge rule groups and run the new <xref> validations.
packtools/sps/validation_rules/xref_rules.json Adds configurable rule levels/lists for <xref> validations.
tests/sps/validation/test_article_xref.py Replaces prior tests with broader coverage and i18n-aware response shape assertions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 374 to 379
for element_name in elements_requires_xref_rid:
for id, elems in self.xml_cross_refs.elems_by_id(element_name).items():
for id_val, elems in self.xml_cross_refs.elems_by_id(element_name).items():
for elem_data in elems:
tag = elem_data.get("tag")
xrefs = xrefs_by_rid.get(id)
xrefs = xrefs_by_rid.get(id_val)
is_valid = bool(xrefs)
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_element_id_has_corresponding_xref_rid() iterates over elems_by_id(...) keys, which include None for elements without @id. Using xrefs_by_rid.get(id_val) with id_val=None can accidentally treat unrelated <xref> elements missing @rid as a match. Consider skipping id_val when it is falsy (or filtering out None keys in elems_by_id) to avoid reporting incorrect OK results.

Copilot uses AI. Check for mistakes.
Comment on lines +449 to 455
for elem_id, elems in self.xml_cross_refs.elems_by_id(attribs=attribs).items():
for elem_data in elems:
tag = elem_data.get("tag")
xrefs = self.xrefs_by_rid.get(id)

xrefs = self.xrefs_by_rid.get(elem_id)
is_valid = bool(xrefs)
xref_xml = elem_data.get("xref_xml")
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_attrib_name_and_value_has_corresponding_xref() looks up xrefs by elem_id, but XMLCrossReference.elems_by_id(attribs=...) will include an entry with key None if a matching element (e.g., <sec sec-type="transcript">) is missing @id. In that case self.xrefs_by_rid.get(elem_id) becomes get(None) and can be satisfied by <xref> elements missing @rid, producing false positives. Consider explicitly handling the missing-@id case (e.g., yield an error about the missing @id or skip it) and ensure None is not used as a lookup key.

Copilot uses AI. Check for mistakes.
Comment on lines 38 to 39
ids = set(self.xml_cross_refs.elems_by_id("*").keys())
rids = set(self.xrefs_by_rid.keys())
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In __init__, ids and rids are built directly from elems_by_id('*').keys() and xrefs_by_rid.keys(), both of which can include None (elements without @id and xrefs without @rid). This makes missing_xrefs/missing_elems potentially contain None, which is confusing in validation output and can mask real mismatches. Consider filtering out falsy values when building these sets.

Suggested change
ids = set(self.xml_cross_refs.elems_by_id("*").keys())
rids = set(self.xrefs_by_rid.keys())
ids = {elem_id for elem_id in self.xml_cross_refs.elems_by_id("*").keys() if elem_id}
rids = {rid for rid in self.xrefs_by_rid.keys() if rid}

Copilot uses AI. Check for mistakes.
data = xref.data
data["xml"] = xref.xml
# Check if element has text content (for self-closing detection)
content = " ".join(xref_node.xpath(".//text()")).strip()
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all_xrefs() recomputes text content with xref_node.xpath('.//text()') even though Xref.data already includes a content field built from the same XPath. Reusing data["content"].strip() would avoid duplicated work and keep the source of truth for extracted xref content in one place.

Suggested change
content = " ".join(xref_node.xpath(".//text()")).strip()
content = (data.get("content") or "").strip()

Copilot uses AI. Check for mistakes.
Comment on lines +299 to +313
yield build_response(
title="xref aff self-closing",
parent=self._parent,
item="xref",
sub_item="@ref-type",
validation_type="format",
is_valid=False,
expected=f'<xref ref-type="aff" rid="{rid}"/>',
obtained=xref_data.get("xml", ""),
advice=f'For @ref-type="aff" without text content, use self-closing: <xref ref-type="aff" rid="{rid}"/>',
data=xref_data,
error_level=error_level,
advice_text='For @ref-type="aff" without text content, use self-closing: <xref ref-type="aff" rid="{rid}"/>',
advice_params={"rid": rid},
)
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_aff_self_closing() always yields is_valid=False for ref-type="aff" xrefs without text, even when the element is already serialized as self-closing. This makes the advice misleading (it asks to change to <xref .../> even when obtained already matches that format) and produces perpetual INFO findings. Consider computing is_valid based on whether the xref is already self-closing in the serialized XML (or skipping the result when it is), so the advice only triggers when a change is actually needed.

Copilot uses AI. Check for mistakes.
Comment on lines 325 to 333
elements_by_id = self.xml_cross_refs.elems_by_id("*")
for rid, xrefs in self.xrefs_by_rid.items():
for xref in xrefs:
element_data = elements_by_id.get(rid)
is_valid = bool(element_data)
element_name = xref.get("element_name")
xref_content = xref.get("content")
element_name = xref.get("elem_name")
advice = (
f'Found {xref.get("xml")}, but not found the corresponding {xref.get("elem_xml")}'
f'Found {xref.get("tag_and_attribs")}, but not found the corresponding {xref.get("elem_xml")}'
)
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xrefs_by_rid() includes a None key when an <xref> is missing @rid, and elems_by_id('*') also includes None for elements without @id. As a result, validate_xref_rid_has_corresponding_element_id() can incorrectly mark an <xref> without @rid as OK by “matching” against the None bucket. Skipping rid values that are missing/blank (or filtering None out of xrefs_by_rid before iterating) will prevent false positives.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Criar validações para o elemento <xref>

4 participants