Add <xref> element validations per SPS 1.10 specification#1136
Add <xref> element validations per SPS 1.10 specification#1136
Conversation
…pe values, bibr presence, rid-id correspondence, transcript xref, aff self-closing Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com>
Rossi-Luciano
left a comment
There was a problem hiding this comment.
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,Elementxref_rules.json-- parâmetros de configuraçãoarticle_xref.xml-- XML artificial gerado para cobertura de testesarticle_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):
<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 atributoxlink:href.<graphic>sem @id: 2x CRITICAL "Add id= to graphic". Adicionar @id a todos os
elementos graphic.<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).
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
| 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") |
There was a problem hiding this comment.
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.
| ids = set(self.xml_cross_refs.elems_by_id("*").keys()) | ||
| rids = set(self.xrefs_by_rid.keys()) |
There was a problem hiding this comment.
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.
| 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} |
| data = xref.data | ||
| data["xml"] = xref.xml | ||
| # Check if element has text content (for self-closing detection) | ||
| content = " ".join(xref_node.xpath(".//text()")).strip() |
There was a problem hiding this comment.
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.
| content = " ".join(xref_node.xpath(".//text()")).strip() | |
| content = (data.get("content") or "").strip() |
| 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}, | ||
| ) |
There was a problem hiding this comment.
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.
| 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")}' | ||
| ) |
There was a problem hiding this comment.
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.
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) —@ridobrigatório e não-vaziovalidate_ref_type_presence(CRITICAL) —@ref-typeobrigatório e não-vaziovalidate_ref_type_value(ERROR) —@ref-typedeve ser um dos 16 valores permitidosvalidate_bibr_presence(ERROR) — ao menos um<xref ref-type="bibr">no documentovalidate_rid_has_corresponding_id(ERROR) — todo@riddeve ter@idcorrespondenteP1 – 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-closingCorrige bugs no método existente
validate_attrib_name_and_value_has_corresponding_xref()que usavaself.xref_xml(inexistente) e o builtinidem 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?
74 testes unitários:
python -m pytest tests/sps/validation/test_article_xref.py -vAlgum 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 regraspacktools/sps/models/v2/article_xref.py—all_xrefs(),all_ids(),transcript_sections()emXMLCrossReferencepacktools/sps/validation/article_xref.py— 7 novos métodos + correção de bugspacktools/sps/validation/xml_validations.py— orquestrador atualizadotests/sps/validation/test_article_xref.py— 74 testes em 14 classesScreenshots
N/A
Quais são tickets relevantes?
Referências
<xref>: Referência Cruzada<xref>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/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@ride@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:
Ocorrência:
<xref>pode aparecer zero ou mais vezes em:<article-title>,<attrib>,<contrib>,<p>,<td>,<th>,<trans-title>,<sec>,<verse-line>Obrigatoriedade (Critério SciELO Brasil):
<xref>com@ref-type="bibr"deve ocorrer pelo menos uma vez no documentoAtributos obrigatórios:
@rid- Contém identificador do elemento referenciado (obrigatório)@ref-type- Especifica tipo de referência cruzada (obrigatório)Valores permitidos para
@ref-type:aff- Afiliaçãoapp- Apêndiceauthor-notes- Notas relacionadas ao autorbibr- Referência bibliográficabio- Bibliografia do autorboxed-text- Caixa de textocontrib- Autoriacorresp- Autor correspondentedisp-formula- Fórmula/Equaçãofig- Figura ou grupo de figurasfn- Notalist- Lista ou item da listasec- Seçãosupplementary-material- Material suplementartable- Tabela ou grupo de tabelastable-fn- Nota de rodapé de tabelasCorrespondência
@ride@id:@ridobrigatoriamente deve ter@idcorrespondente no XML@idpode ou não ter@ridcorrespondenteRegra especial para transcrição:
<xref ref-type="sec" @rid>é obrigatório quando existe<sec sec-type="transcript">Regra especial para afiliação:
<xref ref-type="aff" rid="aff1"/>(self-closing)Regra de
<sup>:<sup>não pode abarcar<xref>quando não há caracteres textuais<sup>deve estar dentro de<xref>Menção obrigatória:
aff, deve ocorrer menção ou etiqueta correspondente no textoRegras a Implementar
P0 – Críticas (implementar obrigatoriamente)
@rid@ridé obrigatório em<xref>@ref-type@ref-typeé obrigatório em<xref>@ref-type@ref-typedeve estar na lista de valores permitidos@ref-type="bibr"<xref>com@ref-type="bibr"(Critério SciELO Brasil)@ride@id@ridem<xref>deve ter@idcorrespondente no documentoP1 – Importantes (implementar se possível)
<xref>para transcrição<sec sec-type="transcript">, deve haver<xref ref-type="sec">referenciando-a@ref-type="aff"sem conteúdo textual, recomenda-se usar elemento self-closing<xref ... />P2 – Futuras (fora do escopo deste Issue)
<sup>não abarca<xref>sem textoArquivos a Criar/Modificar
Avaliar existentes (podem ter validações parciais):
packtools/sps/models/xref.pyou similar – Verificar se modelo existepacktools/sps/validation/xref.py– Verificar validações existentes📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.