Skip to content

feat(shacl): explicit graph scopes + dataset-preserving SHACL-AF expansion - #6653

Merged
jeswr merged 4 commits into
mainfrom
feat/shacl-af-graph-scopes
Oct 5, 2026
Merged

jeswr merged 4 commits into
mainfrom
feat/shacl-af-graph-scopes

Conversation

@jeswr

@jeswr jeswr commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Requested by Jesse · project thread

Summary

This is a capability addition, not a conformance fix. SHACL-AF over a single selected data graph is valid; this adds an opt-in seam for datasets (feature shacl-af), as requested in #6614.

New public API in sparq_shacl::rules (re-exported at the crate root):

  • GraphScope::{Default, Named(Term), Union { default: bool, named: Vec<Term> }}: which asserted graphs the rule engine sees. The selected graphs are merged into the working default graph used for targets, paths, sh:condition, node expressions and SPARQL rules, so cross-graph joins work. The selected named graphs are also the only named graphs a sh:SPARQLRule body can reach (GRAPH <g> / GRAPH ?g). An unselected graph behaves like an absent one, and FROM / FROM NAMED cannot widen that set. Nothing is unioned implicitly: the default graph is added to a union only through default: true.
  • apply_rules_in_scope(data, shapes, &scope) / apply_rules_in_scope_with_model(..) -> Inference.
  • Destination::{Default, Named(Term)} + expand_dataset(data, shapes, &scope, &dest) -> DatasetExpansion { dataset, inference }: returns a copy of the whole dataset with every asserted named graph kept, including unrelated ones. Derived triples are written to the destination and deduplicated against what that graph already holds. An identical triple asserted in another graph is left where it is. A named destination that does not exist yet is appended. The iterations / capped diagnostics are returned, not hidden. Inferred triples feed later passes through the working scope whatever the destination is. The input is never mutated.

apply_rules / apply_rules_with_model / expand behave exactly as before: they now call a shared internal run_rules with no named graphs, and GraphScope::Default hands off to them. A test pins the issue's legacy example.

