Replacing Prolog-based Subject DNA with W3C SHACL + AD4M Extensions
AD4M uses Subject DNA (SDNA) to define data schemas and their operational behavior. This document describes the migration from Prolog-based SDNA to a SHACL-based approach that:
- W3C Conformant - Schema definitions use standard SHACL predicates
- Extensible - AD4M-specific actions use a separate
ad4m://namespace - Queryable - All metadata stored as links, queryable via SurrealQL or simple link queries
- Consistent - Same pattern for Classes and Flows
- One predicate = one action type - Clear semantics
- SHACL for schema, AD4M for behavior - Clean separation
- Named URIs - Property shapes have queryable URIs (not blank nodes)
- JSON in literals - Actions stored as JSON arrays in
literal://string:format
For a class named Recipe with namespace recipe://:
| Link Type | Source | Predicate | Target |
|---|---|---|---|
| Shape Type | recipe://RecipeShape |
rdf://type |
sh://NodeShape |
| Target Class | recipe://RecipeShape |
sh://targetClass |
recipe://Recipe |
| Has Property | recipe://RecipeShape |
sh://property |
recipe://Recipe.name |
| Property Type | recipe://Recipe.name |
rdf://type |
sh://PropertyShape |
| Property Path | recipe://Recipe.name |
sh://path |
recipe://name |
| Datatype | recipe://Recipe.name |
sh://datatype |
xsd://string |
| Constructor | recipe://RecipeShape |
ad4m://constructor |
literal://string:[...] |
| Destructor | recipe://RecipeShape |
ad4m://destructor |
literal://string:[...] |
| Setter | recipe://Recipe.name |
ad4m://setter |
literal://string:[...] |
| Adder | recipe://Recipe.items |
ad4m://adder |
literal://string:[...] |
| Remover | recipe://Recipe.items |
ad4m://remover |
literal://string:[...] |
@ModelOptions({
name: "Recipe",
namespace: "recipe://",
})
class Recipe {
@SubjectProperty({ through: "recipe://name", writable: true })
name: string = "";
@SubjectProperty({ through: "recipe://rating", writable: true })
rating: number = 0;
@SubjectCollection({ through: "recipe://has_ingredient" })
ingredients: string[] = [];
}# Shape definition
recipe://RecipeShape rdf:type sh:NodeShape .
recipe://RecipeShape sh:targetClass recipe://Recipe .
# Property: name
recipe://Recipe.name rdf:type sh:PropertyShape .
recipe://Recipe.name sh:path recipe://name .
recipe://Recipe.name sh:datatype xsd:string .
recipe://Recipe.name sh:maxCount 1 .
recipe://RecipeShape sh:property recipe://Recipe.name .
# Property: rating
recipe://Recipe.rating rdf:type sh:PropertyShape .
recipe://Recipe.rating sh:path recipe://rating .
recipe://Recipe.rating sh:datatype xsd:integer .
recipe://Recipe.rating sh:maxCount 1 .
recipe://RecipeShape sh:property recipe://Recipe.rating .
# Collection: ingredients
recipe://Recipe.ingredients rdf:type ad4m:CollectionShape .
recipe://Recipe.ingredients sh:path recipe://has_ingredient .
recipe://Recipe.ingredients sh:nodeKind sh:IRI .
recipe://RecipeShape sh:property recipe://Recipe.ingredients .# Constructor - creates instance with default values
recipe://RecipeShape ad4m://constructor """literal://string:[
{"action": "addLink", "source": "this", "predicate": "recipe://name", "target": ""},
{"action": "addLink", "source": "this", "predicate": "recipe://rating", "target": "0"}
]""" .
# Destructor - removes instance links
recipe://RecipeShape ad4m://destructor """literal://string:[
{"action": "removeLink", "source": "this", "predicate": "recipe://name"},
{"action": "removeLink", "source": "this", "predicate": "recipe://rating"}
]""" .
# Property setters
recipe://Recipe.name ad4m://setter """literal://string:[
{"action": "setSingleTarget", "source": "this", "predicate": "recipe://name", "target": "value"}
]""" .
recipe://Recipe.rating ad4m://setter """literal://string:[
{"action": "setSingleTarget", "source": "this", "predicate": "recipe://rating", "target": "value"}
]""" .
# Collection operations
recipe://Recipe.ingredients ad4m://adder """literal://string:[
{"action": "addLink", "source": "this", "predicate": "recipe://has_ingredient", "target": "value"}
]""" .
recipe://Recipe.ingredients ad4m://remover """literal://string:[
{"action": "removeLink", "source": "this", "predicate": "recipe://has_ingredient", "target": "value"}
]""" .| Predicate | Purpose | Bound To |
|---|---|---|
ad4m://constructor |
Create instance with defaults | {namespace}{ClassName}Shape |
ad4m://destructor |
Remove instance and links | {namespace}{ClassName}Shape |
| Predicate | Purpose | Bound To |
|---|---|---|
ad4m://setter |
Set single-valued property | {namespace}{ClassName}.{propertyName} |
ad4m://adder |
Add to collection | {namespace}{ClassName}.{collectionName} |
ad4m://remover |
Remove from collection | {namespace}{ClassName}.{collectionName} |
[
{
"action": "addLink|removeLink|setSingleTarget|collectionSetter",
"source": "this|uuid|literal",
"predicate": "namespace://predicate",
"target": "value|*|specific_value",
"local": true // optional
}
]// Find shape with constructor
let links = self.get_links(&LinkQuery {
predicate: Some("ad4m://constructor".to_string()),
..Default::default()
}).await?;
// Find one matching class name
for link in links {
if link.data.source.ends_with(&format!("{}Shape", class_name)) {
// Parse JSON from literal://string:{json}
let actions = parse_literal_json(&link.data.target)?;
return Ok(actions);
}
}// Find property shape with setter
let prop_suffix = format!("{}.{}", class_name, property_name);
let links = self.get_links(&LinkQuery {
predicate: Some("ad4m://setter".to_string()),
..Default::default()
}).await?;
for link in links {
if link.data.source.ends_with(&prop_suffix) {
let actions = parse_literal_json(&link.data.target)?;
return Ok(actions);
}
}- TypeScript: Generate SHACL JSON with actions in
generateSDNA() - TypeScript: Serialize SHACL to links in
PerspectiveProxy.ensureSdnaLinks() - Rust: Parse SHACL JSON in
shacl_parser.rs - Rust: Store action links with separate predicates
-
get_constructor_actions()- Try SHACL first, fallback to Prolog -
get_destructor_actions()- Try SHACL first, fallback to Prolog -
get_property_setter_actions()- Try SHACL first, fallback to Prolog -
get_collection_adder_actions()- Try SHACL first, fallback to Prolog -
get_collection_remover_actions()- Try SHACL first, fallback to Prolog -
resolve_property_value()- Try SHACL for resolve language - TypeScript
removeSubject()- Try SHACL for destructor actions
Note: Prolog engines (scryer-prolog) are kept available for complex queries and future advanced features. Only the fallback pattern is removed - SHACL is the single source of truth for all SDNA actions.
- Remove Prolog fallbacks for action retrieval (SHACL-first is now SHACL-only)
- Migrate Flows to same SHACL link pattern (
SHACLFlowclass withtoLinks()/fromLinks()) - Keep scryer-prolog dependency (for complex Prolog queries later)
- Refactor TypeScript to use
SHACLShape.fromLinks()/toJSON()throughout - Use batched link operations (
addLinks()) and single SurrealDB queries
- W3C Standard - Interoperable with SHACL ecosystem
- Cleaner Runtime - SHACL as single source for SDNA actions (Prolog still available for complex queries)
- Queryable - All metadata as links in SurrealDB
- Debuggable - Inspect schema as regular links
- Extensible - Add new action types without schema changes
| File | Purpose |
|---|---|
core/src/model/decorators.ts |
generateSHACL() - Creates SHACL from decorators |
core/src/shacl/SHACLShape.ts |
SHACL shape class with toLinks() |
core/src/perspectives/PerspectiveProxy.ts |
ensureSdnaLinks() - Stores SHACL as links |
rust-executor/src/perspectives/shacl_parser.rs |
Parses SHACL JSON, generates links |
rust-executor/src/perspectives/perspective_instance.rs |
Action retrieval with SHACL-first |