Skip to content

feat(gen-shacl): add presence-implies-value rule pattern (stacked on #11)#19

Open
jdsika wants to merge 1 commit into
feat/shaclgen-rules-sparqlfrom
feat/shaclgen-presence-implies-value-stacked
Open

feat(gen-shacl): add presence-implies-value rule pattern (stacked on #11)#19
jdsika wants to merge 1 commit into
feat/shaclgen-rules-sparqlfrom
feat/shaclgen-presence-implies-value-stacked

Conversation

@jdsika

@jdsika jdsika commented Jul 10, 2026

Copy link
Copy Markdown

Summary

Adds the presence-implies-value rule pattern to the SHACL generator's
rules → SHACL-SPARQL converter: a LinkML rules: block whose precondition is
value_presence: PRESENT on a guard slot and whose postcondition is
equals_string / equals_string_in on a target slot is translated into a
sh:SPARQLConstraint that flags focus nodes where the guard is present but the
target does not hold one of the allowed values. Enum permissible values resolve
to their meaning IRIs; values without meaning fall back to string literals.
$this is pre-bound per W3C SHACL §5.3.1.

Example (from the modeled ENVITED-X use case): if sun_altitude is present,
daytime must be one of day / twilight
— cross-parameter consistency the
plain per-slot SHACL constraints cannot express.

Stack position

This PR is stacked on #11 (feat/shaclgen-rules-sparql, green CI), which
provides the rules → SPARQL framework (_add_rules, _rule_to_sparql, the
boolean-guard and exclusive-value patterns). The base branch of this PR is
feat/shaclgen-rules-sparql, so only the presence-implies-value delta shows in
the diff:

main
└─ feat/shaclgen-rules-sparql                      #11 (green)
   └─ feat/shaclgen-presence-implies-value-stacked  ← this PR
      └─ feat/shaclgen-compositional-rule-fallback   (follow-up)
         └─ fix/shaclgen-rule-converter-hardening     (follow-up)

Fork PRs here are review + CI artifacts (fork main mirrors upstream); the
integration branch is feat/envited-x-pipeline, and upstream submissions to
linkml/linkml follow the same sequence.

Relationship to #17 (supersedes)

#17 aimed to deliver this feature as a self-contained branch off main by
bundling a copy of the #11 framework. The assembled bundle has four defects
(found while auditing; #17's CI masked them because quality-checks failed
first and the test job never ran):

  1. The feature is dead code: the call site
    if self.emit_rules: self._add_rules(g, class_uri_with_suffix, c) in the
    class loop was lost during assembly — _add_rules is defined but never
    invoked, so no sh:sparql constraint is ever emitted. 16 of the
    branch's own tests fail locally.
  2. PresenceEnum is used in shaclgen.py but never imported (ruff F821 ×2).
  3. The test file references _RULES_SCHEMA_YAML / EX_RULES fixtures that
    were not carried over (F821 ×3).
  4. ruff format --check fails on the test file.

Instead of patching the bundle, this PR takes the known-good incremental
commit
from the consolidated branch (ea5cf575 on
feat/shacl-rule-converters, whose full-stack CI is green on #18) and stacks
it on #11. The commit content is unchanged (cherry-pick, original authorship
preserved). Recommend closing #17 in favour of this PR.

How was this tested?

Areas of uncertainty

  • An adversarial audit of this stack found two dispatch defects affecting this pattern (greedy operator dispatch dropping extra pre/postcondition operators; the boolean guard shadowing this pattern for equals_string: "true" on non-boolean slots). Both are fixed with regression tests in the stacked fix(gen-shacl): enforce operator exactness, nested-slot parity, numeric bounds #22 — fold them in when upstreaming.
  • Multivalued-target semantics (every value must be allowed) were cross-checked against LinkML's JSON Schema realization of the same rule.

Checklist

  • My code follows the contributor guidelines
  • I have added tests that prove my fix/feature works
  • Existing tests pass locally with my changes

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.

@jdsika

jdsika commented Jul 10, 2026

Copy link
Copy Markdown
Author

Adversarial audit findings (PIV converter, audited at stack tip incl. the hardening PR)

Two substantive findings, both empirically demonstrated with pyshacl end-to-end probes and cross-checked against LinkML's reference rule semantics (gen-json-schema if/then realization):

A1 — real bug: greedy dispatch drops extra pre/postcondition operators → false positives.
_rule_to_sparql dispatches to presence-implies-value whenever the precondition has value_presence: PRESENT and the postcondition has equals_string/equals_string_in — without requiring these to be the only operators set. A precondition {value_presence: PRESENT, minimum_value: 100} loses the threshold: data with temp 50 (precondition unsatisfied, rule vacuously satisfied — LinkML's own JSON-Schema realization accepts it) is flagged as violating. Dual: extra postcondition operators are dropped too (equals_string alongside equals_string_in — the _in list silently wins). This widens/narrows the rule instead of skipping — a mis-translation, not a safe skip.
Fix direction: dispatch only when the pre/post conditions set exactly the pattern's operators; otherwise fall through (the fallback PR's _scalar_filters already does this accounting per-operator — the named patterns need the same exhaustiveness check).

A2 — edge case: boolean-guard shadows PIV for equals_string: "true" on non-boolean slots.
The boolean-guard branch keeps dispatch priority but never checks that the target slot's range is boolean. A rule "if opt present, status (range string) must equal "true"" is hijacked into a boolean comparison; both status "true" (conforming) and status "false" are flagged. Pre-existing in the framework, but this PR codifies the priority order, and PIV is the handler that would translate this rule correctly.
Fix direction: gate the boolean-guard branch on the induced post-slot range being boolean.

Verified clean (attacked, held up): absent-target semantics (violation via !BOUND, matches the JSON-Schema required realization); multivalued-target ∀ semantics (matches items:{const}); multivalued guards; mixed meaning/no-meaning equals_string_in sets (matches rdflib_dumper's IRI-vs-literal convention exactly).

Test gaps (cosmetic): test_presence_implies_value_no_meaning_falls_back_to_literal's "<Manual>" not in query assertion is vacuous (an erroneous emission would be a full IRI, never matching that string); no tests for combined operators (would have caught A1) or equals_string: "true" on a non-boolean slot (would have caught A2).

These findings equally apply to the consolidated branch of #18. Suggest addressing A1/A2 as a follow-up commit on this stack before upstreaming.

@jdsika

jdsika commented Jul 11, 2026

Copy link
Copy Markdown
Author

The substantive audit findings above are resolved in #22 (fix/shaclgen-rule-converter-audit-findings, stacked on this series) — one commit, 19 regression tests, 18 of which fail on the pre-fix source. See the finding→fix table in the #22 description.

Generalise the boolean-guard SHACL-SPARQL pattern to enum-valued targets.
A rule whose precondition is `value_presence: PRESENT` on a value slot and
whose postcondition is `equals_string` / `equals_string_in` on a target
slot now emits an `sh:sparql` constraint requiring the target slot to be
present and hold one of the allowed values. Each allowed value resolves to
its enum `meaning` IRI, with a string-literal fallback.

Motivating case (aiSim environment):
- "if texture_sky_color is set, sky_model must be TextureSky"
- "if overcast_sky_illuminance is set, sky_model must be OvercastSky or
  MeasuredOvercastSky"

The existing boolean-guard (`equals_string: "true"`) and exclusive-value
patterns are unchanged; boolean guard keeps priority over the new branch.

Adds focused unit tests (enum IRI vs. literal fallback, single value vs.
set membership, message emission, SPARQL syntax) plus pyshacl end-to-end
validation for the new pattern.

(cherry picked from commit ea5cf57)
@jdsika jdsika force-pushed the feat/shaclgen-presence-implies-value-stacked branch from 2e56a36 to 8999ad6 Compare July 11, 2026 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants