Created
March 9, 2026 11:21
-
-
Save jeswr/cc6cb057d09e8860aa84dd4b52ddd8e6 to your computer and use it in GitHub Desktop.
SHACL Versioning Pattern for the Solid Ecosystem
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SHACL Versioning Pattern for the Solid Ecosystem | |
| ## Context & Problem Statement | |
| The Solid SHACL Shapes Catalogue (`solid/shapes`) already establishes that published shapes are **immutable contracts** — once merged, their validation rules must not be modified. Evolution happens through new shapes, not mutation of existing ones. | |
| This is a strong foundation, but it doesn't yet address several questions that become critical as the catalogue grows: | |
| - How do consumers know which shape is the "current" version of a concept? | |
| - How do apps discover that a shape they depend on has a successor? | |
| - How do we express the relationship between `AddressShape` and `AddressStrictShape` — is one a refinement, a replacement, or an alternative? | |
| - How do we coordinate versioning across the ecosystem without creating a central bottleneck? | |
| This document proposes a versioning pattern that builds on the catalogue's existing immutability principle and works within the constraints of Solid's decentralised architecture. | |
| --- | |
| ## Design Principles | |
| 1. **Immutability is non-negotiable.** Published shapes are never modified. This is already established and this pattern reinforces it. | |
| 2. **Shapes are contracts, not code.** Versioning semantics should reflect the impact on data consumers and producers, not internal development milestones. | |
| 3. **Decentralised discovery.** Any app should be able to determine version relationships by reading the shapes graph alone — no external registry required. | |
| 4. **Backward compatibility is the default.** New shapes should be additive where possible; breaking changes should be explicit and rare. | |
| 5. **Namespace stability.** The `https://solidproject.org/shapes/{domain}#` namespace should remain stable across versions. | |
| --- | |
| ## Versioning Strategy | |
| ### Level 1: Naming Convention (Current Catalogue Approach) | |
| The existing catalogue already supports evolution through naming: | |
| ```turtle | |
| address-shape:AddressShape # Original | |
| address-shape:AddressMinimalShape # Lighter variant | |
| address-shape:AddressStrictShape # Stricter variant | |
| ``` | |
| **This works for variants** (alternative shapes for different validation needs) but does not express temporal succession or deprecation. It's the right approach for *coexisting alternatives*, not for *replacement*. | |
| ### Level 2: Version Annotation Metadata | |
| For shapes that represent a deliberate evolution of an earlier shape, add annotation metadata using standard vocabularies: | |
| ```turtle | |
| @prefix address-shape: <https://solidproject.org/shapes/address#> . | |
| @prefix sh: <http://www.w3.org/ns/shacl#> . | |
| @prefix owl: <http://www.w3.org/2002/07/owl#> . | |
| @prefix dct: <http://purl.org/dc/terms/> . | |
| @prefix xsd: <http://www.w3.org/2001/XMLSchema#> . | |
| @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . | |
| @prefix vs: <http://www.w3.org/2003/06/sw-vocab-status/ns#> . | |
| # ── Original shape (still valid, never modified) ────────────── | |
| address-shape:AddressShape | |
| a sh:NodeShape ; | |
| rdfs:label "Address Shape" ; | |
| rdfs:comment "Validates a postal address using vCard properties." ; | |
| dct:created "2026-04-01"^^xsd:date ; | |
| vs:term_status "stable" ; | |
| sh:targetClass vcard:Address ; | |
| # ... property shapes ... | |
| . | |
| # ── Evolved shape (successor) ───────────────────────────────── | |
| address-shape:AddressShape-v2 | |
| a sh:NodeShape ; | |
| rdfs:label "Address Shape v2" ; | |
| rdfs:comment "Validates a postal address with structured sub-components." ; | |
| dct:created "2027-01-15"^^xsd:date ; | |
| dct:replaces address-shape:AddressShape ; | |
| vs:term_status "stable" ; | |
| sh:targetClass vcard:Address ; | |
| # ... evolved property shapes ... | |
| . | |
| ``` | |
| And on the original, once a successor exists: | |
| ```turtle | |
| address-shape:AddressShape | |
| dct:isReplacedBy address-shape:AddressShape-v2 ; | |
| vs:term_status "archaic" ; | |
| . | |
| ``` | |
| ### Key Metadata Properties | |
| | Property | Usage | Source Vocabulary | | |
| |---|---|---| | |
| | `dct:created` | Date shape was published | Dublin Core Terms | | |
| | `dct:replaces` | Points from new shape → predecessor | Dublin Core Terms | | |
| | `dct:isReplacedBy` | Points from old shape → successor | Dublin Core Terms | | |
| | `vs:term_status` | Lifecycle stage: `unstable`, `testing`, `stable`, `archaic` | W3C Vocab Status | | |
| | `rdfs:comment` | Human-readable description of what changed | RDFS | | |
| | `owl:versionInfo` | Optional free-text version string | OWL | | |
| ### Why Dublin Core `replaces`/`isReplacedBy`? | |
| These are well-established, widely understood predicates already used in the Linked Data ecosystem. They express directional succession without requiring OWL reasoning. They're also already in use across W3C specifications and the broader vocabulary ecosystem, so Solid apps that understand Dublin Core (which most do via `solid-namespace`) can interpret them without new dependencies. | |
| --- | |
| ## Lifecycle Model | |
| A shape progresses through these stages, expressed via `vs:term_status`: | |
| ``` | |
| unstable → testing → stable → archaic | |
| ``` | |
| | Stage | Meaning | Can be modified? | | |
| |---|---|---| | |
| | **unstable** | Draft, in active development. Not yet in the catalogue. | Yes (pre-merge only) | | |
| | **testing** | Published to catalogue for community review. Apps may experiment. | No (immutable once merged) | | |
| | **stable** | Endorsed for production use. Apps should depend on this. | No | | |
| | **archaic** | Superseded by a successor. Still valid, but new apps should use successor. | No (only `dct:isReplacedBy` and `vs:term_status` annotation added) | | |
| **Important:** Moving a shape to `archaic` is the *only* permitted modification to a published shape, and it only adds metadata — it never changes validation semantics. | |
| --- | |
| ## What Constitutes a Breaking Change? | |
| In the Solid context, a "breaking change" is anything that would cause existing valid Pod data to fail validation, or would prevent an existing app from correctly reading/writing data. Specifically: | |
| ### Breaking (requires new shape): | |
| - Adding mandatory properties (`sh:minCount` increased above 0) | |
| - Removing properties that apps may have written | |
| - Changing `sh:datatype` of an existing property | |
| - Changing `sh:targetClass` | |
| - Tightening cardinality (e.g. `[0..*]` → `[1..1]`) | |
| - Changing `sh:nodeKind` (e.g. `sh:Literal` → `sh:IRI`) | |
| ### Non-breaking (allowed as annotation to existing shape): | |
| - Adding `vs:term_status` | |
| - Adding `dct:isReplacedBy` | |
| - Adding `rdfs:comment` or `rdfs:seeAlso` | |
| - Adding `owl:versionInfo` | |
| ### Requires a new shape variant (not a version bump): | |
| - Adding new *optional* properties | |
| - Adding alternative path constraints | |
| - Loosening cardinality (e.g. `[1..1]` → `[0..*]`) | |
| The distinction between "new version" (`-v2`) and "new variant" (`MinimalShape`, `StrictShape`) matters: versions imply succession; variants imply coexistence. | |
| --- | |
| ## Namespace & IRI Strategy | |
| ### Keep the namespace stable | |
| The existing pattern `https://solidproject.org/shapes/{domain}#` should **not** include version numbers in the namespace: | |
| ``` | |
| # GOOD — stable namespace, version in the local name | |
| https://solidproject.org/shapes/address#AddressShape | |
| https://solidproject.org/shapes/address#AddressShape-v2 | |
| # AVOID — version in namespace creates proliferation | |
| https://solidproject.org/shapes/address/v2#AddressShape | |
| ``` | |
| **Rationale:** Version-in-namespace means every new version creates a new namespace, which breaks prefix caching, complicates SPARQL queries, and makes discovery harder. The FOAF vocabulary's long-lived success with a fixed namespace demonstrates this principle. | |
| ### Use IRIs, not blank nodes, for all shapes | |
| This is already implied by the catalogue's naming conventions but worth making explicit: every shape and every property shape should have a named IRI. This is critical for: | |
| - Cross-version references (`dct:replaces`) | |
| - App-level caching and dependency tracking | |
| - `sh:deactivated` overrides in downstream shapes graphs | |
| **Property shape naming convention:** | |
| ```turtle | |
| address-shape:AddressShape-v2-streetAddress | |
| a sh:PropertyShape ; | |
| sh:path vcard:street-address ; | |
| # ... | |
| . | |
| ``` | |
| --- | |
| ## Discovery Pattern | |
| For apps that need to programmatically find the current version of a shape: | |
| ### Option A: Follow `dct:isReplacedBy` chain | |
| An app holding a reference to `address-shape:AddressShape` can check whether it has been superseded: | |
| ```sparql | |
| SELECT ?current WHERE { | |
| <https://solidproject.org/shapes/address#AddressShape> | |
| dct:isReplacedBy* ?current . | |
| FILTER NOT EXISTS { ?current dct:isReplacedBy ?newer } | |
| } | |
| ``` | |
| This traverses the replacement chain to find the terminal (current) shape. | |
| ### Option B: Shape index document | |
| As the catalogue grows, maintain a lightweight index at a well-known location: | |
| ```turtle | |
| # https://solidproject.org/shapes/index | |
| <https://solidproject.org/shapes/address#AddressShape-v2> | |
| a sh:NodeShape ; | |
| vs:term_status "stable" ; | |
| dct:replaces <https://solidproject.org/shapes/address#AddressShape> . | |
| <https://solidproject.org/shapes/person#PersonShape> | |
| a sh:NodeShape ; | |
| vs:term_status "stable" . | |
| ``` | |
| This gives apps a single document to fetch for the current state of all shapes. | |
| --- | |
| ## Relationship to Solid 2026 and the W3C Shapes Namespace | |
| The `solid/shapes` catalogue currently uses: | |
| ``` | |
| https://solidproject.org/shapes/{domain}# | |
| ``` | |
| The earlier `solid/shapes` README also references the W3C path: | |
| ``` | |
| https://www.w3.org/ns/solid/shapes/{shortname} | |
| ``` | |
| **Recommendation:** Clarify whether these are intended to be the same shapes served from two locations (with one redirecting), or separate concerns. For versioning purposes, shapes should have exactly one canonical IRI. If W3C namespace publication is intended for "blessed" shapes that have reached `stable` status, this could be expressed as: | |
| ```turtle | |
| address-shape:AddressShape-v2 | |
| rdfs:isDefinedBy <https://solidproject.org/shapes/address> ; | |
| rdfs:seeAlso <https://www.w3.org/ns/solid/shapes/address> ; | |
| . | |
| ``` | |
| --- | |
| ## Worked Example: Evolving the Address Shape | |
| ### Phase 1: Initial shape (Solid 2026 launch) | |
| ```turtle | |
| @prefix address-shape: <https://solidproject.org/shapes/address#> . | |
| address-shape:AddressShape | |
| a sh:NodeShape ; | |
| rdfs:label "Address Shape" ; | |
| dct:created "2026-04-01"^^xsd:date ; | |
| vs:term_status "stable" ; | |
| sh:targetClass vcard:Address ; | |
| sh:property address-shape:AddressShape-streetAddress ; | |
| sh:property address-shape:AddressShape-locality ; | |
| sh:property address-shape:AddressShape-postalCode ; | |
| sh:property address-shape:AddressShape-countryName ; | |
| . | |
| address-shape:AddressShape-streetAddress | |
| a sh:PropertyShape ; | |
| sh:path vcard:street-address ; | |
| sh:datatype xsd:string ; | |
| sh:maxCount 1 ; | |
| . | |
| # ... other property shapes ... | |
| ``` | |
| ### Phase 2: Community feedback — apps need structured address lines | |
| Rather than modifying `AddressShape`, a **variant** is created: | |
| ```turtle | |
| address-shape:AddressStructuredShape | |
| a sh:NodeShape ; | |
| rdfs:label "Structured Address Shape" ; | |
| rdfs:comment "Address with separate fields for building, street, etc." ; | |
| dct:created "2026-09-01"^^xsd:date ; | |
| vs:term_status "testing" ; | |
| sh:targetClass vcard:Address ; | |
| # ... structured property shapes ... | |
| . | |
| ``` | |
| Both `AddressShape` and `AddressStructuredShape` remain valid and coexist. | |
| ### Phase 3: Breaking change needed — country code replaces country name | |
| A new version is required because the datatype changes: | |
| ```turtle | |
| address-shape:AddressShape-v2 | |
| a sh:NodeShape ; | |
| rdfs:label "Address Shape v2" ; | |
| rdfs:comment "Uses ISO 3166-1 country codes instead of free-text country names." ; | |
| dct:created "2027-06-01"^^xsd:date ; | |
| dct:replaces address-shape:AddressShape ; | |
| vs:term_status "stable" ; | |
| sh:targetClass vcard:Address ; | |
| # ... updated property shapes ... | |
| . | |
| ``` | |
| And `AddressShape` is annotated (the only permitted change): | |
| ```turtle | |
| address-shape:AddressShape | |
| dct:isReplacedBy address-shape:AddressShape-v2 ; | |
| vs:term_status "archaic" ; | |
| . | |
| ``` | |
| --- | |
| ## Tooling Recommendations | |
| 1. **Sorted Turtle serialisation.** Use deterministic serialisation (e.g. rdflib's Sorted Turtle, or TopBraid's equivalent) for all `.ttl` files in the catalogue. This makes Git diffs meaningful and reviewable. | |
| 2. **CI validation.** The catalogue's build script should validate: | |
| - All shapes parse as valid SHACL | |
| - All shapes have required metadata (`rdfs:label`, `dct:created`, `vs:term_status`) | |
| - No `stable` or `archaic` shape has been modified (checksum comparison) | |
| - `dct:replaces`/`dct:isReplacedBy` links are bidirectional and consistent | |
| 3. **Shape diff tooling.** Consider integrating a SHACL diff tool into the PR review process that can automatically classify changes as breaking/non-breaking. | |
| 4. **Changelog generation.** Metadata in the shapes graph (`dct:created`, `dct:replaces`, `vs:term_status`) is sufficient to auto-generate a human-readable changelog. | |
| --- | |
| ## Summary | |
| | Concern | Approach | | |
| |---|---| | |
| | Immutability | Shapes never change once published (existing policy) | | |
| | Variants (coexisting alternatives) | Descriptive naming: `AddressMinimalShape`, `AddressStrictShape` | | |
| | Versions (temporal succession) | Suffix naming: `AddressShape-v2`, linked via `dct:replaces` | | |
| | Lifecycle status | `vs:term_status`: unstable → testing → stable → archaic | | |
| | Namespace | Stable `https://solidproject.org/shapes/{domain}#` — no version in namespace | | |
| | Discovery | `dct:isReplacedBy` chain traversal or shape index document | | |
| | Breaking change definition | Anything that invalidates existing Pod data or breaks existing app reads/writes | | |
| | Serialisation | Sorted Turtle for meaningful Git diffs | | |
| | CI enforcement | Validate metadata completeness, immutability, and link consistency | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment