Skip to content

gen-shacl rules: translate rules with several postcondition slots #47

Open
linkml/linkml
#3990

Description

@jdsika

Status (2026-10-13): implemented in linkml#3990 (fork #20): one sh:SPARQLConstraint per postcondition slot (option 2). Close when it merges.

What

The composed rule translation in gen-shacl (linkml#3990, _compose_rule_sparql) skips every rule whose postconditions constrain more than one slot. gen-json-schema enforces such rules in a single then.

Reproduction (linkml#3990 head c89faac)

# schema.yaml
id: https://example.org/m
name: m
prefixes:
  ex: https://example.org/m/
  linkml: https://w3id.org/linkml/
default_prefix: ex
imports: [linkml:types]
classes:
  Thing:
    attributes:
      kind: {range: string}
      status: {range: string}
      level: {range: integer}
    rules:
      - preconditions:
          slot_conditions:
            kind: {equals_string: a}
        postconditions:
          slot_conditions:
            status: {equals_string: ok}
            level: {minimum_value: 1}
# data.yaml
- {kind: a, status: ok, level: 2}
- {kind: a, status: bad, level: 0}
- {kind: b, status: bad, level: 0}
# both.yaml
plugins:
  JsonschemaValidationPlugin: {closed: true}
  linkml.validator.plugins.shacl_validation_plugin.ShaclValidationPlugin: {}

linkml-validate --config both.yaml -s schema.yaml -C Thing data.yaml

Rule 1 of class 'Thing': skipped, because its conditions match none of the translated patterns (...), and its postconditions constrain more than one slot.
[ERROR] [data.yaml/1] 'ok' was expected in /status
[ERROR] [data.yaml/1] 0 is less than the minimum of 1 in /level

Expected: SHACL reports item 1 too, with one result for ex:status (value "bad") and one for ex:level (value 0).

Why

The postconditions are one class expression: all of its slot conditions must hold. ¬(P → A ∧ B) ≡ (P ∧ ¬A) ∨ (P ∧ ¬B).

Proposed fix (choose one)

  1. One query. Combine the per-slot patterns of _postcondition_violations in a UNION, with each branch binding its own property: { BIND(<…/status> AS ?path) … } UNION { BIND(<…/level> AS ?path) … }.
    • Project SELECT DISTINCT $this ?path ?value. The current (<iri> AS ?path) is not allowed once ?path is bound in the pattern: SPARQL 1.1 §18.2.1 says "In SELECT, the variable v must not be in-scope in the graph pattern of the SELECT clause". rdflib accepts it anyway.
    • A bound IRI ?path becomes sh:resultPath (SHACL §5.3.2).
  2. One sh:SPARQLConstraint per postcondition slot, reusing the single-slot composition unchanged. The cost is several constraints with the same sh:message.

Scope

The rules example in docs/schemas/advanced.md also uses slot-level any_of (precondition) and pattern (postconditions). Neither is supported, so that example stays skipped after this change.

Acceptance criteria

  • test_compose_agrees_with_json_schema has rows with two postcondition slots, including open_world and a slot violated only by its absence.
  • There is one result per violated slot and value, with that slot's sh:resultPath.
  • Verdicts for single-slot rules are unchanged.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions