Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SHACL Validation

SHACL (Shapes Constraint Language) lets you define what valid data looks like and enforce it at write time. When an agent or user tries to add facts that violate a shape, Quipu rejects the write and returns structured feedback explaining exactly what’s wrong.

Why Validate?

Without validation, agents can write anything:

hw:korimako hw:cpuCores "lots" .   # Should be an integer
hw:korimako a hw:Host .            # Missing required hostname

With SHACL shapes loaded, Quipu catches these problems before they enter the fact log.

Class targets and subclasses

A sh:targetClass selects instances of that class and of its subclasses, following the data graph’s rdfs:subClassOf edges transitively. For example, if the validation data declares ex:Step rdfs:subClassOf ex:Action, an entity typed only ex:Step must satisfy a shape targeting ex:Action. Cycles and multiple inheritance do not duplicate a focus node within the same shape. This follows SHACL class-based targets.

Target selection leaves asserted data unchanged. It does not manufacture rdf:type ex:Action values for a constraint such as sh:hasValue on rdf:type. An explicit-type requirement still needs an explicit assertion. Subclass declarations present only in the shapes graph are not merged into the validation data by this mechanism. Write vocabulary admission remains a separate check against loaded shapes.

Defining a Shape

A shape declares constraints for a class of entities. Here’s a shape that says “every Host must have exactly one hostname (a string) and at least one cpuCores (an integer)”:

@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix hw:  <http://example.org/homelab/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

hw:HostShape
    a sh:NodeShape ;
    sh:targetClass hw:Host ;
    sh:property [
        sh:path hw:hostname ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path hw:cpuCores ;
        sh:datatype xsd:integer ;
        sh:minCount 1 ;
    ] .

Loading Shapes

CLI

quipu shapes load --name homelab --file shapes/homelab.shapes.ttl --db homelab.db

REST API

curl -s localhost:3030/shapes -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "action": "load",
    "name": "homelab",
    "turtle": "@prefix sh: <http://www.w3.org/ns/shacl#> .\n@prefix hw: <http://example.org/homelab/> .\nhw:HostShape a sh:NodeShape ; sh:targetClass hw:Host ; sh:property [ sh:path hw:hostname ; sh:minCount 1 ] ."
  }'

Listing loaded shapes

quipu shapes list --db homelab.db

Declaring ontology classes and properties

The bundled governance shape set sanctions named ontology resources typed rdfs:Class and rdf:Property. Load that set before an adapter imports its ontology declarations through /knot. These declarations require IRI subjects.

Declaring <urn:example:Task> a rdfs:Class in the data graph does not authorize instances of urn:example:Task. The write vocabulary still comes from loaded shape sets; add and load a shape targeting the intended class before writing its instances. Unknown types continue to be refused.

Demoted derivation records

shapes/demoted-derivation.ttl defines the product evidence schema in http://quipu.local/graph#, matching the truth-maintenance runtime vocabulary. Ship and load this set together with the compatible demotion runtime.

A DemotedDerivation is an IRI identifying an rdf:Statement. It retains exactly one rdf:subject, rdf:predicate, rdf:object, premiseGraph, deriverSource, promotionTx, invalidationTx, and prov:invalidatedAtTime. Subject, predicate and premise graph are IRIs; the object may be any RDF term. The source is a string, transaction IDs are integers at least one, and the invalidation time is an xsd:dateTime.

derivationState must be exactly one string: unsupported or resolved. Resolution preserves the evidence record. Extra annotations are allowed. The shape rejects missing or ambiguous fields and incorrect RDF term kinds. It does not establish atomic demotion, deterministic identity, exclusion from premise evaluation, or authorization to promote; those are runtime guarantees. Loading the shape does not repair data or promote unsupported evidence.

Validation in Action

Recorded text-rule cases

The bundled aegis-ontology shapes allow aegis:mustMatch and aegis:mustNotMatch on TextRule and InternalIdentifierPattern. Each property accepts zero or more xsd:string values: example text that should trigger the rule, or should stay silent, respectively. Existing rules without cases remain valid. IRIs, language-tagged strings and numeric literals are not case text.

Schema validity does not establish enforcement readiness. A readiness checker must require at least one case of each polarity and execute every case through the rule engine against a current projection. Missing cases or indeterminate execution must not pass. These text-only examples do not prove path or repository exemptions; contextual cases require a separate model. Recording cases does not change a rule’s enforcement tier.

Rejected writes

Try to add a Host without a hostname:

quipu knot - --db homelab.db --shapes shapes/homelab.shapes.ttl <<'EOF'
@prefix hw: <http://example.org/homelab/> .
hw:badhost a hw:Host .
EOF

Quipu rejects it with structured feedback:

{
  "conforms": false,
  "violations": 1,
  "issues": [
    {
      "severity": "Violation",
      "focus_node": "http://example.org/homelab/badhost",
      "path": "http://example.org/homelab/hostname",
      "component": "MinCountConstraintComponent",
      "message": "Less than 1 values for hw:hostname",
      "source_shape": "http://example.org/homelab/HostShape"
    }
  ]
}

