Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ef2172c
feat(shacl): record sh:sourceConstraintComponent IRI on every validat…
bplatz Jul 2, 2026
ded7af6
fix(json-ld): emit triples of embedded nodes that carry @id
bplatz Jul 2, 2026
b03f5c8
feat(api): SHACL validation-report core (fluree validate façade)
bplatz Jul 2, 2026
65669f3
feat(cli): fluree validate — SHACL validation reports for ledgers and…
bplatz Jul 2, 2026
dec99dc
feat(server): GET|POST /validate/*ledger — SHACL validation report en…
bplatz Jul 2, 2026
d8bba2a
chore: drop needless raw-string hashes in validate test fixtures
bplatz Jul 2, 2026
279309c
fix(shacl): inline shapes' value-set facts now visible to sh:class me…
bplatz Jul 2, 2026
723e76f
fix(cli): validate returns typed CliError::ExitCode instead of proces…
bplatz Jul 2, 2026
a8eaed0
feat(shacl): preserve RDF term fidelity for sh:value in validation re…
bplatz Jul 2, 2026
d17c583
fix(turtle): accept bare blank-node-property-list statements
bplatz Jul 2, 2026
fbe9c66
test(shacl): W3C SHACL core compliance harness (testsuite-shacl)
bplatz Jul 2, 2026
650f31d
fix(shacl): spec-shape results for node-shape constraints; strict sh:…
bplatz Jul 2, 2026
ca07049
feat(shacl): literal sh:targetNode support (focus nodes can be literals)
bplatz Jul 2, 2026
1a51874
fix(shacl): standalone targeted property shapes; sequence-first path …
bplatz Jul 2, 2026
19c1a11
fix(shacl): spec conforms semantics; and-collapse; literal/subclass t…
bplatz Jul 2, 2026
b7e0b29
fix(shacl): report-serialization fidelity and cross-crate conforms se…
bplatz Jul 5, 2026
40e7e6f
perf(shacl): memoize class-target focus nodes across shapes in validate
bplatz Jul 5, 2026
fe06437
Merge origin/main into feature/shacl-validate-cli
bplatz Jul 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "testsuite-sparql/rdf-tests"]
path = testsuite-sparql/rdf-tests
url = https://github.com/w3c/rdf-tests.git
[submodule "testsuite-shacl/data-shapes"]
path = testsuite-shacl/data-shapes
url = https://github.com/w3c/data-shapes
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ members = [
"fluree-bench-support",
"fluree-db-consensus",
]
exclude = ["testsuite-sparql", "scripts/local/load"]
exclude = ["testsuite-sparql", "testsuite-shacl", "scripts/local/load"]

[workspace.package]
version = "4.1.1"
Expand Down
2 changes: 2 additions & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [upsert](cli/upsert.md)
- [update](cli/update.md)
- [query](cli/query.md)
- [validate](cli/validate.md)
- [multi-query](cli/multi-query.md)
- [history](cli/history.md)
- [export](cli/export.md)
Expand Down Expand Up @@ -226,5 +227,6 @@
- [Dev setup](contributing/dev-setup.md)
- [Tests](contributing/tests.md)
- [W3C SPARQL compliance suite](contributing/sparql-compliance.md)
- [W3C SHACL compliance suite](contributing/shacl-compliance.md)
- [Adding tracing spans](contributing/tracing-guide.md)
- [Releasing](contributing/releasing.md)
77 changes: 77 additions & 0 deletions docs/api/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -1427,6 +1427,83 @@ curl -X POST http://localhost:8090/v1/fluree/explain/mydb \
-d '{"select":["?s"],"where":{"@id":"?s"}}'
```

### GET/POST /validate/{ledger...}

Validate the current state of a ledger (or one of its named graphs) against SHACL shapes and return a **validation report** — the HTTP surface of `fluree validate`. Unlike transaction-time enforcement, this never rejects anything; it reports every result it finds. Requires the `shacl` build feature (on by default).

**URL:**
```
GET /validate/{ledger...}
POST /validate/{ledger...}
```

**Request body** (POST; all fields optional — an empty body validates the default graph against the attached shapes):

```json
{
"graph": "http://example.org/graphs/data",
"shapes": { "...JSON-LD shapes document..." },
"shapesGraph": "http://example.org/graphs/shapes",
"includeAttached": false
}
```

- `graph` — IRI of the named data graph to validate (default: the default graph).
- `shapes` — ad-hoc shapes: a JSON-LD object/array, or a **string containing Turtle**. Ad-hoc shapes **replace** the ledger's attached shapes ("does this data conform to *these* rules?").
- `shapesGraph` — IRI of a named graph in this ledger holding the shapes (mutually exclusive with `shapes`).
- `includeAttached` — union ad-hoc shapes with the attached shapes instead of replacing them.

**Response** (negotiated via `Accept`):

- Default / `application/json` — summary envelope:

```json
{
"conforms": false,
"violations": 1,
"warnings": 0,
"infos": 0,
"shapesChecked": 1,
"results": [{
"focus_node": "http://example.org/ns/bob",
"result_path": "http://schema.org/name",
"source_shape": "http://example.org/ns/UserShape",
"source_constraint": "http://example.org/ns/name-ps",
"constraint_component": "http://www.w3.org/ns/shacl#MinCountConstraintComponent",
"severity": "http://www.w3.org/ns/shacl#Violation",
"message": "Expected at least 1 value(s) but found 0"
}]
}
```

- `application/ld+json` — a W3C `sh:ValidationReport` JSON-LD document.
- `text/turtle` — the same report as Turtle.

`result_path` is present only when the path is a single predicate; complex paths are omitted rather than misrepresented.

**Auth:** same read gate as `/query` (Bearer `can_read` on the ledger). Note that validation reads the full graph — results are **not** policy-filtered, so access to this endpoint implies read access to the data it reports on.

**Status Codes:**
- `200 OK` — report returned (conforming or not; non-conformance is not an error)
- `400 Bad Request` — malformed body, conflicting `shapes`/`shapesGraph`, unparseable shapes document
- `401 Unauthorized` — authentication required and missing
- `404 Not Found` — ledger or named graph not found (or not authorized)

**Examples:**

```bash
# Validate against the ledger's attached shapes
curl http://localhost:8090/v1/fluree/validate/mydb

# Trial ad-hoc Turtle shapes (replaces attached shapes)
curl -X POST http://localhost:8090/v1/fluree/validate/mydb \
-H "Content-Type: application/json" \
-d '{"shapes": "@prefix sh: <http://www.w3.org/ns/shacl#> . ..."}'

# W3C report as Turtle
curl http://localhost:8090/v1/fluree/validate/mydb -H "Accept: text/turtle"
```

## Nameservice Metadata

The standalone server does not expose a general-purpose `POST /nameservice/query`
Expand Down
1 change: 1 addition & 0 deletions docs/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ fluree query 'SELECT ?name WHERE { ?s <http://example.org/name> ?name }'
| [`upsert`](upsert.md) | Upsert data (insert or update existing) |
| [`update`](update.md) | Update with WHERE/DELETE/INSERT patterns |
| [`query`](query.md) | Query a ledger |
| [`validate`](validate.md) | Validate data against SHACL shapes (report) |
| [`history`](history.md) | Show change history for an entity |
| [`export`](export.md) | Export ledger data |
| [`log`](log.md) | Show commit log |
Expand Down
107 changes: 107 additions & 0 deletions docs/cli/validate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# fluree validate

Validate data against SHACL shapes and print a **validation report** —
instead of rejecting a transaction the way staging-time enforcement does,
`validate` inspects existing state (or a standalone file) and reports every
result it finds.

```bash
fluree validate [<ledger[:branch]> | <file.ttl|file.jsonld>] [options]
```

Requires the `shacl` build feature (enabled by default).

## Two modes

**Ledger mode** validates the current state of a local ledger — indexed data
plus any commits not yet indexed — against its attached shapes (or ad-hoc
shapes you supply):

```bash
fluree validate mydb # attached shapes
fluree validate mydb --shacl proposed.ttl # trial new shapes (replaces attached)
fluree validate mydb --shacl-graph http://example.org/graphs/shapes
```

**File mode** validates an RDF file with no ledger at all: the data loads
into an ephemeral in-memory ledger, the report prints, and nothing persists.
This is the recommended pre-flight for [bulk import](import.md), which
deliberately never runs SHACL:

```bash
fluree validate data.ttl --shacl shapes.ttl
fluree validate dataset.jsonld --shacl shapes.jsonld --format jsonld
fluree validate data-with-embedded-shapes.ttl
```

A file that embeds its own shapes validates against them (staging-time
enforcement is disabled during the ephemeral load, so violating data can't
be rejected before the report is produced).

## Options

| Option | Description |
|--------|-------------|
| `--graph <iri>` | Validate a named data graph instead of the default graph |
| `--shacl <file>` | Shapes file (Turtle or JSON-LD). **Replaces** the ledger's attached shapes by default |
| `--shacl-graph <iri>` | Named graph in the target ledger holding the shapes (conflicts with `--shacl`) |
| `--include-attached` | Union ad-hoc shapes with the attached shapes instead of replacing them |
| `--format <fmt>` | `table` (default, human), `jsonld`, or `turtle` (W3C `sh:ValidationReport`) |
| `--fail-on <sev>` | Exit non-zero when results at or above this severity exist: `violation` (default), `warning`, or `info` |

## Replace vs union semantics

`--shacl` answers "does this data conform to *these* rules?" — the ledger's
attached shapes are not evaluated. This makes trialing a stricter shape
predictable before you transact it. Pass `--include-attached` to evaluate
both sets together.

## Exit codes

| Code | Meaning |
|------|---------|
| 0 | Report conforms under the `--fail-on` threshold |
| 1 | Validation results at or above the threshold exist (or an operational error) |
| 2 | Usage error (bad flags/arguments) |

CI example:

```bash
fluree validate export.ttl --shacl contracts.ttl --fail-on warning \
|| { echo "source data failed SHACL pre-flight"; exit 1; }
```

## Output

`table` prints one block per result plus a summary line:

```
Violation: http://example.org/ns/bob
path: http://schema.org/name
component: MinCountConstraintComponent
message: Expected at least 1 value(s) but found 0

Conforms: false — 1 violation(s), 0 warning(s), 0 info (1 shape(s) checked)
```

`jsonld` and `turtle` emit a W3C-shaped `sh:ValidationReport` with
`sh:focusNode`, `sh:resultPath` (single-predicate paths only — complex paths
are omitted rather than misrepresented), `sh:resultSeverity`,
`sh:sourceShape`, `sh:sourceConstraintComponent`, `sh:resultMessage`, and
`sh:value`.

If the shapes source produces no shapes, the report is vacuously conforming
and a warning is printed to stderr.

## Current limitations

- The CLI validates local ledgers; for a remote server, call the
[`/validate` HTTP endpoint](../api/endpoints.md#getpost-validateledger)
directly (CLI `--remote` wiring is not yet implemented).
- Shapes from a *different* ledger must be exported to a file first.

## Related

- [Cookbook: SHACL validation](../guides/cookbook-shacl.md) — shape authoring
and transaction-time enforcement
- [import](import.md) — bulk import (SHACL-exempt by design; validate first)
94 changes: 94 additions & 0 deletions docs/contributing/shacl-compliance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# W3C SHACL Compliance Testing

The `testsuite-shacl/` crate runs the official [W3C data-shapes test
suite](https://github.com/w3c/data-shapes) (core section) against the SHACL
engine, through the same `fluree_db_api::validate` core that powers
`fluree validate` and the `/validate` HTTP endpoint.

The crate is **excluded from the workspace** — `cd testsuite-shacl/` before
running any cargo or make commands. The W3C tests are vendored as a git
submodule; on a fresh clone run:

```bash
git submodule update --init testsuite-shacl/data-shapes
```

## Quick commands (from testsuite-shacl/)

| Command | Purpose |
|---------|---------|
| `make count` | Overall pass/fail numbers |
| `make summary` | Per-category breakdown table |
| `make test-cat CAT=node` | Run one category |
| `make test-one TEST=minLength-001` | Run matching tests with full output |
| `make failures` | List failing tests with mismatch reasons |
| `make report-json` | Machine-readable report → `report.json` |
| `make show TEST=node/minLength-001` | Print a W3C test file |

To debug a single case interactively, the CLI runs the same pipeline:

```bash
fluree validate <test>.ttl --format turtle # most tests embed data+shapes
```

## How a test runs

Each `sht:Validate` entry names a data graph and a shapes graph (usually the
test file itself). The runner loads the data into an ephemeral in-memory
ledger (staging-time SHACL disabled via the config graph, so violating data
can load), validates with `ShapesSource::InlineTurtle`, and compares the
produced report against the `sh:ValidationReport` embedded in the manifest.

## Comparison semantics and deliberate leniency

Results are compared as a multiset on `sh:focusNode`, `sh:resultPath`,
`sh:resultSeverity`, `sh:sourceConstraintComponent`, and `sh:value`
(`sh:resultMessage` is never compared, per the suite's own rules). Where
exact comparison needs machinery we don't have yet, the harness is lenient:

- **Blank nodes** (focus nodes, values, complex `sh:resultPath` structures)
match as wildcards instead of by graph isomorphism, and `sh:sourceShape`
is not compared at all.
- **Fields absent from an expected result** accept any actual value.

Tightening these is future work; a pass under leniency is still meaningful
because conformance flag, result count, component IRIs, and severities must
match exactly.

## Known engine gaps the suite surfaces

Failures are expected in these areas (honest gaps, not harness bugs):

- Complex `sh:resultPath` serialization (sequence/inverse path structures)
is omitted from reports.
- `sh:equals` / `sh:lessThan` report one aggregate violation instead of one
per missing/extra value (`equals-001`, `lessThan-002`).
- `xsd:dateTime` range comparison does not implement the spec's
timezone partial order (`minInclusive-002/003`).
- `sh:property` nested on a *property* shape (validating each value against
child property shapes) is not implemented (`property-001`,
`validation-reports/shared`).
- Custom severity IRIs (`sh:severity ex:MySeverity`) collapse to
`sh:Violation` (`severity-002`).
- `sh:sparql` (the whole `sparql/` section of the suite is not wired in).
- `complex/shacl-shacl` (validating shapes against the SHACL-SHACL meta
shapes) depends on several of the above.

A few expectations are unachievable by design — Fluree's value-centric
store differs from RDF term identity:

- `4` and `4.0` are the same value in flake space, so they collapse into
one `sh:targetNode` target (`minExclusive-001` / `maxExclusive-001`);
likewise `"1"^^xsd:boolean` canonicalizes to `true`, so a spec-pedantic
`sh:uniqueLang "1"^^xsd:boolean` (which must be ignored — only the term
`true` activates the component) is indistinguishable from `true` and
fires (`uniqueLang-002`).
- Ill-formed typed literals (`"aldi"^^xsd:integer`, `"none"^^xsd:boolean`)
are rejected at ingest and can never be present to validate
(`datatype-001`, `datatype-ill-formed`, `or-datatypes-001`).
- Duplicate values in an RDF list collapse (`xone-duplicate`,
`path-complex-002`'s `( _:pinv _:pinv )` sequence).

Track the current pass rate with `make count` before and after engine
changes; regressions in previously-passing categories are the signal to
watch.
39 changes: 34 additions & 5 deletions docs/guides/cookbook-shacl.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This guide covers:
- [Predicate-target shapes](#predicate-target-shapes) — `sh:targetSubjectsOf` / `sh:targetObjectsOf`
- [Per-graph enable/disable and warn vs reject](#per-graph-configuration) modes
- [Storing shapes in a named graph](#storing-shapes-in-a-named-graph) with `f:shapesSource`
- [Validation reports](#validation-reports-fluree-validate) — `fluree validate` over a ledger or file
- [What isn't enforced yet](#not-yet-supported)

## When SHACL runs
Expand All @@ -25,9 +26,10 @@ This means you can start using SHACL **without writing any config** — just tra

**Bulk import is deliberately exempt.** The bulk-import pipeline never runs
SHACL — it is a trusted, high-throughput load path. If your source data must
conform, validate it *before* importing (e.g. run a SHACL report over the
source with your shapes) so the ledger starts clean; transaction-time
validation keeps it clean from there.
conform, validate it *before* importing — `fluree validate source.ttl
--shacl shapes.ttl` produces a full report (see
[Validation reports](#validation-reports-fluree-validate)) — so the ledger
starts clean; transaction-time validation keeps it clean from there.

The `shacl` feature must be enabled at build time (it's on by default for the server and CLI binaries). See [Standards and feature flags](../reference/compatibility.md).

Expand Down Expand Up @@ -371,7 +373,7 @@ ex:StrictPersonShape a sh:NodeShape ;
sh:property [ sh:path schema:name ; sh:minCount 1 ] .
```

A closed shape forbids any property not explicitly declared (or listed in `sh:ignoredProperties`). `rdf:type` is implicitly ignored per the SHACL spec.
A closed shape forbids any property not explicitly declared (or listed in `sh:ignoredProperties`). Per the SHACL spec, `rdf:type` is **not** implicitly ignored — a closed shape with `sh:targetClass` (whose instances necessarily carry `rdf:type`) must list it in `sh:ignoredProperties`, as above.

## RDFS subclass reasoning for `sh:class`

Expand Down Expand Up @@ -593,9 +595,36 @@ SHACL validation runs consistently on every write surface:

All three routes go through the same post-stage helper, so the ledger's configured SHACL posture (enable/disable, mode, per-graph, shapes source) applies uniformly.

## Validation reports (`fluree validate`)

Transaction-time enforcement rejects (or warns about) *new* writes.
`fluree validate` answers the complementary questions: *is my existing data
clean?* and *is this source file clean before I import it?* It produces a
W3C-shaped `sh:ValidationReport` instead of an error:

```bash
fluree validate mydb # ledger vs its attached shapes
fluree validate mydb --shacl proposed.ttl # trial stricter shapes (replaces attached)
fluree validate data.ttl --shacl shapes.ttl # standalone file, ephemeral in-memory ledger
fluree validate data.ttl --format turtle # W3C report as Turtle (also: jsonld)
```

Ad-hoc shapes (`--shacl`) **replace** the attached shapes by default so
"does this data conform to these rules?" is answered exactly;
`--include-attached` unions both sets. Exit codes make it CI-friendly:
0 = conforms, 1 = findings at/above `--fail-on` (default `violation`).
See the [`fluree validate` reference](../cli/validate.md).

The same core is served over HTTP as
[`GET|POST /validate/{ledger}`](../api/endpoints.md#getpost-validateledger)
(JSON summary by default; `Accept: application/ld+json` or `text/turtle`
for the W3C report), and exposed in Rust as `fluree_db_api::validate` —
`Fluree::validate_ledger(alias, &ValidateOptions)` returns a `ValidateReport`
with per-result constraint-component IRIs, severities, and messages, plus
`to_jsonld()` / `to_turtle()` serializers.

## Not yet supported

- `sh:targetNode` with a literal value — only IRI/blank-node targets are compiled.
- `sh:sparql` (SPARQL-based constraints).

These are tracked in the SHACL compliance effort.
Expand Down
Loading
Loading