Repository navigation
Conversation
jdsika
marked this pull request as draft
October 5, 2026 12:01
jdsika
force-pushed
the
feat/shaclgen-postcondition-range-expression
branch
from
October 5, 2026 12:11
58b7817 to
cf5a2df
Compare
jdsika
marked this pull request as ready for review
October 5, 2026 12:25
The rules-to-SHACL-SPARQL converter recognised two named patterns. This
adds the presence-implies-value pattern: a precondition asserting
`value_presence: PRESENT` on one slot, and a postcondition constraining
another slot with `equals_string` or `equals_string_in`. It reads as
"if the guard slot is present, the target slot must be present and hold
one of the allowed values", and generalises the boolean guard to
arbitrary enum values. The boolean guard becomes the case
`equals_string: "true"` of this pattern on an xsd:boolean-typed flag.
`equals_string` / `equals_string_in` compare strings. The metamodel
defines them for slots of range string; enums and types with datatype
xsd:string are treated alike, and a slot without a range takes the
schema's default_range. On an xsd:boolean-typed slot each value must be
a lexical form of xsd:boolean (XML Schema 1.1 Part 2 section 3.3.2.2:
true, false, 1, 0) and denotes that boolean. A rule applying them to
any other range is skipped with a warning: its RDF values are typed
literals or IRIs that equal no string. Built-in type names resolve
without importing linkml:types, as in the main slot loop; the built-in
name of `curie` was misspelt there and is corrected.
Values are compared with `=`, spelled out as the disjunction SPARQL 1.1
section 17.4.1.9 defines `IN` to be, so the RDF 1.1-identical plain and
xsd:string literal forms both match and booleans compare by value;
rdflib evaluates `IN` and triple-pattern constants by term identity.
COALESCE turns the type error that RDFterm-equal raises for
incomparable literals into false, so a spec-conformant engine reports
such a value instead of dropping the row. Queries are DISTINCT and
project ?path (and ?value where there is one), so each result names the
property and the offending value. A value containing a backslash
followed by "u" or "U" is split with CONCAT, since codepoint escapes are
replaced before a query is parsed.
The exclusive-value pattern accepts the precondition
`has_member: {equals_string: V}`, which states "V is one of the values".
A bare `equals_string: V` is read the same way, as before, now with a
warning on a multivalued slot: the specification applies a slot
constraint to all members of a collection. The pattern gets the same
range check and value comparison, and its maximum_cardinality > 1 form
tests HAVING on the COUNT aggregate: expressions projected by SELECT are
not visible to HAVING (SPARQL 1.1 section 18.2.4.2), so
`HAVING (?count > N)` never held and the constraint never fired.
A rule applies to all members of its class, so a class shape now
carries the rules of its ancestors and mixins, as the JSON Schema
generator applies them. Each rule is translated in the inheriting
class's context, where slot_usage may refine the slots it references.
Classes sharing a class_uri share one shape, which carries each rule
once. With `open_world: true` "the postconditions may be omitted in
instance data", so an absent target is no violation.
A rule whose conditions carry any operator beyond what a pattern
translates, or name an unknown or identifier slot, is skipped rather
than partially translated. Each problem with a rule is logged once as a
warning naming the declaring class, the rule's position and the class
shapes it affects. The metadata fields the operator accounting ignores
are derived from the metamodel. Slot resolution goes through induced
slots, so slot_usage overrides, slot_uri overrides and alias-form keys
resolve to the IRI sh:path emits.
Behaviour changes for existing schemas: the boolean guard rejects a
string "true" on an xsd:boolean flag and skips a flag whose type has
another datatype IRI (main compared with str()); exclusive-value rules
with maximum_cardinality > 1 now fire; inherited rules are now enforced
on subclass shapes; problems with rules are logged at WARNING instead of
DEBUG; rule results carry sh:resultPath and sh:value; sh:message follows
the rule's in_language or default_language.
Co-authored-by: jdsika <[email protected]>
…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 <[email protected]>
jdsika
force-pushed
the
docs/shaclgen-rule-constraints
branch
2 times, most recently
from
October 6, 2026 18:20
442dd51 to
6a78f26
Compare
Author
|
Update: reopened as a test-only follow-up on #20 (see the description). #20 (mirror of linkml#3990, head c89faac) supersedes this PR's original implementation. Its composed translation handles nested
|
jdsika
changed the base branch from
docs/shaclgen-rule-constraints
to
feat/shaclgen-compositional-rule-fallback
October 7, 2026 08:18
The composed rule translation handles a range_expression in a postcondition with the same code as in a precondition, but its tests exercised nested postconditions only in a closed world. Pin the cases specific to nested postconditions: - open_world: an omitted or empty wrapper satisfies the rule, and a present non-matching value violates it; - closed world: an empty wrapper satisfies the inner value condition, and every member of a multivalued wrapper must satisfy it; - an enum value with a meaning compares as that IRI, resolved on the range class; - a condition on the range class's identifier skips the rule. Signed-off-by: jdsika <[email protected]>
jdsika
force-pushed
the
feat/shaclgen-postcondition-range-expression
branch
from
October 7, 2026 08:20
cf5a2df to
fbc5a66
Compare
jdsika
force-pushed
the
feat/shaclgen-compositional-rule-fallback
branch
from
October 13, 2026 20:11
c89faac to
a71a8f9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Test-only follow-up to #20 (mirror of linkml#3990). #20's composed translation handles a
range_expressionpostcondition with the same code it uses for preconditions, but its tests covered nested postconditions only in a closed world. This PR pins the nested-postcondition cases:open_world, wrapper omitted or emptyopen_world, value doesn't matchmeaningThe JSON Schema cases run for the declaring class and for a subclass. Four mutants of the shared code are each killed by the new cases: the inner presence default flipped,
open_worldignored, the identifier check removed, and enummeaningignored. No code changes.History: this PR first implemented the translation itself, using an existential query form, so it had to skip multivalued wrappers and optional or multivalued inner slots. #20 now translates those cases. On this PR's original schemas, SHACL and JSON Schema agree on all 15 instances checked. The original commit is replaced by this test-only commit.
Fork stack: #19 → #20 → #35 (tests); #23 (docs) is also based on #20.