validibot-shared 0.11.0__tar.gz → 0.12.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 (34) hide show
  1. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/PKG-INFO +2 -2
  2. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/pyproject.toml +2 -2
  3. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/tests/test_schematron_envelopes.py +70 -49
  4. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/tests/test_schematron_svrl.py +1 -3
  5. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/schematron/__init__.py +2 -2
  6. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/schematron/envelopes.py +52 -57
  7. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/schematron/svrl.py +3 -8
  8. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/.gitignore +0 -0
  9. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/LICENSE +0 -0
  10. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/NOTICE +0 -0
  11. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/README.md +0 -0
  12. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/tests/__init__.py +0 -0
  13. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/tests/test_energyplus_envelopes.py +0 -0
  14. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/tests/test_energyplus_models.py +0 -0
  15. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/tests/test_evidence_manifest.py +0 -0
  16. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/tests/test_fmu_envelopes.py +0 -0
  17. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/tests/test_fmu_models.py +0 -0
  18. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/tests/test_package_init.py +0 -0
  19. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/tests/test_shacl_envelopes.py +0 -0
  20. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/tests/test_validations_envelopes.py +0 -0
  21. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/__init__.py +0 -0
  22. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/energyplus/__init__.py +0 -0
  23. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/energyplus/envelopes.py +0 -0
  24. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/energyplus/models.py +0 -0
  25. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/evidence/__init__.py +0 -0
  26. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/evidence/manifest.py +0 -0
  27. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/fmu/__init__.py +0 -0
  28. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/fmu/envelopes.py +0 -0
  29. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/fmu/models.py +0 -0
  30. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/py.typed +0 -0
  31. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/shacl/__init__.py +0 -0
  32. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/shacl/envelopes.py +0 -0
  33. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/validations/__init__.py +0 -0
  34. {validibot_shared-0.11.0 → validibot_shared-0.12.0}/validibot_shared/validations/envelopes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: validibot-shared
3
- Version: 0.11.0
3
+ Version: 0.12.0
4
4
  Summary: Shared library for data interchange between Validibot and validator containers
5
5
  Project-URL: Homepage, https://validibot.com
6
6
  Project-URL: Documentation, https://docs.validibot.com
@@ -28,7 +28,7 @@ Requires-Dist: defusedxml<0.8,>=0.7.1
28
28
  Requires-Dist: pydantic<3.0,>=2.13
29
29
  Provides-Extra: dev
30
30
  Requires-Dist: pytest==9.1.1; extra == 'dev'
31
- Requires-Dist: ruff==0.15.18; extra == 'dev'
31
+ Requires-Dist: ruff==0.15.20; extra == 'dev'
32
32
  Description-Content-Type: text/markdown
33
33
 
