From 889500cda45e2042829032672722ea8c1faab5bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:11:43 +0000 Subject: [PATCH 1/6] Initial plan From ef9b90934905b0535fd99616b2b691103111fa69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:13:51 +0000 Subject: [PATCH 2/6] Initial analysis: existing funding_group validation structure identified Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> --- src/scielo-scholarly-data | 1 + 1 file changed, 1 insertion(+) create mode 160000 src/scielo-scholarly-data diff --git a/src/scielo-scholarly-data b/src/scielo-scholarly-data new file mode 160000 index 000000000..a2899ce8a --- /dev/null +++ b/src/scielo-scholarly-data @@ -0,0 +1 @@ +Subproject commit a2899ce8a1fa77396c516640d36686351210d606 From daab4eb75911d73a641d6a7c47fc66dc15928755 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:15:58 +0000 Subject: [PATCH 3/6] Add 7 new validation methods for funding-group SPS 1.10 compliance with comprehensive tests Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> --- packtools/sps/validation/funding_group.py | 292 ++++++++++++ src/scielo-scholarly-data | 1 - tests/sps/validation/test_funding_group.py | 490 +++++++++++++++++++++ 3 files changed, 782 insertions(+), 1 deletion(-) delete mode 160000 src/scielo-scholarly-data diff --git a/packtools/sps/validation/funding_group.py b/packtools/sps/validation/funding_group.py index e2c0d6d36..08d344138 100644 --- a/packtools/sps/validation/funding_group.py +++ b/packtools/sps/validation/funding_group.py @@ -154,3 +154,295 @@ def validate_funding_statement(self): data=statements, error_level=self.params["funding_statement_error_level"], ) + + def validate_funding_group_uniqueness(self, error_level="ERROR"): + """ + Rule 1: Validates that appears at most once in . + + According to SPS 1.10, only one is allowed per . + + Params + ------ + error_level : str, optional + The severity level of the validation error, by default "ERROR". + + Yields + ------ + dict + Validation result for funding-group uniqueness. + """ + funding_groups = self.xml_tree.xpath(".//article-meta/funding-group") + count = len(funding_groups) + + funding_data = self.funding.data + parent = { + "parent": "article", + "parent_id": None, + "parent_article_type": funding_data.get("article_type"), + "parent_lang": funding_data.get("article_lang"), + } + + is_valid = count <= 1 + advice = None + if not is_valid: + advice = f"Found {count} elements in . Only one is allowed. Merge them into a single ." + + yield build_response( + title="funding-group uniqueness", + parent=parent, + item="funding-group", + sub_item=None, + validation_type="unique", + is_valid=is_valid, + expected="At most one in ", + obtained=f"{count} element(s) found", + advice=advice, + data={"count": count}, + error_level=error_level, + ) + + def validate_funding_statement_presence(self, error_level="CRITICAL"): + """ + Rule 2: Validates that is present in . + + According to SPS 1.10, is mandatory in all cases. + + Params + ------ + error_level : str, optional + The severity level of the validation error, by default "CRITICAL". + + Yields + ------ + dict + Validation result for funding-statement presence. + """ + funding_groups = self.xml_tree.xpath(".//article-meta/funding-group") + + if not funding_groups: + # No funding-group means no validation needed + return + + funding_data = self.funding.data + parent = { + "parent": "article", + "parent_id": None, + "parent_article_type": funding_data.get("article_type"), + "parent_lang": funding_data.get("article_lang"), + } + + funding_statement = self.funding.funding_statement + is_valid = funding_statement is not None + + advice = None + if not is_valid: + advice = "Add element inside . It is mandatory according to SPS 1.10." + + yield build_response( + title="funding-statement presence", + parent=parent, + item="funding-statement", + sub_item=None, + validation_type="exist", + is_valid=is_valid, + expected=" present in ", + obtained=funding_statement if funding_statement else "None", + advice=advice, + data={"funding_statement": funding_statement}, + error_level=error_level, + ) + + def validate_funding_source_in_award_group(self, error_level="CRITICAL"): + """ + Rule 3: Validates that is present when exists. + + According to SPS 1.10, when there are institutions declared via , + is mandatory. + + Params + ------ + error_level : str, optional + The severity level of the validation error, by default "CRITICAL". + + Yields + ------ + dict + Validation results for each award-group. + """ + funding_data = self.funding.data + parent = { + "parent": "article", + "parent_id": None, + "parent_article_type": funding_data.get("article_type"), + "parent_lang": funding_data.get("article_lang"), + } + + for item in self.funding.award_groups: + funding_sources = item["funding-source"] + + is_valid = len(funding_sources) > 0 + advice = None + if not is_valid: + advice = "Add at least one element inside this . It is mandatory when exists." + + yield build_response( + title="funding-source in award-group", + parent=parent, + item="award-group", + sub_item="funding-source", + validation_type="exist", + is_valid=is_valid, + expected="At least one in ", + obtained=f"{len(funding_sources)} element(s) found", + advice=advice, + data=item, + error_level=error_level, + ) + + def validate_label_absence(self, error_level="ERROR"): + """ + Rule 5: Validates that + + + """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + results = list(validator.validate_title_absence()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "ERROR") + self.assertIn("Remove", results[0]["advice"]) + self.assertIn("", results[0]["advice"]) + + +class TestAwardIdFundingSourceConsistency(TestFundingValidationBase): + """Rule 7: Test <award-id> and <funding-source> consistency validation""" + + def test_support_without_contract_valid(self): + """Support without contract (0 award-ids) should be valid""" + xml = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>FAPESP</funding-source> + </award-group> + <funding-statement>Funded by FAPESP</funding-statement> + </funding-group> + </article-meta> + </front> + </article> + """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + results = list(validator.validate_award_id_funding_source_consistency()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_single_contract_valid(self): + """Single contract (1 award-id) for multiple sources should be valid""" + xml = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>FAPESP</funding-source> + <funding-source>CAPES</funding-source> + <award-id>04/08142-0</award-id> + </award-group> + <funding-statement>Funded by FAPESP and CAPES</funding-statement> + </funding-group> + </article-meta> + </front> + </article> + """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + results = list(validator.validate_award_id_funding_source_consistency()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_matching_quantities_valid(self): + """Matching quantities (N sources, N awards) should be valid""" + xml = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>FAPESP</funding-source> + <funding-source>CAPES</funding-source> + <award-id>04/08142-0</award-id> + <award-id>05/09876-5</award-id> + </award-group> + <funding-statement>Funded by FAPESP and CAPES</funding-statement> + </funding-group> + </article-meta> + </front> + </article> + """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + results = list(validator.validate_award_id_funding_source_consistency()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_inconsistent_quantities_warning(self): + """Inconsistent quantities should trigger warning""" + xml = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>FAPESP</funding-source> + <award-id>04/08142-0</award-id> + <award-id>05/09876-5</award-id> + <award-id>06/12345-6</award-id> + </award-group> + <funding-statement>Funded by FAPESP</funding-statement> + </funding-group> + </article-meta> + </front> + </article> + """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + results = list(validator.validate_award_id_funding_source_consistency()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "WARNING") + self.assertIn("Inconsistent quantities", results[0]["advice"]) + + +class TestCompleteValidExamples(TestFundingValidationBase): + """Test complete valid XML examples from the issue""" + + def test_example_1_funding_with_contract(self): + """Example 1: Financing with contract number""" + xml = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group> + <award-group> + <funding-source>Fundação de Amparo à Pesquisa do Estado de São Paulo (FAPESP)</funding-source> + <award-id>04/08142-0</award-id> + </award-group> + <funding-statement>This study was supported by Fundação de Amparo à Pesquisa do Estado de São Paulo (FAPESP - Grant no. 04/08142-0; São Paulo, Brazil)</funding-statement> + </funding-group> + </article-meta> + </front> + </article> + """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + + # All validations should pass + uniqueness = list(validator.validate_funding_group_uniqueness()) + statement = list(validator.validate_funding_statement_presence()) + source = list(validator.validate_funding_source_in_award_group()) + label = list(validator.validate_label_absence()) + title = list(validator.validate_title_absence()) + consistency = list(validator.validate_award_id_funding_source_consistency()) + + self.assertEqual(uniqueness[0]["response"], "OK") + self.assertEqual(statement[0]["response"], "OK") + self.assertEqual(source[0]["response"], "OK") + self.assertEqual(label[0]["response"], "OK") + self.assertEqual(title[0]["response"], "OK") + self.assertEqual(consistency[0]["response"], "OK") + + def test_example_6_negative_funding_declaration(self): + """Example 6: Negative funding declaration""" + xml = """ + <article article-type="research-article" xml:lang="en"> + <front> + <article-meta> + <funding-group> + <funding-statement>Não houve financiamento para esta publicação</funding-statement> + </funding-group> + </article-meta> + </front> + </article> + """ + xml_tree = etree.fromstring(xml) + validator = FundingGroupValidation(xml_tree, self.params) + + # Should pass all checks (no award-group means no source validation) + uniqueness = list(validator.validate_funding_group_uniqueness()) + statement = list(validator.validate_funding_statement_presence()) + source = list(validator.validate_funding_source_in_award_group()) + label = list(validator.validate_label_absence()) + title = list(validator.validate_title_absence()) + + self.assertEqual(uniqueness[0]["response"], "OK") + self.assertEqual(statement[0]["response"], "OK") + self.assertEqual(len(source), 0) # No award-group, so no validation + self.assertEqual(label[0]["response"], "OK") + self.assertEqual(title[0]["response"], "OK") + + if __name__ == "__main__": unittest.main() From fe4c35f976b71ee1ce6f823e58c74fb783187f2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:17:34 +0000 Subject: [PATCH 4/6] Fix docstrings: clarify 'descendants' vs 'children' for XPath behavior Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> --- packtools/sps/validation/funding_group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packtools/sps/validation/funding_group.py b/packtools/sps/validation/funding_group.py index 08d344138..8cfbbe0d1 100644 --- a/packtools/sps/validation/funding_group.py +++ b/packtools/sps/validation/funding_group.py @@ -301,7 +301,7 @@ def validate_funding_source_in_award_group(self, error_level="CRITICAL"): def validate_label_absence(self, error_level="ERROR"): """ - Rule 5: Validates that <label> is not present in <funding-group> or its children. + Rule 5: Validates that <label> is not present in <funding-group> or its descendants. According to SPS 1.10, <label> is not allowed inside <funding-group>. @@ -347,7 +347,7 @@ def validate_label_absence(self, error_level="ERROR"): def validate_title_absence(self, error_level="ERROR"): """ - Rule 6: Validates that <title> is not present in <funding-group> or its children. + Rule 6: Validates that <title> is not present in <funding-group> or its descendants. According to SPS 1.10, <title> is not allowed inside <funding-group>. From 24c8f45775031757ca1098cf4a43831a6d2dc783 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:18:39 +0000 Subject: [PATCH 5/6] Improve test assertions: use assertEqual for clearer failure messages Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> --- tests/sps/validation/test_funding_group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/sps/validation/test_funding_group.py b/tests/sps/validation/test_funding_group.py index 91dec40fb..cb000d375 100644 --- a/tests/sps/validation/test_funding_group.py +++ b/tests/sps/validation/test_funding_group.py @@ -274,7 +274,7 @@ def test_single_funding_group_valid(self): results = list(validator.validate_funding_group_uniqueness()) self.assertEqual(len(results), 1) - self.assertTrue(results[0]["response"] == "OK") + self.assertEqual(results[0]["response"], "OK") def test_multiple_funding_groups_invalid(self): """Multiple <funding-group> elements should be invalid""" @@ -315,7 +315,7 @@ def test_no_funding_group_valid(self): results = list(validator.validate_funding_group_uniqueness()) self.assertEqual(len(results), 1) - self.assertTrue(results[0]["response"] == "OK") + self.assertEqual(results[0]["response"], "OK") class TestFundingStatementPresence(TestFundingValidationBase): From 2c53be0a17a7249386a9abda6dc855e3eac71a50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:19:16 +0000 Subject: [PATCH 6/6] Fix typo: 'Financing' to 'Funding' in test docstring --- tests/sps/validation/test_funding_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sps/validation/test_funding_group.py b/tests/sps/validation/test_funding_group.py index cb000d375..26a68e543 100644 --- a/tests/sps/validation/test_funding_group.py +++ b/tests/sps/validation/test_funding_group.py @@ -674,7 +674,7 @@ class TestCompleteValidExamples(TestFundingValidationBase): """Test complete valid XML examples from the issue""" def test_example_1_funding_with_contract(self): - """Example 1: Financing with contract number""" + """Example 1: Funding with contract number""" xml = """ <article article-type="research-article" xml:lang="en"> <front>