fix(gen-shacl): correct rule-converter path parity, combined bounds, and SPARQL escaping - #21
Conversation
Implement SHACL-SPARQL constraint generation for the boolean-guard pattern commonly used in conditional validation rules. When a LinkML class has rules: blocks with preconditions (value_presence: PRESENT) and postconditions (equals_string: true), the generator now emits sh:SPARQLConstraint nodes on the corresponding sh:NodeShape. Features: - New _add_rules() method translates recognised rule patterns to SPARQL - Boolean-guard pattern: if value present then flag must be true - Rule description mapped to sh:message on the constraint - Deactivated rules are skipped - Warnings emitted for bidirectional/open_world rule flags - New --emit-rules/--no-emit-rules CLI flag (default: enabled) - Full URI references in SPARQL (no PREFIX declarations needed) The generated SPARQL follows W3C SHACL Section 5 and uses the pre-bound \ variable per Section 5.3.1. Constraints are validated by pyshacl with advanced=True. Refs: linkml#2464 Signed-off-by: Carlo van Driesten <carlo.van-driesten@bmw.de>
Adversarial audit of the hardening fixes (probed at this tip)Fix claims that held under attack: top-level induced-slot parity ( C1 — real bug: the parity fix stops one hop short, and the new
C2 — edge case: C3 — real bug (pre-existing; this PR's new docstring mis-states it): C4 — cosmetic: alias-form rule keys silently diverge. A rule key written Also verified: malformed Suggest a follow-up commit on this stack for C1–C3 before upstreaming (C1's fix best lands where the nested emitters live, i.e. the fallback commit, when folding for upstream submission). |
Generalise the boolean-guard SHACL-SPARQL pattern to enum-valued targets. A rule whose precondition is `value_presence: PRESENT` on a value slot and whose postcondition is `equals_string` / `equals_string_in` on a target slot now emits an `sh:sparql` constraint requiring the target slot to be present and hold one of the allowed values. Each allowed value resolves to its enum `meaning` IRI, with a string-literal fallback. Motivating case (aiSim environment): - "if texture_sky_color is set, sky_model must be TextureSky" - "if overcast_sky_illuminance is set, sky_model must be OvercastSky or MeasuredOvercastSky" The existing boolean-guard (`equals_string: "true"`) and exclusive-value patterns are unchanged; boolean guard keeps priority over the new branch. Adds focused unit tests (enum IRI vs. literal fallback, single value vs. set membership, message emission, SPARQL syntax) plus pyshacl end-to-end validation for the new pattern. (cherry picked from commit ea5cf57)
Add a compositional fallback in _rule_to_sparql for rule-operator combinations outside the three named patterns (boolean guard, presence-implies-value, exclusive value). Tried only after the named patterns, so their output is unchanged. The fallback translates a conjunction of precondition slot conditions plus a single postcondition into one SELECT $this violation query; any unsupported operator makes the converter return None -- skip, never mis-translate. Supported combinations: - M1 conditional-required: equals_string / value_presence: PRESENT precondition + required: true postcondition (violation = FILTER NOT EXISTS on the target slot). - M2 conditional-absent: value_presence: ABSENT postcondition (violation = the forbidden slot is present). - M3 numeric threshold preconditions: minimum_value / maximum_value inclusive bounds on the trigger slot, rendered via _sparql_number. - M4 nested precondition: one hop into an inlined child object via range_expression.slot_conditions (e.g. sun_position.elevation <= 0); adds _member_conditions (shared inner-condition emitter) and _resolve_member_enum_ref, which resolves inner enum values against the container slot's range class (handles slot_usage-specialised enums). - M5 has_member list-membership: a multivalued slot must contain a member matching a nested range_expression (violation = FILTER NOT EXISTS over the members); reuses _member_conditions. Tests per converter: structural triple assertions, prepareQuery syntax validation, and pyshacl end-to-end (conforming instances pass, crafted violations fail) with advanced=True. Squashed from the five M1-M5 commits on feat/shacl-rule-converters (7566e30, fac681e, f8ea709, 25f8cd2, 4776977). Signed-off-by: Carlo van Driesten <carlo.van-driesten@vdl.digital>
…and SPARQL escaping
Review hardening for the SHACL-SPARQL rule converters. Three defects, each
with a regression test that fails before this change:
- Induced-slot parity: _slot_uri and _resolve_enum_value_ref resolved the
*base* slot, so a slot_usage override of slot_uri (or a narrowed enum range)
made the generated SPARQL query a property/enum the data never uses while
sh:path used the induced IRI. The constraint then silently never fired
(false negative). Both now resolve the induced slot for the class, matching
the sh:path logic in the main slot loop.
- Combined operators: a single precondition / member condition dispatched on
the first matching operator, so {minimum_value: X, maximum_value: Y} dropped
the lower bound and under-constrained the trigger (false positives). A shared
_scalar_filters helper now emits every recognised operator, and still returns
None -- skip, never mis-translate -- when none is recognised.
- SPARQL string escaping: an equals_string / permissible-value name containing
a double quote, backslash or newline produced invalid, unparseable SPARQL.
New _sparql_string_literal escapes per SPARQL 1.1 section 19.7.
Tests: 6 new regression tests (structural, prepareQuery syntax, and pyshacl
end-to-end) covering all three defects; full shaclgen suite green (111).
Signed-off-by: Carlo van Driesten <carlo.van-driesten@vdl.digital>
Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
(cherry picked from commit e263def)
15c4d0d to
4b2ef26
Compare
8442eb8 to
1ea1ea2
Compare
1ea1ea2 to
59544b4
Compare
|
Closing: the content of this PR is not dropped, it has been folded into the The original five-PR stack introduced two features and then corrected them in The stack has been re-cut into one clean commit per feature: Every fix from this PR is present in the re-cut branches; the tip of #23 is Two defects were found while re-cutting and are fixed in #19:
|
Summary
Review-hardening pass over the rules → SHACL-SPARQL converters. Three latent
defects, each with regression tests that fail before the fix (re-verified
on this stack: running the new tests against the pre-fix generator source
yields 5 failures):
Induced-slot parity (silent false negative).
_slot_uriand_resolve_enum_value_refresolved the base slot, so aslot_usageoverride of
slot_uri(or a narrowed enumrange) made the SPARQL bodyquery a property / enum IRI the data never uses, while
sh:pathused theinduced IRI. The constraint then silently never fired. Both now resolve
the induced slot for the class, matching the
sh:pathlogic in the mainslot loop. Affected code originates in feat(gen-shacl): generate sh:sparql constraints from LinkML rules #11 (framework) — this fix must be
folded into the framework commit when submitting upstream.
Combined operators (silent under-constraint / false positives). A
single precondition or member condition dispatched on the first matching
operator, so a bounded range
{minimum_value: X, maximum_value: Y}droppedthe lower bound. A shared
_scalar_filtershelper now emits everyrecognised operator (and still returns
None— skip, never mis-translate —when none is recognised). Affected code originates in the compositional
fallback (base PR of this one).
SPARQL string escaping (invalid / injectable output). An
equals_stringor permissible-value name containing a",\, or newlineproduced unparseable SPARQL. New
_sparql_string_literalescapes perSPARQL 1.1 §19.7.
Affects every converter that renders string terms (framework,
presence-implies-value, fallback).
Stack position
Base branch:
feat/shaclgen-compositional-rule-fallback— only the hardeningdelta shows in the diff.
Kept as a separate commit (rather than folded into the feature branches) so
the defects, their failure modes, and the regression tests remain visible to
review. When upstreaming to
linkml/linkml, fold each fix into the submissionthat introduces the affected code, as noted per defect above.
Provenance
Cherry-pick of
e263def7fromfeat/shacl-rule-converters(#18), where thefull CI matrix is green.
How was this tested?
tests/linkml/test_generators/test_shaclgen.pyon this branch:106 passed (base suites + 6 regression tests: structural,
prepareQuerysyntax, and pyshacl end-to-end — including the below-threshold case proving
the lower bound is enforced, and an end-to-end proof that a
slot_usage-overridden constraint actually fires).source, the regression tests fail (5 failures) — the tests bite.
ruff checkandruff format --checkclean on both changed files.Areas of uncertainty
range_expression/has_memberconditions) and thatvalue_presence: ABSENTcombined with another operator was still translated; both are fixed with regression tests in the stacked fix(gen-shacl): enforce operator exactness, nested-slot parity, numeric bounds #22.my_slotfor a slot namedmy slot) silently fell back to the default-prefix CURIE — also fixed in the stacked fix(gen-shacl): enforce operator exactness, nested-slot parity, numeric bounds #22 (audit C4).Checklist
AI Assistance
If you used AI tools while preparing this PR, you are still the author and responsible for understanding, verifying, and defending your submission. Please engage with reviewers personally rather than through your agent during feedback and revisions. See our AI Covenant for details.