Repository navigation
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. |
|
Status (head c89faac, mirrored as linkml#3990): all four findings are fixed in this PR's single commit. #21 and #22 were closed unmerged, and their fixes were folded into that commit.
Two "verified clean" statements above no longer describe the code:
|
2e56a36 to
8999ad6
Compare
8442eb8 to
1ea1ea2
Compare
8999ad6 to
e161ce7
Compare
1ea1ea2 to
59544b4
Compare
|
Mirrored upstream as linkml#3990. |
e161ce7 to
7622d70
Compare
59544b4 to
1b1c775
Compare
7622d70 to
bae4a43
Compare
1b1c775 to
d035e8c
Compare
66276ea to
1e506c2
Compare
…ersion A rule that no named pattern matches is now composed from the operators it uses: each precondition becomes SPARQL filters on $this, and the single postcondition slot the union of the ways to violate it, binding the offending value to ?value (SHACL sections 5.3.1 and 5.3.2). A condition may use value_presence, required, equals_string, equals_string_in, minimum_value, maximum_value, a range_expression on the slot's class (nested to any depth) and has_member. The semantics follow the JSON Schema generator's if/then: - presence: value_presence decides, then required, then a default: a precondition requires its slot, a postcondition unless the rule is open_world, an inner condition of a nested expression never; - value operators and range_expression apply to every value of the slot (05validation.md: "all members"), has_member to some value; - equals_string(_in) only on string, enum and boolean slots (the named patterns' range check), bounds only on the numeric datatypes of SPARQL 1.1 section 17.1, guarded with isNumeric, and only finite numbers. Any other operator, a rule with empty pre- or postconditions or several postcondition slots, skips the rule with one warning, so a rule is never partially translated. A rule skipped part-way no longer reports the notes of the abandoned translation. Co-authored-by: jdsika <carlo.van-driesten@vdl.digital>
d035e8c to
c89faac
Compare
Mirror of linkml#3990, same head. Review there; this PR tracks the fork stack.
Summary
When no named pattern from linkml#3989 matches a rule, this PR composes the rule's SHACL-SPARQL constraint from the operators it uses. Each precondition becomes filters on
$this. The single postcondition becomes the union of the ways to violate it, with the offending value bound to?value(SHACL §5.3.1, §5.3.2).With this rule, a
Thingwhoselevelis between 10 and 20 violates the constraint when itsnoteis missing, or for eachnotevalue other thanloworhigh.Supported operators (in any condition, including nested ones):
value_presence,required,equals_string,equals_string_in,minimum_value,maximum_value,range_expressionandhas_member.Anything else is skipped with a warning, as is a rule with empty pre- or postconditions or with several postcondition slots. A rule is never partially translated.
Semantics
These follow jsonschemagen's
if/then.value_presencedecides first, thenrequired, then a default:open_world;range_expressionapply to every value, as jsonschemagen does throughitems. The spec matches slot checks against "all members of the collection" (05validation.md) but doesn't define when a rule condition is satisfied, so this reading is a choice (Rules: value operators on a multivalued slot in rule conditions mean "every value" in some generators and "some value" in others #41).has_memberneeds only one value. For example, the preconditionlevels: {minimum_value: 10}holds for[12, 15]but not for[12, 3].equals_string(_in)is translated only on string, enum and boolean slots (the same check as in feat(gen-shacl): translate presence-implies-value rules to SHACL-SPARQL linkml/linkml#3989)."15" >= 10as true. Bounds are therefore guarded withisNumeric.{required: true}is composed, while{value_presence: PRESENT}matches the named pattern; the tests check both against JSON Schema. Rules that feat(gen-shacl): translate presence-implies-value rules to SHACL-SPARQL linkml/linkml#3989 skipped, such asequals_stringtogether withequals_string_in, or anABSENTguard, are now translated exactly.Differences from jsonschemagen
has_memberinside rule conditions (gen-json-schema rules: deactivated rules enforced; cardinality, has_member and all_members dropped in rule conditions; class-level multivalued ignored #42); this PR enforces it.value_presence: ABSENTinto "not all present" (gen-json-schema: multiplevalue_presence: ABSENTpostconditions in one rule compile to "not both present" instead of "neither present" linkml/linkml#3719); this PR requires each slot to be absent.equals_stringas "one of the values", with a warning. Every other rule reads it as "every value" (question 3 on feat(gen-shacl): translate presence-implies-value rules to SHACL-SPARQL linkml/linkml#3989).[]as present. In RDF an empty list leaves no triple, so the slot counts as absent.Limitations
pattern,equals_number, cardinalities, and slot-levelany_of/none_of.isNumericreturns true for literals such as"abc"^^xsd:integer, so they are still compared. The property shape reports them anyway.Testing
has_member;ABSENTconditions;sh:value;Upstream stack: linkml#3989 → linkml#3990 → linkml#3991 (docs).
Fork stack: #19 → #20 → #23 (docs) and #35 (tests).