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
| Constraint | What it checks |
|---|---|
sh:minCount / sh:maxCount | Cardinality (how many values) |
sh:datatype | Value type (xsd:string, xsd:integer, etc.) |
sh:minInclusive / sh:maxInclusive | Numeric ranges |
sh:minLength / sh:maxLength | String length |
sh:pattern | Regex match |
sh:in | Allowed values (enumeration) |
sh:class | Referenced entity must have rdf:type |
sh:node | Nested shape reference |
sh:or / sh:and / sh:not | Logical constraints |
sh:equals / sh:disjoint | Property 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 resourcesSystemdService,WebApplication,Database— servicesCommandDiskImpactObservation— one privacy-preserving command/filesystem measurement.diskDeltaBytesis 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-safeconfigPathandcontentSha256facts support exact drift lookups. When present, each is single-valued and the digest must be a full lowercase SHA-256 hexadecimal string.CiJobandLocalCommand— 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.