feat(gen-shacl): add compositional fallback rule converters (M1-M5)#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. |
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>
2e56a36 to
8999ad6
Compare
8442eb8 to
1ea1ea2
Compare
Summary
Adds a compositional fallback to the rules → SHACL-SPARQL converter for
rule-operator combinations outside the three named patterns (boolean guard,
presence-implies-value, exclusive value). The fallback is tried only after
the named patterns, so their output is unchanged. It translates a conjunction
of precondition slot conditions plus a single postcondition into one
SELECT $thisviolation query. The safety contract is skip, nevermis-translate: any unsupported operator makes the converter return
Noneand the rule is logged at
DEBUGand skipped.Supported combinations
equals_string/value_presence: PRESENTprecondition →required: trueFILTER NOT EXISTSon the target slotvalue_presence: ABSENTminimum_value/maximum_value(inclusive, per the LinkML metamodel) on the trigger slotFILTERbound, combinable with any supported postconditionrange_expression.slot_conditions(e.g.sun_position.elevation <= 0)_member_conditionsemitterhas_memberlist-membership: a multivalued slot must contain a member matching a nestedrange_expressionFILTER NOT EXISTSover the membersM4/M5 introduce
_member_conditions(shared inner-condition emitter) and_resolve_member_enum_ref, which resolves inner enum values against thecontainer slot's range class, so
slot_usage-specialised enums pick thecorrect permissible values.
Stack position
Base branch:
feat/shaclgen-presence-implies-value-stacked— only thefallback delta shows in the diff.
Provenance
Squashed, content-identical, from the five M1–M5 commits on
feat/shacl-rule-converters(#18:7566e30c,fac681e0,f8ea709d,25f8cd28,47769772), where the full CI matrix is green. Originalauthorship preserved.
Areas of uncertainty
Skipped (never mis-translated), documented as follow-up candidates:
range_expressionwith scalar operators resolvesvia the nested branch only.
postconditions are a conjunction, so the single-postcondition case covers
the modeled cross-parameter rules.
{minimum_value, maximum_value}keeping only one bound) is a known defectfixed in the stacked follow-up
fix/shaclgen-rule-converter-hardening,kept separate so the defect and its regression tests stay reviewable.
How was this tested?
tests/linkml/test_generators/test_shaclgen.pyon this branch:100 passed (base suites + 16 new tests; per converter: structural triple
assertions,
prepareQuerysyntax validation, pyshacl end-to-end withadvanced=True— conforming instances pass, crafted violations fail).ruff checkandruff format --checkclean on both changed files.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.