shacl2cypher
← Shapes for Property Graphs

9. Severity, messages and CI

Shapes that nobody runs don't protect anything. This last chapter makes results readable, separates "must fix" from "should look at", and puts validation in the pipeline.

Examples: 09-severity.ttl.

Severity

sh:property [
    s2c:name "person-name-capitalised" ;
    sh:path ex:name ;
    sh:pattern "^[A-Z]" ;
    sh:severity sh:Warning ;
    sh:message "Name {?value} should start with a capital letter" ;
] ;
sh:property [
    sh:path ex:nickname ;
    sh:maxLength 20 ;
    sh:severity sh:Info ;
] .

Every constraint has a severity: sh:Violation (the default), sh:Warning or sh:Info. Severity doesn't change what is checked, only how much a failure matters. --fail-on decides what fails a run:

--fail-onExit code 1 when a rule fails at
violation (default)Violation
warningViolation or Warning
infoany severity

Custom severities, IRIs other than the three standard ones, count as violations.

Messages

sh:property [
    sh:path ex:name ;
    sh:minCount 1 ;
    sh:message "Person {$this} has no name" ;
] ;

Without sh:message, rows say something like ex:PersonShape violates sh:minCount on ex:name. With it, you choose the wording. {$this} and {?value} are replaced at query time with the actual focus node and value.

Names you can rely on

Every rule has two identifiers:

  • ruleId is structural: ex:PersonShape/ex:name/sh:minCount. It is derived from the shape, path and constraint, so moving shapes around or reordering a file doesn't change it.
  • name is short: PersonShape.name.minCount. s2c:name replaces the shape-and-path part, so the pattern rule above is named person-name-capitalised.pattern.

The manifest also records a fingerprint per rule, a hash of what the rule means. If someone changes a pattern but keeps the name, the fingerprint changes. Commit the manifest and the change shows up in review.

Switching rules off

ex:LegacyShape a sh:NodeShape ;
    sh:targetClass ex:LegacyAccount ;
    sh:deactivated true ;
    sh:property [ sh:path ex:migratedAt ; sh:minCount 1 ] .

A deactivated shape still compiles into the manifest, with status: deactivated and no queries, and shows as skipped in reports. It is visibly parked, not silently deleted. Because shapes files are unioned, you can deactivate a shape from a separate file without touching the original.

A rollout strategy

  1. Add new rules as sh:Warning. Validate with the default --fail-on violation. The pipeline stays green, and the report shows how much data breaks the rule.
  2. Fix the data, or the rule.
  3. Remove the severity so the rule becomes a violation and guards the graph from then on.

In the pipeline

Compile when shapes change, so the reviewed manifest is exactly what runs. Validate on a schedule, or after each data load:

name: graph-validation
on:
  pull_request:
    paths: ['shapes/**']
  schedule:
    - cron: '0 5 * * *'

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: cargo install --locked shacl2cypher --features neo4j   # or download a release binary
      - run: shacl2cypher compile shapes/*.ttl --dialect neo4j --node-key id --strict -o checks
      - run: git diff --exit-code checks                            # manifest committed and current
      - run: shacl2cypher validate --manifest checks/manifest.json --connect ${{ secrets.BOLT_URI }} --timeout 120 --format sarif -o report.sarif
        env:
          NEO4J_PASSWORD: ${{ secrets.NEO4J_PASSWORD }}
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: report.sarif }

What each piece buys you:

  • Exit codes: 0 clean, 1 violations at or above --fail-on, 2 a setup problem, 3 a query timed out and conformance is unknown. Treat 3 as a failure: an incomplete check is not a pass.
  • SARIF puts each violation on the line of the shapes file that declared the constraint, where code-scanning tools can show it.
  • JUnit (--format junit) gives one test case per rule for test dashboards: failures for violations, errors for timeouts.
  • --timeout bounds each query. Expensive rules, with cost classes unbounded-path or quadratic, are the ones to watch. The manifest's recommendedIndexes lists the label and key pairs worth indexing.

Where to go from here

You now know enough SHACL to describe almost any rule a property graph needs. For the full mapping rules and recipes, read the SHACL guide. To generate shapes from a visual model instead of writing them by hand, see LPG Modeler. Every option is listed in the reference.

Takeaways

  • Severity grades failures; --fail-on decides which ones break the build.
  • sh:message with {$this}/{?value} makes rows readable; s2c:name makes names stable and meaningful.
  • Commit the manifest, validate with it, and publish JUnit or SARIF.