This feedback is designed for agents — structured JSON with enough detail to fix the problem automatically.

Value sets across writes

A valid delta can still violate a stored upper bound. For example, writing ex:item ex:value "A" and later ex:item ex:value "B" must not bypass sh:maxCount 1 just because each request carries one value.

Write admission checks predicate and inverse-predicate sh:maxCount, sh:uniqueLang true, sh:disjoint, sh:lessThan, and sh:lessThanOrEquals against the staged current facts inside the same SQLite savepoint as the write. The second distinct value is rejected and rolled back. Split writes cannot bypass language-tag uniqueness or comparisons between the stored values of two properties. Reasserting an identical RDF term remains idempotent; retracting the old value and asserting its replacement in one transaction is valid. Historical facts remain available.

This check covers CLI knot --shapes, the knot and SPARQL Update entrypoints, inline episode shapes, conditional writes with loaded shapes, and configured validate_on_write transactions. It preserves each entrypoint’s existing reject/advisory policy. Low-level transactions without explicit shapes or write validation configured do not acquire an implicit shape policy.

Value-set validation uses each destination graph’s own values. ROOT type and subclass context can select targets in a named graph; ROOT property values and unrelated third graphs do not contribute to its sets. The context is bounded to touched subjects and referenced objects, with at most 100,000 facts and 1,024 classes. Exceeding these limits, or encountering another complex value-set property path, refuses admission with cannot verify rather than accepting partial evidence.

Capability and incoming-context obligations use the batch validator’s compiled shape graph. Its deactivation flags suppress obligations for inactive node shapes and their solely dependent properties. A shared property still imposes its obligations when another active shape or its own target reaches it; logical and qualified shape references retain that conservative check. Unsupported active paths still refuse.

Other constraints retain payload validation and contextual type/required-field repair. This is not whole-store conformance checking or automatic revalidation of every dependent node after changing a schema, class hierarchy, or target relation. Standalone validate remains a check of the supplied data only.

Supported Constraints

ConstraintWhat it checks
sh:minCount / sh:maxCountCardinality (how many values)
sh:datatypeValue type (xsd:string, xsd:integer, etc.)
sh:minInclusive / sh:maxInclusiveNumeric ranges
sh:minLength / sh:maxLengthString length
sh:patternRegex match
sh:inAllowed values (enumeration)
sh:classReferenced entity must have rdf:type
sh:nodeNested shape reference
sh:or / sh:and / sh:notLogical constraints
sh:equals / sh:disjointProperty pair constraints

Quipu supports the full SHACL Core specification via the rudof library.

Dry-Run Validation

Validate data without writing it to the store:

quipu validate --shapes shapes/homelab.shapes.ttl --data data.ttl
curl -s localhost:3030/validate -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "shapes": "@prefix sh: ... shapes turtle ...",
    "data": "@prefix hw: ... data turtle ..."
  }'

Domain shapes in practice

Domain constraints belong to their domain producer. The staged Camayoc domain profiles separate infrastructure, personal media and software production. The worked example explains their compatibility and rollout. The transition retains the existing bundled Aegis shapes until destination publication, pinning, loading and validation have been verified. They cover:

  • LXCContainer, ProxmoxNode, BareMetalHost — compute resources
  • SystemdService, WebApplication, Database — services
  • CommandDiskImpactObservation — one privacy-preserving command/filesystem measurement. diskDeltaBytes is signed: positive values mean space consumed and negative values mean space freed. Command and filesystem identity use canonical classes rather than raw arguments or paths; aggregate counts and quantiles belong in a separate summary entity.
  • ConfigFile — optional migration-safe configPath and contentSha256 facts support exact drift lookups. When present, each is single-valued and the digest must be a full lowercase SHA-256 hexadecimal string.
  • CiJob and LocalCommand — map a CI job to at most one typed local equivalent, the repository paths it gates, and the command text used to run that equivalent before push.
  • Common properties: hostname, ipAddress, memoryMB, cpuCores, dependsOn

An offline check can use the producer-owned file:

quipu validate --shapes ../camayoc/shapes/aegis/homelab-infra.shapes.ttl --data example.ttl

Best Practices

  • Load shapes before data — shapes must be present to validate incoming writes
  • One shape set per domain — group related constraints (e.g., “homelab”, “code”)
  • Start permissive, tighten later — begin with minCount constraints, add datatype checks as your ontology stabilizes
  • Use validation feedback — the structured JSON is designed for automated remediation by agents

Episode provenance activities

The shipped shapes/provenance.ttl includes a minimal prov:Activity profile for episode provenance. It requires a string rdfs:label, which the episode producer emits. Body and source fields remain optional so historical episodes remain valid, and multiple retained comments are permitted. This profile does not authorize a new write path or change import/promotion checks. A receiver must load its reviewed shapes and verify the loaded vocabulary before importing activity facts; a source file alone does not establish runtime governance.

Write-time shape pre-parsing uses the batch validator’s fallback base IRI for relative Turtle identifiers, including relative manifest references in shape documents. An explicit Turtle base still takes precedence. This compatibility does not relax cross-write constraint checks or the atomic refusal of invalid post-state data.