Conversation
jdsika
force-pushed
the
feat/shaclgen-class-boolean-expressions
branch
2 times, most recently
from
September 24, 2026 11:55
a29acc0 to
022e25c
Compare
…ogical constraints gen-shacl dropped class-level any_of, all_of, exactly_one_of and none_of without a trace, so a class stating "a code or a name is required" or "one of two complete profiles" generated shapes that accepted everything. The metamodel maps these operators to sh:or, sh:and, sh:xone and sh:not (their exact_mappings), and SHACL Core defines them with the same semantics (SHACL 4.6). They are now emitted on the class's NodeShape: - any_of / all_of / exactly_one_of: sh:or / sh:and / sh:xone over a list of anonymous member shapes - none_of: one sh:not per member; the values of sh:not are separate constraints that all apply (SHACL 2.1.1) - a member's is_a gives sh:class, nested expressions recurse, and each slot condition gives an sh:property on the path of the slot as induced for the class, so slot_usage and attributes resolve as in the slot loop - conditions translate required, value_presence, the cardinalities, minimum/maximum_value, pattern, equals_string(_in) (as the enum renders its permissible values on an enum slot), equals_number (as an inclusive bound on both sides, so 5 matches 5.0) and range - a parameter SHACL allows once per shape (sh:minInclusive, sh:in, sh:pattern, ...) that one condition needs twice keeps its first value and moves the second into an sh:and member, so the shapes graph stays well-formed - a condition holds vacuously for an absent slot unless required, value_presence or a minimum cardinality of at least 1 says otherwise; inside none_of, at any depth, a condition that constrains values requires the slot, so that the negation does not reject absent slots, unless it decides presence itself (required, value_presence, or a maximum or exact cardinality of 0). The JSON Schema generator requires the slot in a class's own none_of for every condition that sets neither required nor value_presence - an operator whose members use anything else (has_member, slot-level boolean expressions inside a condition, a name that is not a slot, the identifier slot, equals_string on a non-string range, ...) is skipped as a whole with a warning, because dropping a member would change what the operator admits - the slot loop's range dispatch and sh:path computation move into _add_range and _slot_path so that slot conditions reuse them; the output for schemas without class expressions is unchanged The compliance tests test_class_any_of and test_class_any_of_with_required now run for SHACL through the validator's SHACL plugin instead of being skipped as incomplete. Rows stay incomplete only where an integer in a string slot is the sole violation: instances reach the shapes through python dataclasses, which coerce the value. Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
jdsika
force-pushed
the
feat/shaclgen-class-boolean-expressions
branch
from
September 24, 2026 12:04
022e25c to
3904d7d
Compare
…nkml#3855) * fix(schemaview): keep track of which schema requested each import * fix(schemaview): keep URL import keys intact in the closure
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.
Summary
Fixes # — no issue covers the class-level operators. Related: linkml#2400 reports the same gap for the slot-level
all_of/none_of/exactly_one_of, which this PR does not change.gen-shaclsilently drops class-levelany_of,all_of,exactly_one_ofandnone_of. A class that says "a code or a name is required", or "one of two complete profiles", generates a shape that accepts every instance, and nothing is logged. The metamodel itself contains one:UnitOfMeasureinunits.yaml.The metamodel already declares the target: these four slots carry
exact_mappingstosh:or,sh:and,sh:xoneandsh:not. SHACL Core defines those components with the same semantics (SHACL §4.6). This PR emits them on the class'ssh:NodeShape:any_ofsh:orover the member shapesall_ofsh:andover the member shapesexactly_one_ofsh:xoneover the member shapesnone_ofsh:notper member (the values of a single-parameter component are separate, conjunctive constraints, §2.1.1)Each member becomes an anonymous node shape:
is_agivessh:class, and nested expressions recurse. Each slot condition becomes ansh:propertyon the path of the slot as induced for the class, soslot_usageand attributes resolve exactly as in the class's own property shapes. Conditions translate as follows:required,value_presence, cardinalitiessh:minCount/sh:maxCountminimum_value/maximum_valuesh:minInclusive/sh:maxInclusiveequals_number5also matches5.0patternsh:patternequals_string/equals_string_insh:in; on an enum slot, the permissible values as_add_enumrenders them (themeaningIRI where there is one)rangeSHACL allows
sh:minInclusive,sh:maxInclusive,sh:inandsh:patternat most once per shape (§4). A condition can need one twice, for exampleminimum_valuenext toequals_number,equals_stringnext toequals_string_in, orpatternnext to a range type's own pattern. In that case the second value goes into ansh:andmember of the property shape, where it applies to the same values, and the shapes graph stays well-formed.The range dispatch and the
sh:pathcomputation of the slot loop move into_add_rangeand_slot_path, so conditions reuse them.Presence. A slot condition constrains the values that are present, so it holds when the slot is absent unless
required: true,value_presence: PRESENTor a minimum cardinality of at least 1 says otherwise. Insidenone_of, a condition that constrains values requires the slot. Without that,none_of: [{slot_conditions: {s: {equals_string: A}}}]would reject every instance withouts. The exception is a condition that decides presence itself, throughrequired,value_presence, or a maximum or exact cardinality of 0. Addingminimum_cardinality: 0or a maximum cardinality of at least 1 to such a condition never turns an accepted absent slot into a rejected one. The JSON Schema generator requires the slot for the same reason in a class's ownnone_of, for every condition that sets neitherrequirednorvalue_presence.Untranslatable input is skipped, not approximated. A member's fields are checked against the metamodel:
AnonymousClassExpressionhas exactly six semantic fields, and everything else isCommonMetadata. A slot condition's fields are checked against the table above. Some input cannot be translated:has_memberor a slot-levelany_ofinside a conditionequals_stringon a slot whose range does not hold stringsAn operator with such a member is skipped as a whole and logged as a warning that names the reason, because dropping a single member would change what
any_of/exactly_one_ofadmit.How was this tested?
tests/linkml/test_generators/test_shaclgen.pygains 56 cases:any_ofslot_usage, attributes and a slot name containing a spacenone_of, including a nestednone_of, cardinality-only conditions, and monotonicity under an added cardinalityequals_stringon an enum withmeaningequals_numberacross numeric datatypesis_ain both naming modessh:targetClassEvery end-to-end case runs pyshacl with
meta_shacl=True, so an ill-formed shapes graph fails the test. With the new call disabled, 35 of the 56 fail. The rest are cases that conform either way.tests/linkml/test_compliance/test_boolean_slot_compliance.py:test_class_any_ofandtest_class_any_of_with_requirednow run for SHACL through the validator'sShaclValidationPlugin, instead of being skipped asINCOMPLETE. That is 63 cases: all four operators, nested and not nested. With the feature disabled, 35 fail.INCOMPLETEfor SHACL only where an integer in a string slot is its sole violation. Instances reach the shapes through python dataclasses, which coerce5to"5", the reasontest_core_compliancealready gives for OWL/SHACL/ShEx. Rows that also violate the operator are checked.Refactor is output-preserving. Every schema under
tests/andexamples/(373 schemas) was generated in default mode and in native-names-with-suffix mode, and compared againstmain's generator with the new call disabled: 746 comparisons, 0 differences, 0 new errors. With the call enabled, onlymeta.yamlandunits.yamlchange;UnitOfMeasuregains itssh:or.Full suite locally, run as CI runs it (
pytest tests/linkml/ --ignore=tests/linkml/test_notebooks -m "not kroki" -n 8): 12333 passed, 2029 skipped, 12 xfailed, 0 failed.tox -e lintpasses.Areas of uncertainty
Skips are logged at
WARNING, whereas an unsupportedrulespattern is logged atDEBUG. An operator that is not translated leaves a class unconstrained, so I chose the louder level. Happy to align.Divergence from
jsonschemagenin three corners:jsonschemagenrequires the slot only in a class's ownnone_of. Anone_ofnested in another operator does not get it there, soall_of: [{none_of: [X]}]rejects an instance thatnone_of: [X]accepts. Here the rule applies at any depth, so the two read the same.jsonschemagenalso requires the slot for a condition without a value constraint ({s: {}}or cardinality-only) insidenone_of. Here such a condition is taken literally:none_of: [{s: {maximum_cardinality: 0}}]means "sis present", not "sis absent".jsonschemagendoes not translate cardinalities orrangeinside slot conditions. Here they are translated.The first looks like a
jsonschemageninconsistency rather than intended semantics. I'm happy to open a separate issue for it.Subclasses. The constraint sits on the declaring class's shape only.
SchemaView.induced_classdoes not inherit class expressions, andjsonschemagenemits them only for the declaring class. The constraint reaches instances of subclasses the SHACL way, throughsh:targetClassandrdfs:subClassOfin the data graph (§2.1.3.2). Thesh:classfrom a member'sis_aworks the same way, as it does for a slot's range.ShaclValidationPluginputs no class hierarchy in the data graph, so under it an instance of a subclass satisfies neither. Restating the constraint on every subclass shape would avoid that, but the violations would then be reported twice where the hierarchy is present.equals_numberissh:minInclusive n ; sh:maxInclusive ninside conditions, while the slot loop usessh:hasValue n.sh:hasValuecompares terms (5≠5.0) and fails for an absent slot. I left the slot loop alone.Shared
class_uri. In default naming mode, classes sharing aclass_urishare one shape, so their expressions are conjoined on it, as their property shapes already are.sh:closedcounts only the shape's directsh:propertypaths. A condition on a slot the class does not have is therefore still rejected by a closed shape, likeadditionalProperties: falsein JSON Schema.Out of scope:
slot_conditionsoutside an operator.all_of,none_of, andexactly_one_oflinkml/linkml#2400.equals_stringwithequals_string_ingives twosh:in, and a patterned range type withpatterngives twosh:pattern, both onmainalready. Thesh:andapproach used here would fix them too.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.