feat(gen-shacl): add a compositional fallback for rule-to-SPARQL conversion - #20
Conversation
Adversarial audit findings (fallback converters, audited at stack tip incl. the hardening PR)Findings demonstrated with pyshacl end-to-end probes against the stack tip; all apply equally to the consolidated branch of #18. B1 — real bug: M4/M5 inner-slot paths resolve against the OUTER class.
B2 — real bug: recognized+unrecognized operator mixes are silently under-translated → false positives (contract violation). B3 — real bug (artifact-poisoning edge): B4 — edge case: Verified clean (attacked, held up): M2 conjunction semantics for multivalued slots (correct existential violation, deduplicated reports); M3 numeric promotion for well-typed data; M5 zero-member semantics (flagged, as "must contain a member" requires — though no test locks this in: suggest adding a zero-member violating instance); variable namespaces ( Suggest addressing B1–B3 as a follow-up commit on this stack before upstreaming; the probe scripts are reusable as regression-test seeds. |
2e56a36 to
8999ad6
Compare
8442eb8 to
1ea1ea2
Compare
…ersion The named-pattern converters only recognise whole-rule shapes, so a rule one operator away from a known pattern produced no constraint at all. This adds a compositional fallback, tried only after every named pattern has declined, that builds the query from the operators present rather than from a fixed template. Preconditions become a conjunction of graph patterns and FILTERs; the single postcondition becomes its negation. Together they select focus nodes satisfying every precondition while violating the postcondition, which is exactly the SHACL-SPARQL violation contract of SHACL 5.3.1, with $this pre-bound to the focus node. Operator support is declared per operator, and the builder returns None -- skipping the rule -- as soon as it meets one it does not handle, so an unsupported combination is never partially translated. This covers conditional-required and conditional-absent postconditions, numeric threshold preconditions, nested-object preconditions and has_member list membership, in any combination the operators allow. Numeric bounds are validated before interpolation. minimum_value and maximum_value have metamodel range Anything, so YAML strings, dates and .nan / .inf reach the generator unchanged; interpolating them raw either produced unparsable SPARQL that poisons the whole shapes graph at validation time, or -- for a date such as 2020-01-01 -- parsed as an arithmetic expression that silently never fires. Only int and finite float are rendered; anything else skips the rule. String literals are escaped rather than interpolated, and a slot carrying both minimum_value and maximum_value now yields both bounds instead of only the first. Nested and member slots resolve against the range class of their container, so an inner slot is no longer shadowed by a same-named slot on the outer class, and a range narrowed through slot_usage resolves its enum permissible values from the narrowed range. Co-authored-by: jdsika <carlo.van-driesten@vdl.digital>
8999ad6 to
e161ce7
Compare
1ea1ea2 to
59544b4
Compare
|
Mirrored upstream as linkml#3990. |
Summary
The named-pattern converters only recognise whole-rule shapes, so a rule one
operator away from a known pattern produced no constraint at all — silently.
This adds a compositional fallback, tried only after every named pattern has
declined, that builds the query from the operators actually present rather than
from a fixed template.
Preconditions become a conjunction of graph patterns and
FILTERs; the singlepostcondition becomes its negation. Together they select focus nodes that
satisfy every precondition while violating the postcondition — exactly the
SHACL-SPARQL violation contract of
SHACL §5.3.1, with
$thispre-bound to the focus node.Because the named patterns are tried first, their output is unchanged.
What it covers
required: truevalue_presence: ABSENTminimum_value/maximum_valuerange_expression.slot_conditionshas_member...in any combination the operators allow. Operator support is declared per
operator, and the builder returns
None— skipping the rule — as soon as itmeets one it does not handle, so an unsupported combination is never partially
translated.
Soundness of interpolated values
minimum_value/maximum_valuehave metamodel rangeAnything, so YAMLstrings, dates, booleans and
.nan/.infreach the generator unchanged.Interpolating them raw was unsound in two distinct ways:
"abc"produced unparsable SPARQL, which poisons the entire shapes graphat validation time — not just the offending rule.
2020-01-01parsed as the arithmetic expression2020-01-01 = 2018and so silently never fired.Only
intand finitefloatare rendered now; anything else skips the rule.String literals are escaped rather than interpolated, and a slot carrying both
minimum_valueandmaximum_valueyields both bounds instead of only thefirst.
Nested and member slots resolve against the range class of their container,
so an inner slot is no longer shadowed by a same-named slot on the outer class,
and a range narrowed through
slot_usageresolves its enum permissible valuesfrom the narrowed range.
Stack position
Important
Depends on #19. This PR is based on
feat/shaclgen-presence-implies-value-stackedand reuses the shared helpers introduced there (exact-operator detection,
induced-slot resolution, slot-URI resolution, enum
meaningresolution).Review and merge #19 first; the diff shown here is the fallback delta only.
Testing
pytest tests/linkml/test_generators/test_shaclgen.py— 134 passed(105 from #19 plus 29 here), with the full generator suite green
(1774 passed, 52 skipped, 3 xfailed). Each supported combination has SPARQL
syntax validation plus a
pyshaclend-to-end conforming / violating round-trip,and the unsound-bound cases are tested to skip while leaving the shapes graph
parseable.
Review notes
Re-cut from an earlier five-PR stack; the corrections that were previously
separate follow-up PRs are folded into the feature they correct.