You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Written by Claude Code, Opus 4.6. Result of reading all NeoWiki docs, ADRs, context files, and PHP/TS domain models, prompted by @JeroenDeDauw.
Updated 2026-07-05: added the Status table and per-section updates pointing to since-created issues and ADRs; corrected the cross-wiki use case. Claude Code, Fable 5 (max), reviewed by @JeroenDeDauw.
Updated 2026-07-21: refreshed the Status table after the relation integrity pass merged (#1080, #1082, #1083, #1084) and the Relations model was proposed as Draft ADR 28 with planning/Relations.md. Claude Code, Fable 5 (max), not yet reviewed by a human.
Each item here should end up as a decision (ADR where architectural), a dedicated issue, or documentation; once every row points elsewhere, this issue can close. As of 2026-07-21 every row points elsewhere — closing is a maintainer call; rows 1, 2, and 6 finalize when ADR 28 is ratified.
#
Item
Type
Status
1
Relation properties need schema-level definitions
Decided (ADR 28, Draft)
Won't build: qualification uses typed Subjects; the edge-properties bag is removed. Removal tracked in #1119, gated on ADR 28 ratification.
2
Target Schema is mandatory and singular
Decided (ADR 28, Draft)
targetSchema widens to a list of Schemas, tracked in #991; the supertype variant was rejected. The unconstrained "any Subject" case is parked in planning/Relations.md until a concrete need arrives.
3
One-directional relations
Decided
One-directional storage stays. Inverse display is follow-up work, tracked in #904 (successor of closed #743).
#544 fixed: referenced Subjects survive as stub nodes (#1080, follow-ups #1111). ADR 28 makes missing targets red-link semantics; the red-link UI affordance is #1120.
6
Cardinality / uniqueness constraints
Partly landed, rest parked
single-value-only enforced for relations (#1082); node-uniqueness constraints created on rebuild (#1083). Relation-ID uniqueness stays open in #351 (not graph-enforceable over dynamic edge types). Richer cardinality is parked in planning/Relations.md.
How Relations Work Today
A Relation is one of the four value types a Statement can hold (alongside string, number, boolean). When a Subject has a Statement like "CEO": { "type": "relation", "value": [{ "id": "r...", "target": "s..." }] }, that's a Relation.
Each Relation instance has:
A RelationId (15-char nanoid starting with r)
A target SubjectId
Properties — an untyped array<string, mixed> bag of scalar values
The Schema defines relation-typed properties via RelationProperty, which adds:
relation — a RelationType name (e.g., "Has CEO"), used as the Neo4j relationship label
targetSchema — the Schema the target must follow (e.g., "Person")
multiple — whether multiple targets are allowed
In Neo4j, relations become actual graph edges: (:Subject)-[:Has CEO {id, ...properties}]->(:Subject). Everything else (string/number/boolean statements) becomes node properties.
Issues and Design Questions
1. Relation Properties need schema-level definitions
RelationProperties is array<string, mixed> — anything goes. There's no schema defining what properties a relation can or should have, no validation, and no way for API consumers or tooling to discover what properties are meaningful for a given relation type.
Relation properties correspond to Wikibase's "qualifiers" — metadata on a specific relationship instance. The classic example: Company → Person via "Has CEO" with properties { role: "CEO", since: 2019 }. For cultural heritage use cases, these could include provenance, temporal qualifiers, confidence levels, and source references.
The question: Should relation properties get their own schema definitions (like mini property definitions within the relation's Property Definition)? This would make the data model self-describing, enable validation, and provide the structure needed for any future UI or API consumer to work with them meaningfully.
Update (2026-07): Reinforced by the ECHOLOT data-modelling discussion of 2026-06-24: CIDOC-CRM-style structures (dimensions with unit/upper bound/lower bound/source; event qualifiers) hang exactly this kind of data off relations, and #971 defers to this issue for relation-property schemas. Candidate for a dedicated spin-out issue.
Update (2026-07-21): Decided the other way in Draft ADR 28: qualification uses typed Subjects, and relation edge properties are removed rather than given schemas (#1119).
2. Target Schema is mandatory and singular
targetSchema is required on every relation property. You can't create a relation that points to "any Subject" or to "Subjects of type Person OR Organization."
Use cases where this is restrictive:
"Related to" — a generic association to any Subject
Cultural heritage: An artwork might relate to a Person (artist), Organization (museum), Place (location), Event (exhibition) — you'd need four separate relation properties instead of one "Related to"
"References" or "See also" — pointing to any Subject type
The question: Should targetSchema be optional (allow any schema) or support an array of schemas? Making it optional would cover polymorphic use cases without breaking existing typed relations, which benefit from the constraint for validation and UI filtering.
Update (2026-07): The typed-union side is now tracked in #991, which also puts a third option on the table: a shared supertype via schema subtyping (e.g. City and Country subclass Location, and the relation targets Location). The optional/any-target side is a distinct need and remains untracked.
Update (2026-07-21): Draft ADR 28 adopts the list-of-Schemas option (#991) and rejects the supertype variant; the any-target case is parked in planning/Relations.md.
3. Relations are one-directional
If Company X "Has CEO" Person Y, this is stored in Company X's JSON. Person Y's data has no record of this relationship.
In Neo4j you can traverse both directions, so Cypher queries work fine. But Person Y's infobox won't show "Is CEO of Company X" unless you separately create a "Works at" relation on Person Y, meaning bidirectional data must be maintained manually.
One-directional storage is the right call — storing relations on both sides would create consistency nightmares. But displaying inverse relations will need dedicated design, likely via Cypher queries and Views rather than data model changes.
Update (2026-07): Inverse display is tracked in #904 (successor of #743); one-directional storage stands.
4. The conceptual fit of Relations-as-Values
Relations are modeled as a value type within the Statement system. This is clean in the sense that everything about a Subject is in its statements. But it creates a conceptual tension:
Relations are naturally "connections between things": Company → Person
The system handles this by treating them identically at the storage/schema level, then bifurcating at the Neo4j level (relations become edges, scalars become properties). This works, but it means the Statement abstraction is doing double duty — it's both a property-value store and a relationship store.
This isn't necessarily wrong — Wikibase does essentially the same thing (Items as values of claims). But it's worth being aware of the conceptual weight it places on "Statement."
5. What happens to dangling references?
The Neo4j updater does MERGE (target {id: $targetId}) — if the target Subject doesn't exist yet (or was deleted), Neo4j creates a ghost node with just an id. The GraphModel doc says deleted Subjects with incoming relations keep their node around.
This is reasonable for graph consistency, but:
There's no mechanism to clean up fully orphaned ghost nodes
There's no UI indication when a relation target no longer exists
Import scenarios could create many dangling references if data is imported in the wrong order
Update (2026-07): The deletion side is tracked in #544 (savePage() deletes Subjects without checking incoming relations); ghost-node cleanup and UI indication remain untracked.
Update (2026-07-21): Landed: #544 is fixed — referenced Subjects survive as minimal stub nodes (#1080, hardened in #1111), and server-side validation reports missing targets as a non-blocking relation-target-not-found warning (#1082). Draft ADR 28 makes missing targets red-link semantics; the red-link display/create affordance is #1120.
6. No constraints on relation cardinality or uniqueness
The only constraint is multiple: true/false. There's no:
Minimum/maximum number of relations
Uniqueness constraint (can you have two relations to the same target?)
Constraint that a target Subject must actually exist (referential integrity)
For the current stage this is probably fine, but worth noting as future work.
Update (2026-07):#351 covers relation-ID uniqueness; the rest remains open.
Update (2026-07-21): Partly landed: single-value-only is enforced for relations and target existence/schema conformance are validated (#1082); Neo4j node-uniqueness constraints are created on rebuild (#1083). #351 stays open (relation-ID uniqueness is not graph-enforceable over dynamic edge types — see the analysis comment there). Richer cardinality is parked in planning/Relations.md.
Use Cases to Consider
Simple typed links: Book → Author (Person). This works well today.
Qualified relationships: Person → Organization with role and time period. Modeled as typed Subjects per Draft ADR 28 and Qualifiers and References.
Cultural heritage provenance: Artwork → Source with confidence, date, provenance chain. This is a rich qualifier use case.
Cross-wiki relations: A Subject in Wiki A references a Subject in Wiki B. Covered by design since ADR 022 (multi-wiki node identity) and ADR 023 (Subject Sources): the (source, localId) pair is the canonical reference form for relation targets. Implementation is part of the Subject Sources work.
Generic/polymorphic relations: "Related to" any type of Subject. Blocked by mandatory targetSchema. The typed-union variant (a known set of acceptable schemas) is Relations need to support multiple target schemas #991; the fully generic case remains untracked.
What Seems Right
Relations as a value type within Statements — keeps the model uniform
RelationIds — necessary for stable identity and Neo4j MERGE operations
One-directional storage — the right call, inverse display should be a query concern
Target schema constraint for typed relations — provides useful validation and UI filtering
Separate Property Name and Relation Type — UI labels and graph edge labels serve different purposes. (Now questioned: Draft ADR 28's least-settled decision proposes a single name keyed on the property.)
What Needs Work
Relation properties need schema-level definitions before they can be meaningfully used. The current untyped bag makes them opaque to API consumers and tooling. (2026-07-21: resolved the other way — removed per Draft ADR 28; Remove relation edge properties in favor of typed Subjects #1119.)
Target Schema could be optional or support multiple schemas to enable polymorphic relations. (Typed unions: Relations need to support multiple target schemas #991.) (2026-07-21: list-of-Schemas adopted in Draft ADR 28.)
Written by Claude Code, Opus 4.6. Result of reading all NeoWiki docs, ADRs, context files, and PHP/TS domain models, prompted by @JeroenDeDauw.
Updated 2026-07-05: added the Status table and per-section updates pointing to since-created issues and ADRs; corrected the cross-wiki use case. Claude Code,
Fable 5 (max), reviewed by @JeroenDeDauw.Updated 2026-07-21: refreshed the Status table after the relation integrity pass merged (#1080, #1082, #1083, #1084) and the Relations model was proposed as Draft ADR 28 with planning/Relations.md. Claude Code,
Fable 5 (max), not yet reviewed by a human.See also: issues labeled "relation"
Status (2026-07-21)
Each item here should end up as a decision (ADR where architectural), a dedicated issue, or documentation; once every row points elsewhere, this issue can close. As of 2026-07-21 every row points elsewhere — closing is a maintainer call; rows 1, 2, and 6 finalize when ADR 28 is ratified.
propertiesbag is removed. Removal tracked in #1119, gated on ADR 28 ratification.targetSchemawidens to a list of Schemas, tracked in #991; the supertype variant was rejected. The unconstrained "any Subject" case is parked in planning/Relations.md until a concrete need arrives.single-value-onlyenforced for relations (#1082); node-uniqueness constraints created on rebuild (#1083). Relation-ID uniqueness stays open in #351 (not graph-enforceable over dynamic edge types). Richer cardinality is parked in planning/Relations.md.How Relations Work Today
A Relation is one of the four value types a Statement can hold (alongside string, number, boolean). When a Subject has a Statement like
"CEO": { "type": "relation", "value": [{ "id": "r...", "target": "s..." }] }, that's a Relation.Each Relation instance has:
r)array<string, mixed>bag of scalar valuesThe Schema defines relation-typed properties via
RelationProperty, which adds:relation— a RelationType name (e.g., "Has CEO"), used as the Neo4j relationship labeltargetSchema— the Schema the target must follow (e.g., "Person")multiple— whether multiple targets are allowedIn Neo4j, relations become actual graph edges:
(:Subject)-[:Has CEO {id, ...properties}]->(:Subject). Everything else (string/number/boolean statements) becomes node properties.Issues and Design Questions
1. Relation Properties need schema-level definitions
RelationPropertiesisarray<string, mixed>— anything goes. There's no schema defining what properties a relation can or should have, no validation, and no way for API consumers or tooling to discover what properties are meaningful for a given relation type.Relation properties correspond to Wikibase's "qualifiers" — metadata on a specific relationship instance. The classic example: Company → Person via "Has CEO" with properties
{ role: "CEO", since: 2019 }. For cultural heritage use cases, these could include provenance, temporal qualifiers, confidence levels, and source references.The question: Should relation properties get their own schema definitions (like mini property definitions within the relation's Property Definition)? This would make the data model self-describing, enable validation, and provide the structure needed for any future UI or API consumer to work with them meaningfully.
Update (2026-07): Reinforced by the ECHOLOT data-modelling discussion of 2026-06-24: CIDOC-CRM-style structures (dimensions with unit/upper bound/lower bound/source; event qualifiers) hang exactly this kind of data off relations, and #971 defers to this issue for relation-property schemas. Candidate for a dedicated spin-out issue.
Update (2026-07-21): Decided the other way in Draft ADR 28: qualification uses typed Subjects, and relation edge properties are removed rather than given schemas (#1119).
2. Target Schema is mandatory and singular
targetSchemais required on every relation property. You can't create a relation that points to "any Subject" or to "Subjects of type Person OR Organization."Use cases where this is restrictive:
The question: Should
targetSchemabe optional (allow any schema) or support an array of schemas? Making it optional would cover polymorphic use cases without breaking existing typed relations, which benefit from the constraint for validation and UI filtering.Update (2026-07): The typed-union side is now tracked in #991, which also puts a third option on the table: a shared supertype via schema subtyping (e.g.
CityandCountrysubclassLocation, and the relation targetsLocation). The optional/any-target side is a distinct need and remains untracked.Update (2026-07-21): Draft ADR 28 adopts the list-of-Schemas option (#991) and rejects the supertype variant; the any-target case is parked in planning/Relations.md.
3. Relations are one-directional
If Company X "Has CEO" Person Y, this is stored in Company X's JSON. Person Y's data has no record of this relationship.
In Neo4j you can traverse both directions, so Cypher queries work fine. But Person Y's infobox won't show "Is CEO of Company X" unless you separately create a "Works at" relation on Person Y, meaning bidirectional data must be maintained manually.
One-directional storage is the right call — storing relations on both sides would create consistency nightmares. But displaying inverse relations will need dedicated design, likely via Cypher queries and Views rather than data model changes.
Update (2026-07): Inverse display is tracked in #904 (successor of #743); one-directional storage stands.
4. The conceptual fit of Relations-as-Values
Relations are modeled as a value type within the Statement system. This is clean in the sense that everything about a Subject is in its statements. But it creates a conceptual tension:
"Founded in" → 2019The system handles this by treating them identically at the storage/schema level, then bifurcating at the Neo4j level (relations become edges, scalars become properties). This works, but it means the Statement abstraction is doing double duty — it's both a property-value store and a relationship store.
This isn't necessarily wrong — Wikibase does essentially the same thing (Items as values of claims). But it's worth being aware of the conceptual weight it places on "Statement."
5. What happens to dangling references?
The Neo4j updater does
MERGE (target {id: $targetId})— if the target Subject doesn't exist yet (or was deleted), Neo4j creates a ghost node with just anid. The GraphModel doc says deleted Subjects with incoming relations keep their node around.This is reasonable for graph consistency, but:
Update (2026-07): The deletion side is tracked in #544 (
savePage()deletes Subjects without checking incoming relations); ghost-node cleanup and UI indication remain untracked.Update (2026-07-21): Landed: #544 is fixed — referenced Subjects survive as minimal stub nodes (#1080, hardened in #1111), and server-side validation reports missing targets as a non-blocking
relation-target-not-foundwarning (#1082). Draft ADR 28 makes missing targets red-link semantics; the red-link display/create affordance is #1120.6. No constraints on relation cardinality or uniqueness
The only constraint is
multiple: true/false. There's no:For the current stage this is probably fine, but worth noting as future work.
Update (2026-07): #351 covers relation-ID uniqueness; the rest remains open.
Update (2026-07-21): Partly landed:
single-value-onlyis enforced for relations and target existence/schema conformance are validated (#1082); Neo4j node-uniqueness constraints are created on rebuild (#1083). #351 stays open (relation-ID uniqueness is not graph-enforceable over dynamic edge types — see the analysis comment there). Richer cardinality is parked in planning/Relations.md.Use Cases to Consider
(source, localId)pair is the canonical reference form for relation targets. Implementation is part of the Subject Sources work.targetSchema. The typed-union variant (a known set of acceptable schemas) is Relations need to support multiple target schemas #991; the fully generic case remains untracked.What Seems Right
What Needs Work