sdcvalidator 4.2.1__tar.gz → 4.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/PKG-INFO +24 -2
  2. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/README.md +22 -0
  3. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/pyproject.toml +2 -2
  4. sdcvalidator-4.4.0/src/sdcvalidator/__init__.py +65 -0
  5. sdcvalidator-4.4.0/src/sdcvalidator/constants.py +162 -0
  6. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/src/sdcvalidator/converters.py +4 -2
  7. sdcvalidator-4.4.0/src/sdcvalidator/error_mapper.py +343 -0
  8. sdcvalidator-4.4.0/src/sdcvalidator/exceptions.py +63 -0
  9. sdcvalidator-4.4.0/src/sdcvalidator/instance_modifier.py +424 -0
  10. sdcvalidator-4.4.0/src/sdcvalidator/validator.py +398 -0
  11. sdcvalidator-4.4.0/src/sdcvalidator/xsd11_restriction.py +167 -0
  12. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/PKG-INFO +24 -2
  13. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/SOURCES.txt +9 -1
  14. sdcvalidator-4.4.0/tests/test_error_mapper.py +153 -0
  15. sdcvalidator-4.4.0/tests/test_instance_modifier.py +75 -0
  16. sdcvalidator-4.4.0/tests/test_recovery.py +149 -0
  17. sdcvalidator-4.4.0/tests/test_xsd11_restriction.py +204 -0
  18. sdcvalidator-4.2.1/src/sdcvalidator/__init__.py +0 -38
  19. sdcvalidator-4.2.1/src/sdcvalidator/constants.py +0 -62
  20. sdcvalidator-4.2.1/src/sdcvalidator/validator.py +0 -174
  21. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/LICENSE +0 -0
  22. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/setup.cfg +0 -0
  23. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/src/sdcvalidator/cli.py +0 -0
  24. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/src/sdcvalidator/error_classifier.py +0 -0
  25. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/src/sdcvalidator/mcp_server.py +0 -0
  26. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/src/sdcvalidator/schema_checker.py +0 -0
  27. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/dependency_links.txt +0 -0
  28. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/entry_points.txt +0 -0
  29. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/requires.txt +0 -0
  30. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/top_level.txt +0 -0
  31. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/tests/test_converters.py +0 -0
  32. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/tests/test_error_classifier.py +0 -0
  33. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/tests/test_mcp_server.py +0 -0
  34. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/tests/test_schema_checker.py +0 -0
  35. {sdcvalidator-4.2.1 → sdcvalidator-4.4.0}/tests/test_validator.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdcvalidator
3
- Version: 4.2.1
4
- Summary: SDC4 structural validator — thin wrapper over xmlschema with error classification
3
+ Version: 4.4.0
4
+ Summary: SDC4 structural validator with ExceptionalValue recovery — thin wrapper over xmlschema
5
5
  Author: Semantic Data Charter Foundation
6
6
  License-Expression: Apache-2.0
7
7
  Classifier: Development Status :: 4 - Beta
@@ -144,6 +144,24 @@ sdcvalidator-xml2json instance.xml -o output.json
144
144
  sdcvalidator-json2xml data.json schema.xsd -o output.xml
145
145
  ```
146
146
 
147
+ ## MCP Server
148
+
149
+ `sdcvalidator` ships a stdio MCP (Model Context Protocol) server so any MCP-capable agent can validate SDC4 data without importing the Python library. It implements JSON-RPC 2.0 directly over stdio — no external MCP SDK dependency.
150
+
151
+ ```bash
152
+ sdcvalidator-mcp serve --mcp
153
+ ```
154
+
155
+ ### Tools
156
+
157
+ | Tool | Purpose |
158
+ |------|---------|
159
+ | `validate_instance` | Validate an XML instance against its SDC4 XSD schema. Returns pass/fail with error count and classified errors. |
160
+ | `validate_and_report` | Validate an instance and return a detailed report with two-tier (structural vs semantic) error classification. |
161
+ | `check_schema_compliance` | Check whether an XSD schema follows SDC4 principles (restriction only, no `xsd:extension`). Does not validate instances. |
162
+
163
+ `validate_instance` and `validate_and_report` take `schema_path` and `instance_path`, plus an optional `check_compliance` boolean (default `true`). `check_schema_compliance` takes `schema_path` only.
164
+
147
165
  ## Two-Tier Error Classification
148
166
 
149
167
  | Tier | Type | Examples | Action |
@@ -157,6 +175,10 @@ SDC4 data models must use `xsd:restriction` only — never `xsd:extension`. This
157
175
 
158
176
  The validator checks this by default and rejects schemas that violate this principle.
159
177
 
178
+ ## Status
179
+
180
+ Production-ready. Available on [PyPI](https://pypi.org/project/sdcvalidator/) under Apache 2.0.
181
+
160
182
  ## License
161
183
 
162
184
  Apache License 2.0 — see [LICENSE](LICENSE).
@@ -122,6 +122,24 @@ sdcvalidator-xml2json instance.xml -o output.json
122
122
  sdcvalidator-json2xml data.json schema.xsd -o output.xml
123
123
  ```
124
124
 
125
+ ## MCP Server
126
+
127
+ `sdcvalidator` ships a stdio MCP (Model Context Protocol) server so any MCP-capable agent can validate SDC4 data without importing the Python library. It implements JSON-RPC 2.0 directly over stdio — no external MCP SDK dependency.
128
+
129
+ ```bash
130
+ sdcvalidator-mcp serve --mcp
131
+ ```
132
+
133
+ ### Tools
134
+
135
+ | Tool | Purpose |
136
+ |------|---------|
137
+ | `validate_instance` | Validate an XML instance against its SDC4 XSD schema. Returns pass/fail with error count and classified errors. |
138
+ | `validate_and_report` | Validate an instance and return a detailed report with two-tier (structural vs semantic) error classification. |
139
+ | `check_schema_compliance` | Check whether an XSD schema follows SDC4 principles (restriction only, no `xsd:extension`). Does not validate instances. |
140
+
141
+ `validate_instance` and `validate_and_report` take `schema_path` and `instance_path`, plus an optional `check_compliance` boolean (default `true`). `check_schema_compliance` takes `schema_path` only.
142
+
125
143
  ## Two-Tier Error Classification
126
144
 
127
145
  | Tier | Type | Examples | Action |
@@ -135,6 +153,10 @@ SDC4 data models must use `xsd:restriction` only — never `xsd:extension`. This
135
153
 
136
154
  The validator checks this by default and rejects schemas that violate this principle.
137
155
 
156
+ ## Status
157
+
158
+ Production-ready. Available on [PyPI](https://pypi.org/project/sdcvalidator/) under Apache 2.0.
159
+
138
160
  ## License
139
161
 
140
162
  Apache License 2.0 — see [LICENSE](LICENSE).
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sdcvalidator"
7
- version = "4.2.1"
8
- description = "SDC4 structural validator — thin wrapper over xmlschema with error classification"
7
+ version = "4.4.0"
8
+ description = "SDC4 structural validator with ExceptionalValue recovery — thin wrapper over xmlschema"
9
9
  license = "Apache-2.0"
10
10
  requires-python = ">=3.10"
11
11
  readme = "README.md"
@@ -0,0 +1,65 @@
1
+ #
2
+ # Copyright 2025 Semantic Data Charter Foundation
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ """
11
+ sdcvalidator — SDC4 structural validator with ExceptionalValue recovery.
12
+
13
+ Thin wrapper over xmlschema with two-tier error classification and the SDC4
14
+ "quarantine-and-tag" recovery pattern (ISO 21090 ExceptionalValues).
15
+ """
16
+
17
+ __version__ = "4.4.0"
18
+
19
+ # etree_tostring is re-exported from xmlschema so callers can serialize the
20
+ # ElementTree returned by validate_with_recovery() without importing xmlschema
21
+ # directly. The import location moved across xmlschema versions; try both.
22
+ try: # xmlschema >= 2.x
23
+ from xmlschema import etree_tostring
24
+ except ImportError: # pragma: no cover - fallback for older layouts
25
+ from xmlschema.etree import etree_tostring
26
+
27
+ from .validator import (
28
+ SDC4Validator,
29
+ ValidationResult,
30
+ validate_with_recovery,
31
+ )
32
+ from .error_classifier import ErrorClassifier
33
+ from .error_mapper import ErrorMapper
34
+ from .instance_modifier import InstanceModifier
35
+ from .exceptions import SDC4StructuralValidationError
36
+ from .constants import (
37
+ ErrorTier,
38
+ ExceptionalValueType,
39
+ EXCEPTIONAL_VALUE_TYPES,
40
+ )
41
+ from .schema_checker import (
42
+ validate_sdc4_schema_compliance,
43
+ assert_sdc4_schema_compliance,
44
+ SDC4SchemaValidationError,
45
+ )
46
+
47
+ # Intentionally NOT exporting XMLSchema11 — SDCStudio's fallback import handles this.
48
+
49
+ __all__ = [
50
+ "__version__",
51
+ "SDC4Validator",
52
+ "ValidationResult",
53
+ "ErrorClassifier",
54
+ "ErrorMapper",
55
+ "InstanceModifier",
56
+ "ErrorTier",
57
+ "ExceptionalValueType",
58
+ "EXCEPTIONAL_VALUE_TYPES",
59
+ "SDC4StructuralValidationError",
60
+ "validate_with_recovery",
61
+ "etree_tostring",
62
+ "validate_sdc4_schema_compliance",
63
+ "assert_sdc4_schema_compliance",
64
+ "SDC4SchemaValidationError",
65
+ ]
@@ -0,0 +1,162 @@
1
+ #
2
+ # Copyright 2025 Semantic Data Charter Foundation
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ """
11
+ Constants and type definitions for SDC4 validation.
12
+ """
13
+
14
+ from enum import Enum
15
+ from typing import Dict
16
+
17
+ # SDC4 Namespace URIs
18
+ SDC4_NAMESPACE = "https://semanticdatacharter.com/ns/sdc4/"
19
+ SDC4_META_NAMESPACE = "https://semanticdatacharter.com/ontology/sdc4-meta/"
20
+ XSD_NAMESPACE = "http://www.w3.org/2001/XMLSchema"
21
+
22
+
23
+ class ErrorTier(Enum):
24
+ """
25
+ Two-tier error classification for SDC4 validation.
26
+
27
+ STRUCTURAL (Tier 1): Unknown elements, wrong nesting, cardinality -> REJECT
28
+ SEMANTIC (Tier 2): Type errors, pattern violations, range constraints -> REPORT
29
+ """
30
+ STRUCTURAL = "structural"
31
+ SEMANTIC = "semantic"
32
+
33
+
34
+ # Data-bearing elements in SDC4 schemas
35
+ DATA_BEARING_ELEMENTS = {
36
+ 'xdstring-value',
37
+ 'xdcount-value',
38
+ 'xdquantity-value',
39
+ 'xdboolean-value',
40
+ 'xdfile-value',
41
+ 'xdlink-value',
42
+ 'xdtemporal-value',
43
+ 'xdordinal-value',
44
+ 'xdratio-value',
45
+ 'xdinterval-value',
46
+ 'xdtoken-value',
47
+ }
48
+
49
+ # Structural/metadata elements that should FAIL validation (not recoverable)
50
+ STRUCTURAL_ELEMENTS = {
51
+ 'label', # Component labels
52
+ 'act', # Audit/control/trust
53
+ 'vtb', # Valid time begin
54
+ 'vte', # Valid time end
55
+ 'tr', # Transaction time
56
+ 'modified', # Modification timestamp
57
+ 'latitude', # Geographic coordinate
58
+ 'longitude', # Geographic coordinate
59
+ 'normal-status', # Quantified metadata
60
+ 'magnitude-status', # Quantified metadata
61
+ 'accuracy_margin', # Quantified metadata
62
+ 'precision_digits', # Quantified metadata
63
+ }
64
+
65
+
66
+ class ExceptionalValueType(Enum):
67
+ """
68
+ SDC4 ExceptionalValue types based on ISO 21090 NULL Flavors.
69
+
70
+ These types indicate why data is missing or invalid. All types inherit
71
+ from ExceptionalValueType in the SDC4 reference model.
72
+ """
73
+
74
+ # Primary types for validation errors
75
+ INV = ("INV", "Invalid",
76
+ "The value as represented in the instance is not a member of the "
77
+ "set of permitted data values in the constrained value domain of a variable.")
78
+
79
+ OTH = ("OTH", "Other",
80
+ "The actual value is not a member of the permitted data values in the "
81
+ "variable (e.g., when the value of the variable is not by the coding system).")
82
+
83
+ UNC = ("UNC", "Unencoded",
84
+ "No attempt has been made to encode the information correctly but the "
85
+ "raw source information is represented, usually in free text.")
86
+
87
+ # Missing data types
88
+ NI = ("NI", "No Information",
89
+ "The value is exceptional (missing, omitted, incomplete, improper). "
90
+ "No information as to the reason for being an exceptional value is provided. "
91
+ "This is the most general exceptional value and the default.")
92
+
93
+ NA = ("NA", "Not Applicable",
94
+ "No proper value is applicable in this context (e.g., the number of "
95
+ "cigarettes smoked per day by a non-smoker subject).")
96
+
97
+ UNK = ("UNK", "Unknown",
98
+ "A proper value is applicable, but not known.")
99
+
100
+ ASKU = ("ASKU", "Asked but Unknown",
101
+ "Information was sought but not found (e.g., patient was asked but did not know).")
102
+
103
+ ASKR = ("ASKR", "Asked and Refused",
104
+ "Information was sought but refused to be provided (e.g., patient was "
105
+ "asked but refused to answer).")
106
+
107
+ NASK = ("NASK", "Not Asked",
108
+ "This information has not been sought (e.g., patient was not asked).")
109
+
110
+ NAV = ("NAV", "Not Available",
111
+ "This information is not available and the specific reason is not known.")
112
+
113
+ MSK = ("MSK", "Masked",
114
+ "There is information on this item available but it has not been provided "
115
+ "by the sender due to security, privacy or other reasons.")
116
+
117
+ # Special value types
118
+ DER = ("DER", "Derived",
119
+ "An actual value may exist, but it must be derived from the provided "
120
+ "information; usually an expression is provided directly.")
121
+
122
+ PINF = ("PINF", "Positive Infinity", "Positive infinity of numbers.")
123
+
124
+ NINF = ("NINF", "Negative Infinity", "Negative infinity of numbers.")
125
+
126
+ TRC = ("TRC", "Trace",
127
+ "The content is greater or less than zero but too small to be quantified.")
128
+
129
+ QS = ("QS", "Sufficient Quantity",
130
+ "The specific quantity is not known, but is known to non-zero and it is "
131
+ "not specified because it makes up the bulk of the material.")
132
+
133
+ def __init__(self, code: str, name: str, description: str):
134
+ self.code = code
135
+ self.ev_name = name
136
+ self.description = description
137
+
138
+ @classmethod
139
+ def from_code(cls, code: str) -> 'ExceptionalValueType':
140
+ """Get ExceptionalValueType from its code string."""
141
+ for ev_type in cls:
142
+ if ev_type.code == code:
143
+ return ev_type
144
+ raise ValueError(f"Unknown ExceptionalValue code: {code}")
145
+
146
+
147
+ # Mapping of ExceptionalValue types to their codes and names
148
+ EXCEPTIONAL_VALUE_TYPES: Dict[str, tuple] = {
149
+ ev_type.code: (ev_type.ev_name, ev_type.description)
150
+ for ev_type in ExceptionalValueType
151
+ }
152
+
153
+ # All ExceptionalValue code strings (used to detect existing EV elements)
154
+ EXCEPTIONAL_VALUE_CODES = [ev_type.code for ev_type in ExceptionalValueType]
155
+
156
+ # XPath pattern for locating data-bearing elements in SDC4 instances
157
+ XDANY_TYPE_PATTERN = r"xd(?:string|count|quantity|boolean|file|link|temporal|ordinal|ratio|interval)-value"
158
+
159
+ # Element sequence position for ExceptionalValue in XdAnyType
160
+ # Per SDC4 schema: label, act, ExceptionalValue, vtb, vte, tr, modified, latitude, longitude, ...
161
+ EXCEPTIONAL_VALUE_INSERT_AFTER = ['label', 'act']
162
+ EXCEPTIONAL_VALUE_INSERT_BEFORE = ['vtb', 'vte', 'tr', 'modified', 'latitude', 'longitude']
@@ -18,6 +18,8 @@ from xml.etree import ElementTree as ET
18
18
 
19
19
  from xmlschema import XMLSchema11
20
20
 
21
+ from .xsd11_restriction import build_xsd11_schema
22
+
21
23
 
22
24
  def xml_to_json(
23
25
  xml_path: Union[str, Path],
@@ -43,7 +45,7 @@ def xml_to_json(
43
45
  if schema_path is None:
44
46
  raise ValueError("schema_path is required for xml_to_json conversion")
45
47
 
46
- schema = XMLSchema11(str(schema_path))
48
+ schema = build_xsd11_schema(str(schema_path))
47
49
  return schema.to_dict(str(xml_path))
48
50
 
49
51
 
@@ -73,7 +75,7 @@ def json_to_xml(
73
75
  elif isinstance(json_data, str):
74
76
  json_data = json.loads(json_data)
75
77
 
76
- schema = XMLSchema11(str(schema_path))
78
+ schema = build_xsd11_schema(str(schema_path))
77
79
  element = schema.encode(json_data)
78
80
 
79
81
  output_path = Path(output_path)
@@ -0,0 +1,343 @@
1
+ #
2
+ # Copyright 2025 Semantic Data Charter Foundation
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ """
11
+ Maps XML Schema validation errors to SDC4 ExceptionalValue types.
12
+
13
+ Tiering (structural vs semantic) is delegated to ErrorClassifier so there is a
14
+ single source of truth for that decision; this module adds only the mapping
15
+ from a semantic error to the appropriate ExceptionalValue type.
16
+ """
17
+
18
+ import re
19
+ from typing import Optional, Callable, Dict, List
20
+ from xmlschema.validators.exceptions import (
21
+ XMLSchemaValidationError,
22
+ XMLSchemaDecodeError,
23
+ XMLSchemaChildrenValidationError,
24
+ )
25
+ from .constants import ExceptionalValueType, STRUCTURAL_ELEMENTS
26
+ from .error_classifier import ErrorClassifier
27
+
28
+
29
+ class ErrorMapper:
30
+ """
31
+ Maps XMLSchema validation errors to appropriate SDC4 ExceptionalValue types.
32
+
33
+ The mapper uses a rule-based system to classify errors. Rules can be customized
34
+ or extended for domain-specific requirements.
35
+ """
36
+
37
+ def __init__(self, classifier: Optional[ErrorClassifier] = None):
38
+ """
39
+ Initialize the error mapper with default rules.
40
+
41
+ :param classifier: Optional ErrorClassifier used for the structural-vs-semantic
42
+ decision. Defaults to a fresh ErrorClassifier so the tiering
43
+ logic stays identical to SDC4Validator.validate().
44
+ """
45
+ self._classifier = classifier or ErrorClassifier()
46
+ self._rules: List[tuple[Callable, ExceptionalValueType]] = []
47
+ self._register_default_rules()
48
+
49
+ def _register_default_rules(self):
50
+ """Register the default error mapping rules."""
51
+ # Order matters - more specific rules should come first
52
+
53
+ # Missing required elements/attributes
54
+ self.add_rule(
55
+ lambda err: self._is_missing_required(err),
56
+ ExceptionalValueType.NI
57
+ )
58
+
59
+ # Type violations (wrong data type, invalid format)
60
+ self.add_rule(
61
+ lambda err: self._is_type_violation(err),
62
+ ExceptionalValueType.INV
63
+ )
64
+
65
+ # Pattern, facet, or constraint violations
66
+ self.add_rule(
67
+ lambda err: self._is_constraint_violation(err),
68
+ ExceptionalValueType.INV
69
+ )
70
+
71
+ # Enumeration violations
72
+ self.add_rule(
73
+ lambda err: self._is_enumeration_violation(err),
74
+ ExceptionalValueType.OTH
75
+ )
76
+
77
+ # Unexpected elements/attributes in strict contexts
78
+ self.add_rule(
79
+ lambda err: self._is_unexpected_content(err),
80
+ ExceptionalValueType.NA
81
+ )
82
+
83
+ # Encoding/format errors
84
+ self.add_rule(
85
+ lambda err: self._is_encoding_error(err),
86
+ ExceptionalValueType.UNC
87
+ )
88
+
89
+ # Default fallback
90
+ self.add_rule(
91
+ lambda err: True, # Matches everything
92
+ ExceptionalValueType.NI
93
+ )
94
+
95
+ def add_rule(self, condition: Callable[[XMLSchemaValidationError], bool],
96
+ ev_type: ExceptionalValueType):
97
+ """
98
+ Add a custom mapping rule.
99
+
100
+ :param condition: A callable that takes an error and returns True if the rule matches.
101
+ :param ev_type: The ExceptionalValueType to return when the rule matches.
102
+ """
103
+ self._rules.append((condition, ev_type))
104
+
105
+ def map_error(self, error: XMLSchemaValidationError) -> Optional[ExceptionalValueType]:
106
+ """
107
+ Map a validation error to an ExceptionalValue type.
108
+
109
+ Only data-bearing elements (xdstring-value, xdcount-value, etc.) can receive
110
+ ExceptionalValue tags. Structural/metadata elements (label, vtb, vte, tr, etc.)
111
+ should fail validation.
112
+
113
+ :param error: The XML Schema validation error.
114
+ :return: The appropriate ExceptionalValueType, or None if this is a structural
115
+ element that should fail validation.
116
+ """
117
+ # Extract element name from the error path
118
+ element_name = self._extract_element_name(error.path)
119
+
120
+ # Check if this is a structural element - these should fail validation
121
+ if element_name and element_name in STRUCTURAL_ELEMENTS:
122
+ # Don't map to ExceptionalValue - let validation fail
123
+ return None
124
+
125
+ # For SDC4 data-bearing elements or unknown elements, map to appropriate ExceptionalValue
126
+ # Note: We're permissive here - only STRUCTURAL_ELEMENTS fail validation
127
+ # This allows the system to work with custom element names and future SDC versions
128
+ for condition, ev_type in self._rules:
129
+ if condition(error):
130
+ return ev_type
131
+
132
+ # Should never reach here due to default rule, but just in case
133
+ return ExceptionalValueType.NI
134
+
135
+ def is_structural_error(self, error: XMLSchemaValidationError) -> bool:
136
+ """
137
+ Classify an error as structural (Tier 1) vs semantic (Tier 2).
138
+
139
+ Delegated to ErrorClassifier so the tiering decision is identical to the
140
+ one used by SDC4Validator.validate().
141
+
142
+ :param error: The validation error to classify.
143
+ :return: True if structural (reject), False if semantic (quarantine).
144
+ """
145
+ return self._classifier.is_structural_error(error)
146
+
147
+ def _extract_element_name(self, xpath: Optional[str]) -> Optional[str]:
148
+ """
149
+ Extract the element name from an XPath expression.
150
+
151
+ Handles paths like:
152
+ - /DataModel/xdstring-value
153
+ - /ns:DataModel/ns:xdstring-value[1]
154
+ - //xdcount-value
155
+
156
+ :param xpath: The XPath expression from the validation error.
157
+ :return: The local element name (without namespace prefix), or None.
158
+ """
159
+ if not xpath:
160
+ return None
161
+
162
+ # Get the last path component
163
+ parts = xpath.strip('/').split('/')
164
+ if not parts:
165
+ return None
166
+
167
+ last_part = parts[-1]
168
+
169
+ # Remove namespace prefix (e.g., 'sdc4:xdstring-value' -> 'xdstring-value')
170
+ if ':' in last_part:
171
+ last_part = last_part.split(':')[-1]
172
+
173
+ # Remove predicates (e.g., 'xdstring-value[1]' -> 'xdstring-value')
174
+ if '[' in last_part:
175
+ last_part = last_part[:last_part.index('[')]
176
+
177
+ return last_part if last_part else None
178
+
179
+ # =========================================================================
180
+ # Error classification helper methods
181
+ # =========================================================================
182
+
183
+ def _is_missing_required(self, error: XMLSchemaValidationError) -> bool:
184
+ """Check if error indicates missing required element/attribute."""
185
+ if not error.reason:
186
+ return False
187
+
188
+ reason = error.reason.lower()
189
+ # Use simple substring checks where possible; regex only where needed
190
+ keywords = [
191
+ 'missing required',
192
+ 'is not complete',
193
+ ]
194
+ if any(kw in reason for kw in keywords):
195
+ return True
196
+
197
+ patterns = [
198
+ r'required \S+ is missing',
199
+ r'element \S+ is required',
200
+ r'minimum \S+ is \d+',
201
+ ]
202
+ return any(re.search(pattern, reason) for pattern in patterns)
203
+
204
+ def _is_type_violation(self, error: XMLSchemaValidationError) -> bool:
205
+ """Check if error indicates wrong data type."""
206
+ # Decode errors are always type issues
207
+ if isinstance(error, XMLSchemaDecodeError):
208
+ return True
209
+
210
+ if not error.reason:
211
+ return False
212
+
213
+ reason = error.reason.lower()
214
+ keywords = [
215
+ 'not a valid value',
216
+ 'invalid value',
217
+ 'is not valid for type',
218
+ 'cannot be converted',
219
+ 'expected type',
220
+ 'wrong type',
221
+ 'malformed',
222
+ ]
223
+ if any(kw in reason for kw in keywords):
224
+ return True
225
+
226
+ patterns = [
227
+ r'type \S+ does not match',
228
+ r'invalid\S* format',
229
+ ]
230
+ return any(re.search(pattern, reason) for pattern in patterns)
231
+
232
+ def _is_constraint_violation(self, error: XMLSchemaValidationError) -> bool:
233
+ """Check if error indicates constraint/facet violation."""
234
+ if not error.reason:
235
+ return False
236
+
237
+ reason = error.reason.lower()
238
+ keywords = [
239
+ 'does not match pattern',
240
+ 'length constraint',
241
+ 'minlength', 'maxlength',
242
+ 'mininclusive', 'maxinclusive',
243
+ 'minexclusive', 'maxexclusive',
244
+ 'totaldigits', 'fractiondigits',
245
+ 'constraint',
246
+ ]
247
+ if any(kw in reason for kw in keywords):
248
+ return True
249
+
250
+ patterns = [
251
+ r'pattern\S* not matched',
252
+ r'assertion\S* failed',
253
+ r'exceeds\S* maximum',
254
+ r'below\S* minimum',
255
+ ]
256
+ return any(re.search(pattern, reason) for pattern in patterns)
257
+
258
+ def _is_enumeration_violation(self, error: XMLSchemaValidationError) -> bool:
259
+ """Check if error indicates enumeration violation."""
260
+ if not error.reason:
261
+ return False
262
+
263
+ reason = error.reason.lower()
264
+ keywords = [
265
+ 'not in enumeration',
266
+ 'invalid enumeration',
267
+ ]
268
+ if any(kw in reason for kw in keywords):
269
+ return True
270
+
271
+ patterns = [
272
+ r'not\S* allowed value',
273
+ r'not\S* permitted value',
274
+ r'value\S* not\S* allowed',
275
+ ]
276
+ return any(re.search(pattern, reason) for pattern in patterns)
277
+
278
+ def _is_unexpected_content(self, error: XMLSchemaValidationError) -> bool:
279
+ """Check if error indicates unexpected element/attribute."""
280
+ if isinstance(error, XMLSchemaChildrenValidationError):
281
+ # Check if it's an unexpected child element
282
+ if error.invalid_tag is not None:
283
+ return True
284
+
285
+ if not error.reason:
286
+ return False
287
+
288
+ reason = error.reason.lower()
289
+ keywords = [
290
+ 'unexpected',
291
+ 'not allowed',
292
+ 'not permitted',
293
+ 'extra element',
294
+ 'unknown element',
295
+ 'not expected',
296
+ ]
297
+ return any(kw in reason for kw in keywords)
298
+
299
+ def _is_encoding_error(self, error: XMLSchemaValidationError) -> bool:
300
+ """Check if error indicates encoding/format problem."""
301
+ if not error.reason:
302
+ return False
303
+
304
+ reason = error.reason.lower()
305
+ keywords = [
306
+ 'encoding error',
307
+ 'decode error',
308
+ 'invalid character',
309
+ 'whitespace',
310
+ ]
311
+ if any(kw in reason for kw in keywords):
312
+ return True
313
+
314
+ return bool(re.search(r'character\S* not\S* allowed', reason))
315
+
316
+ def get_error_summary(self, error: XMLSchemaValidationError,
317
+ ev_type: Optional[ExceptionalValueType]) -> Dict[str, str]:
318
+ """
319
+ Generate a summary of the error mapping.
320
+
321
+ :param error: The validation error.
322
+ :param ev_type: The mapped ExceptionalValueType, or None for structural elements.
323
+ :return: A dictionary with error details.
324
+ """
325
+ if ev_type is None:
326
+ # Structural/metadata element error - no ExceptionalValue
327
+ return {
328
+ 'xpath': error.path or 'unknown',
329
+ 'error_type': type(error).__name__,
330
+ 'reason': error.reason or 'No reason provided',
331
+ 'exceptional_value_type': None,
332
+ 'exceptional_value_name': None,
333
+ 'description': 'Structural/metadata element - validation fails',
334
+ }
335
+
336
+ return {
337
+ 'xpath': error.path or 'unknown',
338
+ 'error_type': type(error).__name__,
339
+ 'reason': error.reason or 'No reason provided',
340
+ 'exceptional_value_type': ev_type.code,
341
+ 'exceptional_value_name': ev_type.ev_name,
342
+ 'description': ev_type.description,
343
+ }