34
34
  <div align="center">
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "validibot-shared"
7
- version = "0.11.0"
7
+ version = "0.12.0"
8
8
  description = "Shared library for data interchange between Validibot and validator containers"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -37,7 +37,7 @@ classifiers = [
37
37
  dependencies = ["pydantic>=2.13,<3.0", "defusedxml>=0.7.1,<0.8"]
38
38
 
39
39
  [project.optional-dependencies]
40
- dev = ["pytest==9.1.1", "ruff==0.15.18"]
40
+ dev = ["pytest==9.1.1", "ruff==0.15.20"]
41
41
 
42
42
  [project.urls]
43
43
  Homepage = "https://validibot.com"
@@ -1,18 +1,19 @@
1
1
  """Tests for the Schematron container contract (ADR-2026-07-01, D4b/D9).
2
2
 
3
- The Schematron validator ships an **artefact reference** (staged URI +
4
- checksums) rather than inlined rule text, and its outputs carry an
5
- ``engine_status`` failure taxonomy where ``passed`` is *tri-state* —
6
- True/False when the engine ran, ``None`` (unknown) when it could not. These
7
- tests pin those contract properties so a refactor cannot silently weaken
8
- them: the Django side and the validator backend both program against exactly
9
- this shape.
3
+ The Schematron validator ships the author's rules **inline as text** (the
4
+ SHACL ``shapes_text`` pattern the container compiles them itself), and its
5
+ outputs carry an ``engine_status`` failure taxonomy where ``passed`` is
6
+ *tri-state* — True/False when the engine ran, ``None`` (unknown) when it
7
+ could not. These tests pin those contract properties so a refactor cannot
8
+ silently weaken them: the Django side and the validator backend both program
9
+ against exactly this shape.
10
10
  """
11
11
 
12
12
  import pytest
13
13
  from pydantic import ValidationError
14
14
 
15
15
  from validibot_shared.schematron.envelopes import (
16
+ ENGINE_ERROR_RULES_INVALID,
16
17
  ENGINE_STATUS_OK,
17
18
  ENGINE_STATUS_TIMEOUT,
18
19
  SchematronFinding,
@@ -32,7 +33,13 @@ from validibot_shared.validations.envelopes import (
32
33
  DEFAULT_MAX_FINDINGS = 500
33
34
  DEFAULT_XSLT_TIMEOUT = 60
34
35
  TEST_ERROR_COUNT = 3
35
- ARTIFACT_SHA = "b" * 64
36
+ RULES_SHA = "b" * 64
37
+
38
+ SCH_SOURCE = (
39
+ '<schema xmlns="http://purl.oclc.org/dsdl/schematron">'
40
+ "<pattern><rule context='/'><assert test='true()'>ok</assert></rule>"
41
+ "</pattern></schema>"
42
+ )
36
43
 
37
44
 
38
45
  class _ValidatorStub:
@@ -45,46 +52,43 @@ class _ValidatorStub:
45
52
 
46
53
  def _inputs(**overrides) -> SchematronInputs:
47
54
  base = {
48
- "pack_id": "en16931-ubl",
49
- "pack_version": "1.3.16",
50
- "artifact_uri": "gs://bucket/run-1/pack.xslt",
51
- "artifact_sha256": ARTIFACT_SHA,
52
- "query_binding": "xslt2",
55
+ "schematron_text": SCH_SOURCE,
56
+ "schematron_sha256": RULES_SHA,
53
57
  }
54
58
  base.update(overrides)
55
59
  return SchematronInputs(**base)
56
60
 
57
61
 
58
- def test_schematron_inputs_carry_artifact_reference_and_limit_defaults():
59
- """Inputs pin the staged artefact + checksum and default the D8 limits.
62
+ def test_inputs_carry_inline_rules_and_limit_defaults():
63
+ """Inputs ship the rules inline with provenance sha and D8 defaults.
60
64
 
61
- The container MUST be able to verify what it fetched before executing —
62
- so the artefact URI and sha256 are required while the resource limits
63
- default to the ADR's table values for direct consumers.
65
+ Inline text is the whole delivery model (no staging, no artefact URIs):
66
+ the container gets everything it needs from the envelope alone, exactly
67
+ like SHACL's shapes_text.
64
68
  """
65
69
  inputs = _inputs()
66
70
 
67
- assert inputs.artifact_uri == "gs://bucket/run-1/pack.xslt"
68
- assert inputs.artifact_sha256 == ARTIFACT_SHA
71
+ assert inputs.schematron_text == SCH_SOURCE
72
+ assert inputs.schematron_sha256 == RULES_SHA
69
73
  assert inputs.max_findings == DEFAULT_MAX_FINDINGS
70
74
  assert inputs.xslt_timeout_seconds == DEFAULT_XSLT_TIMEOUT
71
75
 
72
76
 
73
- def test_schematron_inputs_require_the_artifact_fields():
74
- """Omitting the artefact reference is a validation error, not a default.
77
+ def test_inputs_require_the_rules_text():
78
+ """Omitting schematron_text is a validation error, not a default.
75
79
 
76
- An input envelope without a verifiable artefact would force the container
77
- to either refuse (good, but late) or trust an unpinned path (never).
80
+ An input envelope without rules would force the container to run
81
+ nothing and report... something. Refuse at the contract layer.
78
82
  """
79
83
  with pytest.raises(ValidationError):
80
- SchematronInputs(pack_id="x", pack_version="1")
84
+ SchematronInputs()
81
85
 
82
86
 
83
87
  def test_outputs_default_to_unknown_passed_not_false():
84
88
  """``passed`` defaults to None — unknown, not failed (D9).
85
89
 
86
90
  A default of False would make an unpopulated envelope read as "the
87
- invoice failed the rules"; None forces every consumer to distinguish
91
+ document failed the rules"; None forces every consumer to distinguish
88
92
  "engine didn't run" from "rules failed".
89
93
  """
90
94
  outputs = SchematronOutputs()
@@ -92,35 +96,46 @@ def test_outputs_default_to_unknown_passed_not_false():
92
96
  assert outputs.engine_status == ENGINE_STATUS_OK
93
97
 
94
98
 
95
- def test_engine_failure_shape_round_trips():
96
- """A timeout outputs object serializes and re-validates losslessly.
99
+ def test_engine_failure_shapes_round_trip():
100
+ """Timeout and rules-invalid outputs serialize and re-validate losslessly.
97
101
 
98
102
  The callback path deserializes output.json with this model; the D9
99
- fields must survive the JSON round trip exactly.
103
+ fields must survive the JSON round trip exactly, including the
104
+ machine hint that lets Django distinguish "your rules don't compile"
105
+ from a generic engine error.
100
106
  """
101
- outputs = SchematronOutputs(
107
+ timeout = SchematronOutputs(
102
108
  engine_status=ENGINE_STATUS_TIMEOUT,
103
109
  engine_message="Transform exceeded 60s",
104
110
  passed=None,
105
- error_count=0,
106
- )
107
- envelope = SchematronOutputEnvelope(
108
- run_id="run-1",
109
- validator={"id": "v1", "type": ValidatorType.SCHEMATRON, "version": "1"},
110
- status=ValidationStatus.FAILED_RUNTIME,
111
- timing={},
112
- outputs=outputs,
113
111
  )
114
-
115
- restored = SchematronOutputEnvelope.model_validate(
116
- envelope.model_dump(mode="json"),
112
+ invalid_rules = SchematronOutputs(
113
+ engine_status="error",
114
+ engine_error_code=ENGINE_ERROR_RULES_INVALID,
115
+ engine_message="Schematron failed to compile: unexpected element",
116
+ passed=None,
117
117
  )
118
- assert restored.outputs.engine_status == ENGINE_STATUS_TIMEOUT
119
- assert restored.outputs.passed is None
120
-
121
-
122
- def test_findings_and_rule_id_map_round_trip():
123
- """Findings keep native ids/locations and the map keeps its D2 shape."""
118
+ for outputs, status in (
119
+ (timeout, ValidationStatus.FAILED_RUNTIME),
120
+ (invalid_rules, ValidationStatus.FAILED_RUNTIME),
121
+ ):
122
+ envelope = SchematronOutputEnvelope(
123
+ run_id="run-1",
124
+ validator={"id": "v1", "type": ValidatorType.SCHEMATRON, "version": "1"},
125
+ status=status,
126
+ timing={},
127
+ outputs=outputs,
128
+ )
129
+ restored = SchematronOutputEnvelope.model_validate(
130
+ envelope.model_dump(mode="json"),
131
+ )
132
+ assert restored.outputs.engine_status == outputs.engine_status
133
+ assert restored.outputs.engine_error_code == outputs.engine_error_code
134
+ assert restored.outputs.passed is None
135
+
136
+
137
+ def test_findings_map_and_provenance_round_trip():
138
+ """Findings keep native ids/locations; provenance keeps the rules sha."""
124
139
  outputs = SchematronOutputs(
125
140
  engine_status=ENGINE_STATUS_OK,
126
141
  passed=False,
@@ -135,12 +150,17 @@ def test_findings_and_rule_id_map_round_trip():
135
150
  flag="fatal",
136
151
  ),
137
152
  ],
153
+ schematron_sha256=RULES_SHA,
154
+ query_binding="xslt2",
155
+ engine="SaxonC-HE 12.9",
138
156
  )
139
157
 
140
158
  restored = SchematronOutputs.model_validate(outputs.model_dump(mode="json"))
141
159
  assert restored.finding_rule_ids_by_severity["BR-CO-15"] == "ERROR"
142
160
  assert restored.findings[0].rule_id == "BR-CO-15"
143
161
  assert restored.findings[0].location_xpath == "/Invoice/LegalMonetaryTotal"
162
+ assert restored.schematron_sha256 == RULES_SHA
163
+ assert restored.query_binding == "xslt2"
144
164
 
145
165
 
146
166
  def test_outputs_forbid_unknown_fields():
@@ -148,9 +168,10 @@ def test_outputs_forbid_unknown_fields():
148
168
 
149
169
  Contract models must reject unknown keys so a mismatched backend/Django
150
170
  version pair surfaces as an explicit error instead of dropped data.
171
+ This also guards the 0.11 → 0.12 break: old pack_* fields are refused.
151
172
  """
152
173
  with pytest.raises(ValidationError):
153
- SchematronOutputs(engine_status="ok", not_a_field=1)
174
+ SchematronOutputs(engine_status="ok", pack_id="stale-field")
154
175
 
155
176
 
156
177
  def test_build_schematron_input_envelope_assembles_the_xml_submission():
@@ -180,4 +201,4 @@ def test_build_schematron_input_envelope_assembles_the_xml_submission():
180
201
  assert file_item.name == "submission.xml"
181
202
  assert file_item.mime_type == SupportedMimeType.APPLICATION_XML
182
203
  assert file_item.role == "primary-model"
183
- assert envelope.inputs.pack_id == "en16931-ubl"
204
+ assert envelope.inputs.schematron_text == SCH_SOURCE
@@ -64,9 +64,7 @@ def _failed_assert(
64
64
  if role:
65
65
  attrs += f' role="{role}"'
66
66
  return (
67
- f"<svrl:failed-assert{attrs}>"
68
- f"<svrl:text>{text}</svrl:text>"
69
- "</svrl:failed-assert>"
67
+ f"<svrl:failed-assert{attrs}><svrl:text>{text}</svrl:text></svrl:failed-assert>"
70
68
  )
71
69
 
72
70
 
@@ -1,8 +1,8 @@
1
1
  """Schematron Advanced validator envelopes and models."""
2
2
 
3
3
  from validibot_shared.schematron.envelopes import (
4
- ENGINE_ERROR_ARTIFACT_MISMATCH,
5
4
  ENGINE_ERROR_BACKEND_UNAVAILABLE,
5
+ ENGINE_ERROR_RULES_INVALID,
6
6
  ENGINE_STATUS_ERROR,
7
7
  ENGINE_STATUS_OK,
8
8
  ENGINE_STATUS_TIMEOUT,
@@ -26,8 +26,8 @@ from validibot_shared.schematron.svrl import (
26
26
  )
27
27
 
28
28
  __all__ = [
29
- "ENGINE_ERROR_ARTIFACT_MISMATCH",
30
29
  "ENGINE_ERROR_BACKEND_UNAVAILABLE",
30
+ "ENGINE_ERROR_RULES_INVALID",
31
31
  "ENGINE_STATUS_ERROR",
32
32
  "ENGINE_STATUS_OK",
33
33
  "ENGINE_STATUS_TIMEOUT",
@@ -2,27 +2,27 @@
2
2
  Pydantic envelopes for the Schematron Advanced validator backend.
3
3
 
4
4
  The Schematron validator is an Advanced validator — it runs in an isolated
5
- container/Cloud Run Job rather than inside the Django worker. The official
6
- rule packs (EN 16931, Peppol BIS Billing 3.0) are authored with
7
- ``queryBinding="xslt2"`` and require a Saxon engine executing rule-pack XSLT
8
- over untrusted submitted XML; that never runs next to the worker's database
9
- credentials, identity, or network (see ADR-2026-07-01, decision D4).
10
-
11
- These schemas define the contract between Django and the Schematron container:
12
-
13
- - **Input envelope**: the XML submission (as an ``InputFileItem`` URI) plus a
14
- reference to the *staged* rule-pack artefact a container-visible URI
15
- (``gs://…`` on Cloud Run, ``file://…`` for local Docker) together with the
16
- pinned checksums and query binding. Unlike SHACL, which inlines merged
17
- shapes *text*, Schematron ships an artefact reference: the compiled XSLT is
18
- large, and the container MUST fetch it by URI and verify ``artifact_sha256``
19
- before executing (D4b). The D8 resource limits ride along, already clamped
5
+ container/Cloud Run Job rather than inside the Django worker. Schematron
6
+ rules compile to XSLT (a full programming language), so author-uploaded
7
+ rules are executable code and only ever run inside the sandboxed container:
8
+ no database, no secrets, no network egress, locked-down engine
9
+ (ADR-2026-07-01, decisions D4/D8).
10
+
11
+ These schemas define the contract between Django and the Schematron
12
+ container:
13
+
14
+ - **Input envelope**: the XML submission (as an ``InputFileItem`` URI) plus
15
+ the author's Schematron rules **inline as text** exactly how SHACL
16
+ ships its merged shapes text. Django resolves the rules from the step's
17
+ ``Ruleset`` before dispatch; the container compiles them (SchXslt2
18
+ XSLT, baked into the image as fixed tooling) and runs the result over the
19
+ submission. The D8 resource limits ride along, already clamped
20
20
  Django-side; the container re-clamps defensively.
21
21
  - **Output envelope**: the parsed SVRL summary — per-severity counts, the
22
- ``finding_rule_ids_by_severity`` map, structured findings preserving native
23
- rule ids/locations — plus ``engine_status`` (D9: findings are only
24
- meaningful when the engine actually ran) and the full artefact/engine
25
- provenance (D5).
22
+ ``finding_rule_ids_by_severity`` map, structured findings preserving
23
+ native rule ids/locations — plus ``engine_status`` (D9: findings are only
24
+ meaningful when the engine actually ran) and provenance (the sha256 of
25
+ the executed rules, the detected query binding, and the engine that ran).
26
26
  """
27
27
 
28
28
  from __future__ import annotations
@@ -49,10 +49,15 @@ ENGINE_STATUS_TIMEOUT = "timeout"
49
49
 
50
50
  # Machine hints for ``SchematronOutputs.engine_error_code`` — Django maps
51
51
  # these to its reserved ``schematron.*`` finding codes.
52
- ENGINE_ERROR_ARTIFACT_MISMATCH = "artifact_mismatch"
52
+ #
53
+ # ``rules_invalid``: the author's uploaded Schematron failed to compile.
54
+ # That's a workflow-authoring problem, not a fact about the submitted
55
+ # document — the submitter's run still reads "the check couldn't run".
56
+ ENGINE_ERROR_RULES_INVALID = "rules_invalid"
53
57
  ENGINE_ERROR_BACKEND_UNAVAILABLE = "backend_unavailable"
54
58
 
55
- # Query bindings a pack may declare (mirrors the Django-side pack registry).
59
+ # Schematron query bindings (declared by the .sch root's ``queryBinding``
60
+ # attribute; the container detects and echoes it for provenance).
56
61
  QUERY_BINDING_XSLT1 = "xslt1"
57
62
  QUERY_BINDING_XSLT2 = "xslt2"
58
63
 
@@ -60,38 +65,26 @@ QUERY_BINDING_XSLT2 = "xslt2"
60
65
  class SchematronInputs(BaseModel):
61
66
  """Schematron run configuration resolved by Django for the container.
62
67
 
63
- Everything the container needs without database access: which pinned
64
- pack to run (by *staged artefact reference*, never a Django package
65
- path), the checksums to verify before executing, and the D8 resource
66
- limits (already clamped Django-side; re-clamped in the container).
68
+ Everything the container needs without database access: the author's
69
+ rules inline (the SHACL ``shapes_text`` pattern a ``.sch`` is a text
70
+ document, typically tens to hundreds of KB) and the D8 resource limits
71
+ (already clamped Django-side; re-clamped in the container).
67
72
  """
68
73
 
69
- pack_id: str = Field(description="Identifier of the vetted rule pack.")
70
- pack_version: str = Field(description="Pinned version of the rule pack.")
71
- artifact_uri: str = Field(
74
+ schematron_text: str = Field(
72
75
  description=(
73
- "Container-visible URI of the staged compiled XSLT "
74
- "(gs://… on Cloud Run, file://… for local Docker)."
76
+ "The Schematron source (.sch) to compile and run, resolved from "
77
+ "the step's Ruleset by Django. The container compiles it with "
78
+ "the SchXslt2 transpiler baked into the image."
75
79
  ),
76
80
  )
77
- artifact_sha256: str = Field(
81
+ schematron_sha256: str = Field(
82
+ default="",
78
83
  description=(
79
- "sha256 of the compiled XSLT. The container MUST verify the "
80
- "fetched artefact against this before executing (D4b)."
84
+ "sha256 of schematron_text, computed by Django at dispatch "
85
+ "the provenance identity of the rules this run executed."
81
86
  ),
82
87
  )
83
- source_sha256: str = Field(
84
- default="",
85
- description="sha256 of the pinned .sch source (provenance echo).",
86
- )
87
- query_binding: str = Field(
88
- default=QUERY_BINDING_XSLT2,
89
- description="Schematron query binding of the pack: xslt1 | xslt2.",
90
- )
91
- engine: str = Field(
92
- default="saxonc-he",
93
- description="XSLT engine the pack requires (e.g. saxonc-he).",
94
- )
95
88
 
96
89
  # ── D8 resource limits (clamped Django-side; re-clamped in container) ──
97
90
  max_input_bytes: int = Field(default=10_000_000, gt=0)
@@ -108,7 +101,7 @@ class SchematronFinding(BaseModel):
108
101
 
109
102
  Both ``svrl:failed-assert`` and ``svrl:successful-report`` entries are
110
103
  active findings (a ``<report>`` can carry a publisher-authored error).
111
- ``rule_id`` is the publisher's native identifier (``BR-CO-15``,
104
+ ``rule_id`` is the rule's native identifier (``BR-CO-15``,
112
105
  ``PEPPOL-EN16931-R010``) and becomes ``ValidationFinding.code`` in
113
106
  Django; ``location_xpath`` preserves the SVRL ``@location`` so the
114
107
  finding can point at the offending element. ``flag``/``role`` carry the
@@ -155,7 +148,7 @@ class SchematronOutputs(BaseModel):
155
148
  engine_error_code: str = Field(
156
149
  default="",
157
150
  description=(
158
- "Machine hint for the failure kind (e.g. artifact_mismatch, "
151
+ "Machine hint for the failure kind (e.g. rules_invalid, "
159
152
  "backend_unavailable); maps to Django's reserved codes."
160
153
  ),
161
154
  )
@@ -200,18 +193,21 @@ class SchematronOutputs(BaseModel):
200
193
  description="How many findings the cap suppressed (counts stay full).",
201
194
  )
202
195
 
203
- # ── Provenance of the exact executed artefact + engine (D5) ──
204
- pack_id: str = Field(default="")
205
- pack_version: str = Field(default="")
206
- pack_source_sha256: str = Field(default="")
207
- pack_artifact_sha256: str = Field(
196
+ # ── Provenance of the executed rules + engine (D5) ──
197
+ schematron_sha256: str = Field(
198
+ default="",
199
+ description="sha256 of the Schematron source that was executed.",
200
+ )
201
+ query_binding: str = Field(
208
202
  default="",
209
- description="sha256 of the compiled XSLT actually executed.",
203
+ description=(
204
+ "Query binding detected from the .sch root (xslt1/xslt2/…), "
205
+ "echoed for provenance."
206
+ ),
210
207
  )
211
- query_binding: str = Field(default="")
212
208
  engine: str = Field(
213
209
  default="",
214
- description="Engine name + version that ran, e.g. 'SaxonC-HE 12.5'.",
210
+ description="Engine name + version that ran, e.g. 'SaxonC-HE 12.9'.",
215
211
  )
216
212
 
217
213
  execution_seconds: float = Field(default=0.0, ge=0)
@@ -259,8 +255,7 @@ def build_schematron_input_envelope(
259
255
  org_id / org_name: Organization identity.
260
256
  workflow_id / step_id / step_name: Workflow context.
261
257
  submission_uri: Storage URI for the XML submission (gs:// or file://).
262
- inputs: Fully-resolved ``SchematronInputs`` (staged artefact
263
- reference + checksums + limits).
258
+ inputs: Fully-resolved ``SchematronInputs`` (inline rules + limits).
264
259
  callback_url: URL the container POSTs to on completion.
265
260
  execution_bundle_uri: Base URI for this run's bundle.
266
261
  callback_id: Idempotency key echoed back in the callback.
@@ -152,9 +152,7 @@ def parse_svrl(
152
152
  raise SvrlParseError("Empty SVRL content.")
153
153
 
154
154
  raw = (
155
- svrl_content.encode("utf-8")
156
- if isinstance(svrl_content, str)
157
- else svrl_content
155
+ svrl_content.encode("utf-8") if isinstance(svrl_content, str) else svrl_content
158
156
  )
159
157
  try:
160
158
  root = SafeET.fromstring(raw, forbid_dtd=True)
@@ -192,13 +190,10 @@ def parse_svrl(
192
190
  existing is None
193
191
  or _SEVERITY_RANK[finding.severity] < _SEVERITY_RANK[existing]
194
192
  ):
195
- summary.finding_rule_ids_by_severity[finding.rule_id] = (
196
- finding.severity
197
- )
193
+ summary.finding_rule_ids_by_severity[finding.rule_id] = finding.severity
198
194
  else:
199
195
  logger.warning(
200
- "SVRL %s finding has no @id; kept without a rule id "
201
- "(message=%r)",
196
+ "SVRL %s finding has no @id; kept without a rule id (message=%r)",
202
197
  finding.element,
203
198
  finding.message[:120],
204
199
  )