Ontology & Reasoning
When Do You Need an Ontology?
Section titled “When Do You Need an Ontology?”An ontology captures meaning about your data—relationships that exist at the type level, not just instance level. You need ontology when:
- Type hierarchies: “A Podcast is a type of Media” (query for Media, get Podcasts too)
- Concept relationships: “Machine Learning is narrower than AI” (topic navigation)
- Constraints: “A Person cannot also be an Organization” (prevent invalid data)
- Edge implications: Query
knowsthrough more-specificmarriedTorows when explicitly requested - Bidirectional queries: “manages and managedBy are inverses” (traverse in either direction)
Without ontology, you’d implement these manually—if statements scattered throughout your code, hand-rolled validation, duplicate queries. Ontology centralizes this logic in your schema.
How It Works
Section titled “How It Works”TypeGraph treats semantic relationships between types as meta-edges—edges at the type level rather than instance level:
// Instance edges: relationships between INSTANCES// "Alice knows Bob"const knows = defineEdge("knows");
// Meta-edges: relationships between TYPES// "Employee subClassOf Person"subClassOf(Employee, Person);When you define an ontology, TypeGraph:
- Precomputes closures at store initialization (not query time)
- Expands only the query operations that explicitly opt in (except inverse
traversal, whose store default is
"inverse"and can be changed) - Enforces the documented constraints when building a registry or writing data
It does not run a general reasoner, materialize implied edges, substitute properties between types, or automatically expand every query.
Verified Support Matrix
Section titled “Verified Support Matrix”| Relation / feature | Runtime contract |
|---|---|
subClassOf |
Transitive registry closure, write-path endpoint assignability, and opt-in node-query expansion with includeSubClasses |
disjointWith |
Same-ID collision enforcement, propagated through interleaved subClassOf and equivalentTo closure (sameAs remains a deprecated equivalence alias) |
implies |
Transitive registry closure and opt-in traversal expansion with expand: "implying"; endpoints are validated |
inverseOf |
Single inverse partner, endpoint reversal validation, and traversal expansion with expand: "inverse" (the default store setting) |
equivalentTo |
Registry lookups and graph-merge type reconciliation; no automatic query or property behavior. sameAs is folded in as a full alias — the merge type reconciler and the registry treat a sameAs declaration identically to equivalentTo |
broader / narrower |
Transitive registry introspection only |
partOf / hasPart |
Transitive registry introspection only |
relatedTo |
Symmetric direct registry introspection through getRelatedKinds only |
Type-level sameAs |
Deprecated name for equivalentTo (see above); prefer calling equivalentTo directly |
Type-level differentFrom |
Deprecated and decorative — never enforced instance identity; migrate to the graph-level TypeGraph Identity Profile |
Custom metaEdge() properties |
Serialized introspection metadata only; custom transitivity, symmetry, inverse, and inference settings are not executed |
Core Meta-Edges
Section titled “Core Meta-Edges”TypeGraph provides a standard set of meta-edges:
import { subClassOf, broader, narrower, equivalentTo, sameAs, differentFrom, disjointWith, partOf, hasPart, relatedTo, inverseOf, implies } from "@nicia-ai/typegraph";Subsumption (Type Inheritance)
Section titled “Subsumption (Type Inheritance)”subClassOf: Defines type inheritance where instances of the child are also instances of the parent.
subClassOf(Podcast, Media);subClassOf(Article, Media);subClassOf(Company, Organization);Query Behavior:
Subclass expansion is opt-in via includeSubClasses: true:
// Without expansion: returns only nodes with kind="Media"const mediaOnly = await store .query() .from("Media", "m") .select((ctx) => ctx.m) .execute();
// With expansion: returns Media, Podcast, AND Article nodesconst allMedia = await store .query() .from("Media", "m", { includeSubClasses: true }) .select((ctx) => ctx.m) .execute();// Results include nodes of kind "Media", "Podcast", and "Article"This is a fundamental difference from traditional ORM inheritance—TypeGraph stores the concrete type
(kind: "Podcast") in the database, and expands at query time when requested.
Hierarchical (Concept Hierarchy)
Section titled “Hierarchical (Concept Hierarchy)”broader and narrower: Define conceptual hierarchy without identity.
broader(MachineLearning, ArtificialIntelligence);broader(DeepLearning, MachineLearning);broader(ArtificialIntelligence, Technology);Important: This is different from subClassOf. A topic instance of “ML” is related to “AI”,
but is not an instance of “AI”.
// Get all topics narrower than Technologyconst narrowerTopics = registry.expandNarrower("Technology");// ["ArtificialIntelligence", "MachineLearning", "DeepLearning", ...]Equivalence
Section titled “Equivalence”equivalentTo: Defines semantic equivalence between types or external IRIs.
equivalentTo(Person, "https://schema.org/Person");equivalentTo(Organization, "https://schema.org/Organization");sameAs and differentFrom are deprecated type-level factories.
sameAs is currently a type-equivalence alias; differentFrom is decorative.
For durable individual identity, enable the graph-level TypeGraph Identity
Profile and use store.identity. That ledger deliberately does not provide OWL
property substitution or automatic graph-wide query expansion.
Constraints
Section titled “Constraints”disjointWith: Declares that two types cannot share the same ID.
disjointWith(Person, Organization);disjointWith(Podcast, Article);Disjointness is inherited by subclasses. If Company subClassOf Organization,
then disjointWith(Person, Organization) also makes Person and Company
disjoint.
Effect: Attempting to create a node that violates disjointness throws DisjointError:
// Create a Person with ID "entity-1"await store.nodes.Person.create({ name: "Alice" }, { id: "entity-1" });
// Throws DisjointError: Person and Organization are disjointawait store.nodes.Organization.create({ name: "Acme" }, { id: "entity-1" });Coherence rules: disjointWith cannot contradict the rest of the ontology.
A kind disjoint with itself, a kind disjoint with one of its own subclass
ancestors, a common subclass of two disjoint parents, and a kind declared both
equivalentTo and disjointWith another are all rejected, including overlaps
reached through mixed equivalence/subclass paths. These checks run
both when you construct a graph and when a persisted schema is reloaded, so a
document written by an older, more permissive version can fail validation on
load with a ConfigurationError whose details code is
ONTOLOGY_DISJOINT_CONFLICT. To recover, fix the graph definition and, for a
persisted schema, correct the stored document before upgrading (or rewrite it
through the previous minor version, which still accepts it). The same
construction-and-reload rule applies to the other ontology coherence checks
(duplicate relations, hierarchical self-loops and cycles, and inverse-partner
uniqueness).
Composition
Section titled “Composition”partOf and hasPart: Define compositional relationships.
partOf(Chapter, Book);hasPart(Book, Chapter);partOf(Episode, Podcast);hasPart(Podcast, Episode);Edge Relationships
Section titled “Edge Relationships”inverseOf: Declares two edge kinds as inverses of each other.
inverseOf(manages, managedBy);inverseOf(cites, citedBy);inverseOf(follows, followedBy);Effect: You can query in either direction using the registry:
const inverse = registry.getInverseEdge("manages"); // "managedBy"You can also expand traversals to include inverse edge kinds at query time:
const relationships = await store .query() .from("Person", "p") .traverse("manages", "e", { expand: "inverse" }) .to("Person", "other") .select((ctx) => ({ other: ctx.other.name, via: ctx.e.kind, })) .execute();For symmetric relationships, declare an edge as its own inverse:
inverseOf(collaboratesWith, collaboratesWith);An edge may have only one distinct inverse partner. Every allowed pair must be
compatible with a reversed pair in its partner, in both traversal directions,
using equal kinds or subClassOf assignability. Matching the independent source
and target unions is insufficient for source-dependent edges. A self-inverse
edge must satisfy the same reversed-pair check against itself.
implies: Declares that one edge kind implies another exists.
implies(marriedTo, knows);implies(bestFriends, friends);implies(friends, knows);Effect: Query for knows can include marriedTo, bestFriends, and friends edges:
const connections = await store .query() .from("Person", "p") .traverse("knows", "e", { expand: "implying" }) .to("Person", "other") .select((ctx) => ctx.other) .execute();Endpoint compatibility is required. implies(edgeA, edgeB) only makes
sense if every node kind edgeA can connect could also, in principle,
satisfy edgeB’s own domain/range — otherwise expand: "implying" would
traverse rows whose kinds don’t match what the traversal actually asked for.
Every allowed pair in edgeA must match a single allowed pair in edgeB:
both endpoints must be assignable — equal, or a subClassOf descendant —
to their corresponding endpoint in that pair. For
source-dependent targets, finding
the source in one entry and the target in another does not suffice.
An incompatible pair (say, Author -> Paper implying
Paper -> Topic) throws ConfigurationError wherever the graph is built
into a store or committed as a schema version (createStore,
createStoreWithSchema, store.evolve({ ontology })) — including relations
authored through a graph extension, not just implies() calls in code.
Using the Ontology
Section titled “Using the Ontology”In Graph Definition
Section titled “In Graph Definition”const graph = defineGraph({ id: "knowledge_base", nodes: { ... }, edges: { ... }, ontology: [ // Type hierarchy subClassOf(Podcast, Media), subClassOf(Article, Media), subClassOf(Company, Organization),
// Concept hierarchy broader(MachineLearning, ArtificialIntelligence), broader(DeepLearning, MachineLearning),
// Constraints disjointWith(Person, Organization), disjointWith(Media, Person),
// Composition partOf(Episode, Podcast),
// Edge relationships inverseOf(cites, citedBy), implies(marriedTo, knows), ],});Registry Lookups
Section titled “Registry Lookups”The type registry (accessed via store.registry) provides methods to query the ontology:
const registry = store.registry;
// Subsumptionregistry.isSubClassOf("Podcast", "Media"); // trueregistry.expandSubClasses("Media"); // ["Media", "Podcast", "Article"]
// Hierarchyregistry.expandNarrower("Technology"); // ["AI", "ML", "DL", ...]registry.expandBroader("DeepLearning"); // ["ML", "AI", "Technology"]
// Constraintsregistry.areDisjoint("Person", "Organization"); // trueregistry.getDisjointKinds("Person"); // ["Organization", "Media", ...]
// Edge relationshipsregistry.getInverseEdge("cites"); // "citedBy"registry.getImpliedEdges("marriedTo"); // ["knows"]registry.getImplyingEdges("knows"); // ["marriedTo", "bestFriends", "friends"]registry.getRelatedKinds("MachineLearning"); // ["DataScience", ...]Custom Meta-Edges
Section titled “Custom Meta-Edges”Define domain-specific meta-edges for serialized introspection metadata:
import { metaEdge } from "@nicia-ai/typegraph";
// Custom meta-edge for prerequisite relationshipsconst prerequisiteOf = metaEdge("prerequisiteOf", { transitive: true, inference: "hierarchy", description: "Learning prerequisite (Calculus prerequisiteOf LinearAlgebra)",});
// Custom meta-edge for superseding relationshipsconst supersedes = metaEdge("supersedes", { transitive: true, inference: "substitution", description: "Replacement relationship (v2 supersedes v1)",});Meta-Edge Properties
Section titled “Meta-Edge Properties”Each custom meta-edge can carry these properties as metadata. In the current release they do not make the registry compute a custom closure or make the query builder execute custom inference. Only the built-in relations in the support matrix have runtime behavior.
| Property | Type | Description |
|---|---|---|
transitive |
boolean |
A→B, B→C implies A→C |
symmetric |
boolean |
A→B implies B→A |
reflexive |
boolean |
A→A is always true |
inverse |
string |
Name of inverse meta-edge |
inference |
InferenceType |
How this affects queries |
Inference Types
Section titled “Inference Types”For custom meta-edges, inference is descriptive metadata for consumers:
| Type | Description |
|---|---|
"subsumption" |
Query for X includes instances of subclasses |
"hierarchy" |
Enables broader/narrower traversal |
"substitution" |
Can substitute equivalent types |
"constraint" |
Validation rules |
"composition" |
Part-whole navigation |
"association" |
Discovery/recommendation |
"none" |
No automatic inference |
Closure Computation
Section titled “Closure Computation”TypeGraph precomputes transitive closures at store initialization:
// subClassOf closure// If: Podcast subClassOf Media, Episode subClassOf Media// Then: expandSubClasses("Media") = ["Media", "Podcast", "Episode"]
// implies closure// If: marriedTo implies partneredWith, partneredWith implies knows// Then: getImpliedEdges("marriedTo") = ["partneredWith", "knows"]This makes queries efficient—expansion happens at query compilation time, not execution time.
Best Practices
Section titled “Best Practices”Separate subClassOf from broader
Section titled “Separate subClassOf from broader”These have different semantics:
subClassOf: Type membership (a Podcast instance is also a Media instance)broader: Conceptual relation (ML relates to AI, but ML instance ≠ AI instance)
// CORRECT: Type hierarchysubClassOf(Podcast, Media);
// CORRECT: Concept hierarchybroader(MachineLearning, ArtificialIntelligence);
// WRONG: Don't mix them// subClassOf(MachineLearning, ArtificialIntelligence);Use Disjoint Constraints
Section titled “Use Disjoint Constraints”Prevent impossible combinations:
// Good: Prevent ID conflictsdisjointWith(Person, Organization);disjointWith(Person, Product);disjointWith(Organization, Product);Model Edge Hierarchies with Implies
Section titled “Model Edge Hierarchies with Implies”// Relationship hierarchy: specific → generalimplies(marriedTo, partneredWith);implies(partneredWith, knows);implies(parentOf, relatedTo);implies(siblingOf, relatedTo);implies(relatedTo, knows);Use InverseOf for Bidirectional Queries
Section titled “Use InverseOf for Bidirectional Queries”inverseOf(manages, managedBy);inverseOf(follows, followedBy);inverseOf(cites, citedBy);This lets you query efficiently in either direction without duplicating edges.
API Reference
Section titled “API Reference”Ontology Functions
Section titled “Ontology Functions”subClassOf(child, parent)
Section titled “subClassOf(child, parent)”Declares type inheritance.
function subClassOf(child: NodeType, parent: NodeType): OntologyRelation;broader(narrower, broader)
Section titled “broader(narrower, broader)”Declares hierarchical relationship (narrower concept to broader concept).
function broader(narrower: NodeType, broader: NodeType): OntologyRelation;narrower(broader, narrower)
Section titled “narrower(broader, narrower)”Declares hierarchical relationship (broader concept to narrower concept).
function narrower(broader: NodeType, narrower: NodeType): OntologyRelation;equivalentTo(a, b)
Section titled “equivalentTo(a, b)”Declares semantic equivalence between types or with external IRIs.
function equivalentTo( a: NodeType | string, b: NodeType | string): OntologyRelation;sameAs(kindA, kindBOrIri)
Section titled “sameAs(kindA, kindBOrIri)”Deprecated type-level alias of equivalentTo, including the equivalence with
external IRIs. Migrate to the graph-level TypeGraph Identity Profile for
individual identity.
function sameAs(kindA: NodeType, kindBOrIri: NodeType | string): OntologyRelation;differentFrom(a, b)
Section titled “differentFrom(a, b)”Deprecated decorative type-level relation. Migrate to the graph-level TypeGraph Identity Profile for individual identity.
function differentFrom(a: NodeType, b: NodeType): OntologyRelation;disjointWith(a, b)
Section titled “disjointWith(a, b)”Declares mutual exclusion (types cannot share the same ID).
function disjointWith(a: NodeType, b: NodeType): OntologyRelation;partOf(part, whole)
Section titled “partOf(part, whole)”Declares compositional relationship (part to whole).
function partOf(part: NodeType, whole: NodeType): OntologyRelation;hasPart(whole, part)
Section titled “hasPart(whole, part)”Declares compositional relationship (whole to part).
function hasPart(whole: NodeType, part: NodeType): OntologyRelation;relatedTo(a, b)
Section titled “relatedTo(a, b)”Declares a symmetric association available through
registry.getRelatedKinds(kind). It has no query behavior.
function relatedTo(a: NodeType, b: NodeType): OntologyRelation;inverseOf(edgeA, edgeB)
Section titled “inverseOf(edgeA, edgeB)”Declares edge types as inverses of each other.
function inverseOf(edgeA: AnyEdgeType, edgeB: AnyEdgeType): OntologyRelation;implies(edgeA, edgeB)
Section titled “implies(edgeA, edgeB)”Declares that one edge type implies another exists.
function implies(edgeA: AnyEdgeType, edgeB: AnyEdgeType): OntologyRelation;Each allowed pair in edgeA must be assignable to one allowed pair in edgeB
(equal, or a subClassOf descendant, on both endpoints). Throws
ConfigurationError when the graph is built into a store or committed as a
schema version if they aren’t — see Edge Relationships
above.
metaEdge(name, options?)
Section titled “metaEdge(name, options?)”Creates a custom meta-edge for domain-specific relationships.
function metaEdge( name: string, options?: { transitive?: boolean; symmetric?: boolean; reflexive?: boolean; inverse?: string; inference?: InferenceType; description?: string; },): MetaEdge;Type Registry API
Section titled “Type Registry API”The type registry is available via store.registry and provides methods to query the ontology at runtime.
isSubClassOf(child, parent)
Section titled “isSubClassOf(child, parent)”Checks if a type is a subclass of another.
registry.isSubClassOf(child: string, parent: string): boolean;
registry.isSubClassOf("Podcast", "Media"); // trueexpandSubClasses(type)
Section titled “expandSubClasses(type)”Returns a type and all its subclasses.
registry.expandSubClasses(type: string): readonly string[];
registry.expandSubClasses("Media"); // ["Media", "Podcast", "Article"]areDisjoint(a, b)
Section titled “areDisjoint(a, b)”Checks if two types are disjoint.
registry.areDisjoint(a: string, b: string): boolean;
registry.areDisjoint("Person", "Organization"); // truegetDisjointKinds(type)
Section titled “getDisjointKinds(type)”Returns all types disjoint with the given type.
registry.getDisjointKinds(type: string): readonly string[];
registry.getDisjointKinds("Person"); // ["Organization", "Media", ...]expandNarrower(type)
Section titled “expandNarrower(type)”Returns all types narrower than the given type (via broader relationships).
registry.expandNarrower(type: string): readonly string[];
registry.expandNarrower("Technology"); // ["AI", "ML", "DeepLearning", ...]expandBroader(type)
Section titled “expandBroader(type)”Returns all types broader than the given type.
registry.expandBroader(type: string): readonly string[];
registry.expandBroader("DeepLearning"); // ["MachineLearning", "AI", "Technology"]getInverseEdge(edgeType)
Section titled “getInverseEdge(edgeType)”Returns the inverse of an edge type.
registry.getInverseEdge(edgeType: string): string | undefined;
registry.getInverseEdge("manages"); // "managedBy"getImpliedEdges(edgeType)
Section titled “getImpliedEdges(edgeType)”Returns edges implied by an edge type.
registry.getImpliedEdges(edgeType: string): readonly string[];
registry.getImpliedEdges("marriedTo"); // ["knows"]getImplyingEdges(edgeType)
Section titled “getImplyingEdges(edgeType)”Returns edges that imply an edge type.
registry.getImplyingEdges(edgeType: string): readonly string[];
registry.getImplyingEdges("knows"); // ["marriedTo", "bestFriends", "friends"]expandImplyingEdges(edgeType)
Section titled “expandImplyingEdges(edgeType)”Returns an edge type and all edges that imply it.
registry.expandImplyingEdges(edgeType: string): readonly string[];
registry.expandImplyingEdges("knows"); // ["knows", "marriedTo", "bestFriends", "friends"]