Out of scope: an inferred-triple budget beyond the existing MAX_ITERATIONS cap, and dataset-scoped validation (validate_with_domain, related #4293).

Closes #6614

Base gate (always required)

  • cargo build --workspace succeeds. (left to CI; ran crate-scoped)
  • cargo clippy --workspace --exclude sparq-py --all-targets -- -D warnings is clean. (left to CI; ran cargo clippy -p sparq-shacl --all-targets --all-features -- -D warnings, clean)
  • The code this PR touches is formatted (matching the surrounding committed style).
  • cargo test passes for every crate this PR touches. (cargo test -p sparq-shacl --all-features: all pass except diff_fuzz::node_adapter_excludes_nested_sh_detail, which fails locally only because the npm package @zazuko/env-node is not installed. That is environmental and unrelated to this change.)

Targeted re-evaluation (check the rows that apply to your change)

  • Public API: updated skills/shacl-validation/SKILL.md (new section, API list, feature-gate symbol list, description) and the crate README in this change, and re-ran the sparq-shacl tests.
  • SHACL (sparq-shacl): fetched the pinned W3C suite (fetch-shacl-tests.sh, b6e73695) and re-ran all w3c_* ratchets with default features and with shacl-af. Both are green.

New tests are in crates/sparq-shacl/tests/shacl_af_graph_scopes.rs (12 tests). They cover: the issue's example (legacy behaviour unchanged); default, named and explicit-union scopes, including an absent graph and an empty union; a cross-graph join over targets, path and sh:condition; allowed and disallowed GRAPH access in SPARQLRule bodies; a FROM NAMED bypass attempt; preservation of unrelated graphs and the input; default and named destinations, including merging into an existing graph; per-destination dedup; fixpoint chaining with a named destination; and cap-diagnostic propagation. The two access-boundary tests were mutation-checked: they fail if every named graph is exposed.

Ratchets and conventions

  • I did not lower any conformance / perf / coverage ratchet.
  • No hard-coded performance numbers added to markdown.
  • Follow-up / discovered work is captured as beads, not as TODO/FIXME markers. (none added)
  • If this change makes a doc statement false (in either direction), I updated that doc in the same change.

Security

  • This change does not introduce a security regression. The SPARQL-rule named-graph boundary works by passing only the selected graphs into the engine's working dataset, so a query has no handle on any other graph.

Performance check (local, before vs after main)

Non-canonical, shared-box measurements: 4 cores, other jobs running, release profile. Each binary was built from origin/main and from this branch in separate target dirs. The runs alternate main/PR to cancel drift. Noise band is the p10–p90 spread of the per-run times, relative to the median.

Workload main (median ms) PR (median ms) Δ median (Δ best) Noise band
apply_rules + expand, default graph, 3,000 focus nodes, TripleRule + SPARQLRule + a chained TripleRule (9,000 inferred triples); 15 runs × median of 3 2002.6 2010.4 +0.4% (+3.0%) ±5.7%

Verdict: no regression beyond noise. On the default-graph path, run_rules receives an empty named-graph slice, and expand_graph is the same code factored into graph_triples.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ScyGGohDhirnbLbrUSnA5n


Generated by Claude Code

…nsion (#6614)

Add an opt-in, additive SHACL-AF API (feature `shacl-af`) for datasets:
`GraphScope` (Default / Named / explicit Union) selects which asserted graphs
the rule engine sees; the selected named graphs are also the only ones a
sh:SPARQLRule body reaches via GRAPH (FROM NAMED cannot widen it).
`expand_dataset` materializes derived triples into a `Destination` (default
or a named graph, deduplicated there) while preserving every asserted named
graph, and returns the Inference diagnostics (iterations / capped).
`apply_rules` / `expand` are unchanged (GraphScope::Default delegates to them).

Capability addition, not a conformance fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScyGGohDhirnbLbrUSnA5n
@jeswr jeswr self-assigned this Oct 5, 2026
…copes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScyGGohDhirnbLbrUSnA5n
@jeswr
jeswr marked this pull request as ready for review October 5, 2026 19:34
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScyGGohDhirnbLbrUSnA5n
@jeswr

jeswr commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

🔎 Codex reviewer — gpt-6.1-sol

Automated review by the Codex reviewer (OpenAI gpt-6.1-sol via Codex CLI) of head 65348e869aa9. Scope: correctness, security, soundness and design; style nits omitted. A new head gets a fresh review.

Findings

  1. [medium] Literal destinations create datasets that cannot be reopened — crates/sparq-shacl/src/rules.rs:685
    Destination::Named accepts any Term and appends it without validation. For example, Destination::Named(Term::Literal("derived".into())) produces an invalid RDF graph name, even with no inferred triples. With mmap enabled, saving this dataset encodes the literal’s quoted representation as an IRI; reopening fails because decode_graph_name rejects that IRI, making the saved dataset unreadable.

    Restrict named destinations to NamedOrBlankNode, or make expansion fallible and reject invalid graph-name terms before constructing the output.

Verdict: Fix destination validation before merging.

A literal or triple-term destination produced a dataset whose graph name
cannot be persisted and reopened. The type now rules it out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScyGGohDhirnbLbrUSnA5n
@jeswr

jeswr commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

🔎 Codex reviewer — gpt-6.1-sol

Automated review by the Codex reviewer (OpenAI gpt-6.1-sol via Codex CLI) of head 0cc12a0b3ff1. Scope: correctness, security, soundness and design; style nits omitted. A new head gets a fresh review.

Findings

No correctness, security, soundness or design problems found.

Verdict: The PR is safe to merge as is.

jeswr commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Local ci-fast gate (GitHub Actions outage; Jesse approved local-gate merges). PR head 0cc12a0b merged with main 02be0c03:

  • clippy -D warnings (core crates, all targets): pass
  • nextest (core crates, ci profile): pass
  • doctests (core crates): pass
  • W3C SPARQL conformance 1229/1229 (ratchet 1229): pass

Squash-merging under the local-gate rule.


Generated by Claude Code

@jeswr
jeswr merged commit 7bef271 into main Oct 5, 2026
6 checks passed
@jeswr
jeswr deleted the feat/shacl-af-graph-scopes branch October 5, 2026 22:30
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.

Support explicit graph scopes and dataset-preserving SHACL-AF expansion

2 participants