High-level shape of the system. Updated as the code's architecture changes — not as implementation progresses within an already-described component. The canonical spec is
TheKnowledgeNetwork.md; the kernel implements that model. Outstanding work is described in../TASKS.md.
| Component | State |
|---|---|
| Build | Compiles clean — zero warnings (Erlang/OTP, the Open Telecom Platform, version 28 / rebar3 3.27) |
nref subsystem |
Fully implemented; backed by DETS (Disk-based Erlang Term Storage); set_floor/1 API |
dictionary_imp |
Implemented; not yet wired to dictionary_server / term_server |
graphdb_bootstrap |
Implemented — Mnesia schema, table creation, scaffold loader |
graphdb_mgr |
Implemented — bootstrap startup, read API, category guard, cache audit/repair; retire_node/1 / unretire_node/1 soft-retire runtime nodes via a boolean retired marker AVP; public get_node/1 returns {error, retired} for retired nodes; delete_node/1 remains unimplemented, reserved for a future hard delete. |
graphdb_attr |
Implemented — attribute library (name, literal, relationship attributes); seeds the retired lifecycle marker literal-attribute |
graphdb_class |
Implemented — taxonomic hierarchy with multi-parent inheritance (BFS — breadth-first search — over a DAG, a directed acyclic graph); abstract (non-instantiable) classes via the instantiable marker |
graphdb_instance |
Implemented — compositional hierarchy + four-level inheritance with multi-class membership and ambiguity-detecting class resolver; refuses instantiation/membership of abstract classes; refuses retired nodes as new instance targets, compositional parents, and arc endpoints; fires composition rules on create_instance/3 and surfaces proposed outcomes for propose-mode rules; fires connection rules via a caller-supplied resolver on create_instance/4; applies horizontal conflict precedence via a caller-overridable resolver on create_instance/5 |
graphdb_rules |
Implemented — rule meta-ontology, applies_to attachment, scope-aware create/retrieve, taxonomy-walking effective-rules read, composition firing engine, propose mode, connection firing, horizontal conflict precedence |
graphdb_language |
Implemented — multilingual overlay layer (label resolution, dialect chains, per-language Mnesia overlay tables) |
graphdb_query |
Implemented — query language with snapshot-semantics sessions and continuation-based bounded BFS |
| Tests | 537 passing (432 Common Test + 105 EUnit) |
The kernel is functional under multi-inheritance, multi-class-
membership, and per-class template semantics. Multilingual label
overlay (§10) and the query language (§11) are landed.
The graphdb_rules data model (§12) is landed, along with the
taxonomy-walk effective-rules read, the composition firing engine,
propose mode (create_instance/3 surfaces proposed outcomes),
connection firing, and horizontal conflict precedence. The later
firing-engine work — the instantiation engine and reactive learning —
remains. Node soft-retire (retire_node/1 / unretire_node/1) is
implemented; hard delete (delete_node/1) remains reserved.
| Subsystem | Storage | Why |
|---|---|---|
graphdb_* (nodes, relationships) |
Mnesia (disc_copies) |
ACID across tables, secondary indexes, distribution-ready |
nref_allocator / nref_server |
DETS | Simple persistent counter; no relational query needs |
dictionary_imp |
ETS (in-memory Erlang Term Storage) + tab2file |
In-memory cache, persistent serialisation |
nodes — one row per concept node (primary key: nref)
relationships — one row per directed arc (primary key: id)
Two tables cover the entire graph. Bidirectional logical edges are stored
as two directed rows in relationships, written atomically.
Indexes:
relationships— secondary onsource_nrefandtarget_nreffor O(1) forward and reverse traversal.nodescarries no secondary index. Downward queries ("children of X") read outgoing arcs fromrelationshipsfiltered by kind + characterization (see §3 cache invariant).
Embedding relationships inside the node record (Dallas's original DETS design) is rejected: it makes reverse-lookup an O(N) full-scan and prevents transactional updates spanning both endpoints.
-record(node, {
nref, %% integer() — primary key
kind, %% category | attribute | class | instance | template
parents = [], %% [integer()] — cache of parent arcs (composition/taxonomy)
classes = [], %% [integer()] — cache of instantiation arcs (instances only)
attribute_value_pairs %% [#{attribute => Nref, value => term()}]
}).| Kind | Purpose | Creatable at runtime? |
|---|---|---|
category |
Top-level organisational scaffold; bootstrap skeleton | No — bootstrap-only |
attribute |
Named concept used as an arc label, name attribute, or literal attribute descriptor | Yes |
class |
Type/schema; manages the taxonomic ("is a") hierarchy | Yes |
instance |
Concrete entity in a project; managed by the compositional ("part of") hierarchy | Yes |
template |
Named semantic context attached to a class; scopes connection arcs (see §4) | Yes |
category immutability is enforced by graphdb_mgr:check_category_guard/1;
no runtime API can create, modify, or delete a category node.
parents and classes are caches of the authoritative arcs in the
relationships table. The decision record is
arcs-authoritative.md; the rules are:
- Every taxonomic, compositional, and instantiation relationship is
canonical in
relationships. node.parentsandnode.classesare reconstructable from those arcs at any time. They exist purely so reads that need only the "who are my parents / classes" structure can skip the relationship index.- A cache that disagrees with the arcs is a fatal error, not correctable drift.
Cache field sources:
| Cache field | Authoritative arcs | Owner worker |
|---|---|---|
node.parents |
21/22 composition (category) | graphdb_bootstrap (writes); graphdb_mgr:rebuild_caches/0 populates |
node.parents |
23/24 taxonomy (attribute) | graphdb_attr |
node.parents |
25/26 taxonomy (class) or composition (template) | graphdb_class |
node.parents |
27/28 composition (instance) | graphdb_instance |
node.classes |
29 instantiation (instance → class) | graphdb_instance |
Note on attribute parent/child arcs: arc-label nrefs 23 ("Parent") and
24 ("Child") were minted at bootstrap as the attribute-subtree labels.
Arcs written under those labels carry kind = taxonomy, not
composition: an attribute parent/child relation is a refinement of
kind ("welded attachment" is-a-kind-of "attachment"), not part-whole.
The category scaffold above (Root → Attributes/Classes/Languages/
Projects) keeps kind = composition because categories are
organisational containers.
Each owner worker writes the arcs and the matching cache update inside
one mnesia:transaction/1. Other workers never touch the table
directly — they call the owner's API.
graphdb_mgr exposes two audit/repair APIs:
| Function | Purpose |
|---|---|
verify_caches/0 |
Scans every node; returns ok or {error, [{Nref, Field, Expected, Actual}, ...]}. |
rebuild_caches/0 |
Rewrites every node's caches from the arcs in one transaction. |
CT enforcement: every test suite calls verify_caches/0 in
end_per_testcase. A failed verify is a fatal CT failure. The
bootstrap loader runs rebuild_caches/0 followed by verify_caches/0
once all rows are written; a mismatch throws
{bootstrap_cache_invariant_failed, Mismatches} and aborts startup.
The root node is nref = 1, kind = category, parents = []. It is
the only node in the database with an empty parents list.
Five top-level categories are pre-assigned at bootstrap:
1 Root
2 ├── Attributes (parent of Names, Literals, Relationships subtrees)
3 ├── Classes (parent of all class taxonomies)
4 ├── Languages (language domain concepts — see §10)
5 └── Projects (organisational anchor for project databases — see §6)
The bootstrap loads 38 nodes in total: the 35-node BFS scaffold (nrefs
1–35), the permanent English instance seed (nref 10000), and 2 labeled
permanent nodes (lang_code, lang_human — nrefs assigned by the
loader's local counter starting at label_start = 10001). The full
content is documented in apps/graphdb/priv/bootstrap.terms. Code that
needs specific nrefs uses the constants defined as macros in the worker
that owns them (graphdb_attr, graphdb_class, graphdb_instance).
-record(relationship, {
id, %% integer() — primary key
kind, %% taxonomy | composition | connection | instantiation
source_nref, %% integer() — arc origin
characterization, %% integer() — arc label (an attribute nref)
target_nref, %% integer() — arc target
reciprocal, %% integer() — arc label as seen from target back
avps %% [#{attribute => Nref, value => term()}] — per-direction metadata
}).A logical edge between two nodes is stored as two rows, one per
direction, written atomically in a single Mnesia transaction. The
graphdb_bootstrap loader and graphdb_instance:add_relationship both
expand a single bidirectional intent into two directed records.
avps carries metadata that is asymmetric between the two directions —
provenance, confidence, weights, validity time frames, flags. Per
TheKnowledgeNetwork.md §5, this metadata
is part of the connection's identity for ASSOCIATE-type arcs, but does
not participate in graph traversal by default.
Every kind = connection arc carries a Template AVP — #{attribute => 31, value => TemplateNref} — that scopes the connection's
semantic context. The AVP attribute is bootstrap-seeded at nref 31;
it is forbidden on relationships of any other kind. Template nodes
are compositional children of class nodes (see §3 cache field
sources). API: graphdb_instance:add_relationship/5,6,7 (session-first — SP1).
Connection edges are mutated through graphdb_instance (connection-arcs
only — these never touch the parents/classes caches; all take a project
Session first arg — SP1 — and reject an invalid one with
{error, invalid_session}): remove_relationship/4,5 deletes both
directed rows of a logical edge atomically; update_relationship/5,6 and
update_relationship_both/5,6 edit the per-direction AVP metadata, reusing the
slice-B AVP merge grammar. Remove
is logical-edge-level; AVP update is directed-row-level (the (S,C,T) triple
names one directed row — name (T,R,S) to edit the reverse). The Template
AVP is protected from edit. Since nothing dedups connection edges at write
time, a key matching more than one logical edge yields
{ambiguous_relationship, Templates}; no match yields
relationship_not_found.
Five OTP applications started by application_master in dependency order.
graphdb declares mnesia and nref as dependencies; database declares
graphdb and dictionary; seerstone declares nref and database.
nref (application — started first)
└── nref_sup
├── nref_allocator — DETS-backed block allocator
└── nref_server — public nref API; calls allocator
graphdb (application — started after mnesia + nref)
└── graphdb_sup
├── graphdb_nref — switchable node-nref allocation facade (permanent during init; runtime after flip)
├── rel_id_server — arc row ID allocator (separate from nref space)
├── graphdb_mgr — primary coordinator; bootstrap startup
├── graphdb_attr — attribute library
├── graphdb_class — taxonomic hierarchy
├── graphdb_instance — compositional hierarchy + inheritance
├── graphdb_language — multilingual label overlay
├── graphdb_query — query language gen_server
└── graphdb_rules — rule meta-ontology + create/retrieve + composition firing + propose mode + connection firing + conflict precedence
dictionary (application — started alongside graphdb)
└── dictionary_sup
├── dictionary_server — ETS-backed key-value store
└── term_server — ETS-backed term store
database (application — started after graphdb + dictionary)
└── database_sup — empty supervisor; attachment point for future
database-level services
seerstone (application — top-level; started last)
└── seerstone_sup — empty supervisor; placeholder for future
seerstone-specific workers
graphdb and dictionary are independent peer applications.
database_sup is intentionally empty — it serves as an attachment point for
any future database-level coordination services without reintroducing the
included_applications coupling.
Worker boundaries: each graphdb_* worker owns the schema/contract it
maintains. graphdb_mgr is the public entry point and routes to the
workers — read path and soft-retire implemented; remaining write-side
routing is pending (see ../TASKS.md).
The tier-3 batch entry point graphdb_mgr:mutate/1 applies an ordered list
of add_relationship / retire_node / unretire_node / update_node_avps /
remove_relationship / update_relationship / update_relationship_both
mutations atomically in one transaction, composing the tier-1 primitives
directly.
The system separates definitional knowledge from instance data.
| Body | Contents | Mutability |
|---|---|---|
| Ontology | Categories, attributes, classes, languages, templates, rules — the bootstrap scaffold and the live schema | Categories: immutable. All other nodes grow at runtime. |
| Project (instance space) | Instance nodes and their relationships — one database per project | Fully mutable |
The ontology is shared across all projects. The same ontology can serve unrelated domains. Project databases are independent — multiple may exist on the same node, each with its own Mnesia schema.
| Concept | Location |
|---|---|
| Category, attribute, class nodes | Ontology |
| Language class nodes; domain connection arcs | Ontology — see §10 |
| Permanent ontology instance seeds (e.g., English nref 10000) | Ontology — see §10 |
| Bootstrap and runtime compositional arcs | Ontology |
| Project anchor nodes (children of Projects nref 5) | Ontology |
| Language overlay tables for environment nrefs | Ontology node (language_<code>) |
| Instance nodes (project entities) | Project |
| Instance compositional arcs | Project |
| Instance → class membership arcs | Project |
| Instance user-defined connections | Project |
| Language overlay tables for project nrefs | Project node (language_<code>_<anchor_nref>) |
Nrefs are plain integer()s with no embedded database tag. Context
determines routing:
| Relationship field | Resolves to |
|---|---|
source_nref |
Same database as the relationship row |
characterization, reciprocal |
Always the ontology |
target_nref |
Routed by the arc label's target_kind AVP |
target_kind :: category | attribute | class | instance is stored as a
literal AVP on every arc-label attribute node. Built-in arc labels
(nrefs 21–30) carry it; graphdb_attr:create_relationship_attribute_pair/4
requires it for runtime additions.
This routing table is the code contract of the pure module graphdb_ns
(namespace_of/1, target_namespace/1 → environment | project | home) —
see the SP1 model below. Against today's single store it is behaviour-
preserving; SP2 gives it physical teeth.
Every graphdb_attr creator takes an explicit, validated ParentNref
(must name an existing kind=attribute node); the named functions
(create_name_attribute, create_literal_attribute,
create_relationship_type, create_relationship_attribute_pair) are thin
wrappers over the canonical create_value_attribute/4 (single node) and
create_relationship_attribute_pair/4 (reciprocal pair), defaulting the
parent to the appropriate scaffold subtree (6/7/8) when omitted.
Every project must have an anchor node in the environment as a
child of the Projects category. That node's environment nref is the
project's permanent cross-system identity token — used to scope
project-side language overlay tables and as the stable reference point
for cross-database arcs.
Projects may be remote: all project-side Mnesia tables (nodes,
relationships, per-language overlays) reside on the project's own
node, which may differ from the environment node. Mnesia handles
transparent remote access within a cluster; fully independent remote
projects are a future distribution concern.
Visibility of the anchor node is governed by ACL AVPs on that node (not yet implemented). Globally visible projects have no access restriction; owner-specific projects have a permissioned ACL. The node always exists regardless of its visibility.
The environment/project separation is a four-sub-project program
(design: designs/project-env-reference-namespace-model-design.md; tracking:
../TASKS.md → Multi-project sessions). SP1 is implemented at the API/code
layer only — no node/relationship record changes:
graphdb_ns— pure namespace-resolution module encoding the routing table above; every nref field resolves toenvironment | project | home.graphdb_project— project registry (register_project/1,is_project/1) creating the nref-5 anchor, plus the project session (open_session/1,session_project/1,require_session/1) and the canonical project-scoped relationship API surface. A session is an opaque value threaded as data — the workers are shared singletons, so project context cannot be ambient.- Required session on the project write path —
create_instance,add_relationship,remove_relationship,update_relationship(_both), andadd_class_membershiptake aSessionfirst arg and reject a missing/ invalid one with{error, invalid_session}. - Proxy contract — a cross-project link is a local node of the seeded
"Remote Reference" class carrying
remote_project/remote_nrefAVP payload; no structural reference crosses a project boundary. Recognized bygraphdb_instance:is_proxy/1/proxy_coordinates/1. Representation only; creation/dereference are SP2/SP3. - Namespace-agnostic in SP1 —
mutate/1and the instance reads (get_instance/children/compositional_ancestors/resolve_value), likeget_node/get_relationships, are not session-gated:mutate/1is a mixed env/project batch, and the reads are consumed bygraphdb_query. Their per-namespace routing lands in SP2.
SP2 (physical per-project store + allocator-from-1), SP3 (distribution / residency + proxy dereference), and SP4 (migration) remain.
Bootstrap introduces three nref tiers:
| Tier | Range | Contents |
|---|---|---|
| Scaffold | 1 – 9 999 | Pre-assigned category/attribute bootstrap nodes |
| Permanent concept seeds | ?LABEL_START – ?NREF_START − 1 |
English (10000), loader-assigned atom-labeled nodes starting at ?LABEL_START (10001), and worker init/1 seeds (graphdb_attr, graphdb_language sub-groups) |
| Runtime | ≥ ?NREF_START |
Post-boot allocations from nref_server — all instance/class/attribute runtime APIs, relationship row IDs |
Tier boundaries are the ?LABEL_START and ?NREF_START macros in
apps/graphdb/include/graphdb_nrefs.hrl (?LABEL_START = 10001,
?NREF_START = 1 000 000). They are not directives in bootstrap.terms
— that file contains only node and relationship terms.
Node-nref allocation is routed through graphdb_nref (first child of
graphdb_sup): during the init phase it hands out permanent-tier nrefs
computed from the nodes table; after the graphdb:start/2 phase flip it
delegates to nref_server:get_nref/0. The phase is held in
persistent_term so a process restart cannot resurrect the wrong phase.
graphdb:start/2 brackets the boot: it calls
graphdb_nref:set_permanent_phase/0 before graphdb_sup:start_link/0, so
the bootstrap loader and every worker init/1 allocate in the permanent
tier; after all child init/1s complete it calls
graphdb_nref:set_runtime_phase/0, which also raises the nref_server
floor to ?NREF_START.
graphdb_bootstrap assigns atom-labeled node nrefs from a local counter
starting at ?LABEL_START. If the counter would reach ?NREF_START the
loader throws {labels_exceeded_nref_start, ...}; the graphdb_nref
spillover path (raise floor and continue via get_nref) is not yet wired to
the loader. With ?NREF_START = 1 000 000 and ?LABEL_START = 10 001 the
permanent tier has roughly 990 000 free slots — spill-over is not expected.
Per-project; start at 1; no bootstrap floor. The project allocator layer is not yet implemented — when added, the simplest design mirrors the ontology allocator with a per-project DETS file. Numerical nref overlap with the ontology is not a problem because every lookup is routed to a specific database (see §6 cross-database resolution).
Single authoritative config: apps/seerstone/priv/default.config.
[{seerstone_graph_db, [
{app_port, 8080},
{log_path, "log"},
{data_path, "data"},
{bootstrap_file, "apps/graphdb/priv/bootstrap.terms"}
]},
{mnesia, [{dir, "data"}]}].Relative paths resolve from the OTP release root; absolute paths take
effect as-is. Mnesia reads dir from its own application env — no code
sets it.
Erlang terms via file:consult/1. Two term shapes (full schema in
graphdb_bootstrap.erl):
{node, Nref, Kind, {NameAttrNref, NameValue}, ExtraAVPs}.
{relationship, N1, R1, AVPs1, R2, N2, AVPs2, Kind}.Tier boundaries (?LABEL_START, ?NREF_START) are compile-time macros in
graphdb_nrefs.hrl — no {nref_start, N} or {label_start, N} directives
live in the file.
Nref (and endpoint fields N1, N2) may be either a pre-assigned
integer() or an atom label — a symbolic placeholder resolved at
load time. Atom labels allow mutable support nodes to be declared in
the bootstrap file without pre-assigning nrefs.
Hierarchy is encoded only in the relationship arcs — the node tuple
carries no parent field. Per-arc inline %% comments make the file
readable top-to-bottom.
Erlang Terms chosen over JSON / XML / custom DSL for zero added dependencies and direct pattern matching.
graphdb_bootstrap:load/0 is idempotent: creates Mnesia schema and
tables if absent, loads scaffold only if nodes is empty. Called from
graphdb_mgr:init/1. Processing is a two-pass sequence:
classify_terms— partition into{Nodes, Rels}; unknown terms are rejectedvalidate— accept integer nrefs< ?NREF_STARTand atom labels; reject unknown kindsvalidate_relationships— reject unknown relationship kindsbuild_symbol_table— allocate a permanent-tier nref for each unique atom label from a local counter starting at?LABEL_START(nonref_servercall)apply_symbol_table— substitute all atom labels with their allocated nrefsvalidate_no_unresolved_labels— sanity-check; no atom must survive resolutionwrite_nodes→write_relationships— write to Mnesiarebuild_caches+verify_caches— enforce the cache invariant (see §3)
Relationship IDs are allocated outside Mnesia transactions to avoid
retry side-effects. A verify mismatch in step 9 throws
{bootstrap_cache_invariant_failed, Mismatches} as a fatal startup
error.
category writes are permitted only inside graphdb_bootstrap. After
the loader finishes, graphdb_mgr rejects any runtime request to
create, modify, or delete a category node.
graphdb_instance:resolve_value/2 implements the four-level priority
order from TheKnowledgeNetwork.md §6:
- Local AVPs on the instance — highest.
- Class-bound values — every class membership in
node.classes; for each, walk the class itself plus its taxonomic ancestor DAG (graphdb_class:ancestors/1, BFS over multi-parent classes, nearest first). Per-membership hits are gathered as[{ClassNref, Value}]and reduced: a single distinct value wins ({ok, Value}); two or more distinct values produce{error, {ambiguous_class_value, AttrNref, Hits}}; zero hits fall through. - Compositional ancestors — unbroken upward walk via the
node.parentscache. Composition is a tree (one whole has at most one parent), so the walk is single-chain. - Directly connected nodes —
kind = connectionarcs only, one level deep — lowest.
Each level is consulted only if higher levels returned not_found.
The Languages category (nref 4) is the organisational root for all
communicative systems recognised by the knowledge network. A language, in
this model, is any system with grammar, syntax, and tokens or icons —
human natural languages, programming languages, diagram notations, and
rendering engines all qualify. Four subcategories are established at
bootstrap:
| Nref | Name | Domain |
|---|---|---|
| 32 | Human Languages | Natural languages spoken or signed by humans |
| 33 | Formal Languages | Programming languages, query languages, notations |
| 34 | Diagram Languages | UML, engineering schematics, tabular notation |
| 35 | Renderers | Rendering engines categorised by rendering mechanics |
The subcategory nodes (nrefs 32–35) are domain markers in the
organisational scaffold — analogous to Attributes and Classes — not
containers for language class nodes. The abstract class hierarchy for
language concepts lives under Classes (nref 3); see below.
Language nodes live in the ontology (see §6 for cross-database routing). The connection arcs from language class nodes to their domain subcategory (e.g., English → Human Languages, nref 32) are also written to the ontology.
Languages are not merely label lookup tables. Each language is a knowledge domain: a self-contained body of concepts covering grammar, syntax, vocabulary, and notation. In the knowledge network model:
- The abstract concepts — "Human Language", "Dialect", "Grammar Rule",
"Word", "Token", "Syntax Rule" — are class nodes in the ontology under a
Languagesuperclass seeded at runtime underClasses(nref 3). - Each specific language ("English", "German") and each dialect
("en_gb", "pt_br") is an instance node — an instance of
Human Languageor a more specific class. English is bootstrapped as a permanent ontology instance at nref 10000; all other language nodes are seeded at runtime bygraphdb_language:init/1orregister_language/2. Usingkind=instanceeliminates the dual-mechanism risk: instances do not participate in taxonomic IS-A arcs, so thebase_languageAVP is the sole authority for the base/dialect relationship. - The long-term shape is a dedicated project database per language,
populated with instances of
Word,GrammarRule,SyntaxRule, and related classes specific to that language.
The long-term consequence is that all concept names, attribute labels, and string values are ultimately compositions of instances in language projects. A label rendered for a node is not a stored string — it is a reference to a vocabulary instance in the appropriate language project, resolved through the inheritance chain. This is a direct expression of the self-referential nature of knowledge: the graph eventually describes itself in its own terms.
Domain membership is recorded by a lateral connection arc from each language class node to the appropriate subcategory (e.g., English → nref 32). The subcategory nodes are not parents in the class hierarchy; they are category anchors in the organisational scaffold.
The full language-project mechanism is a future capability. The current
implementation provides a pragmatic foundation: per-language Mnesia overlay
tables (language_en, language_de, …) that store per-attribute label
overrides keyed by nref. A language chain — an ordered list of language
codes with a resolution context — walks these tables left to right, falling
back to the terminal ontology node record.
This overlay mechanism is designed as a replaceable abstraction: when
language projects are built out, the backing will shift from flat per-nref
rows to traversal into project instance graphs, and the overlay tables will
become caches of that traversal. The resolve_label/3 API does not change
when the backing changes. See ../TASKS.md for the
remaining multilingual write-path work.
graphdb_query is the sole entry point for read-side traversal of the
graph. It is a gen_server peer to the other graphdb workers under
graphdb_sup.
Architectural shape:
- AST records in
apps/graphdb/include/graphdb_query.hrl(#q_get_node{},#q_get_arcs{},#q_describe{},#q_instances_of{},#q_find_path{}). - Session is a value-passed map carrying a snapshot timestamp and a read-through cache of node and arc reads.
- Sessions are snapshots:
refresh/1is the only invalidation path. Continuations are tagged with their issuing snapshot; resuming against a refreshed session returns{error, snapshot_expired}. - Mnesia access is funnelled through
session_read_node/2andsession_read_arcs/4. The executor never callsmnesia:dirty_*directly. find_pathis always bounded (caller suppliesmax_depth); reaching the bound returns{partial, Path, Continuation}for later resumption. The cont stores the originalmax_depthasremaining_depthso resume gets a fresh full budget.- Category-kind nodes (nrefs 1-5) are filtered as structural scaffold
in BFS expansion, matching the semantics already encoded in
graphdb_class:ancestors/1's NREF_CLASSES filter.
See designs/f3-graphdb-query-design.md for the durable architectural
contract.
graphdb_rules implements the rules data model and the firing engine:
storage and retrieval, taxonomy-walking effective-rules reads, the
composition firing engine, propose mode, connection firing, and
horizontal conflict precedence. The later firing-engine work — the
instantiation engine and reactive learning — remains, tracked in
../TASKS.md.
Architectural shape:
- A rule is a
kind = instancenode. Its class membership is one of two seeded meta-classes,CompositionRuleorConnectionRule, both subclasses of an abstractRuleroot (non-instantiable via theinstantiablemarker). The meta-ontology, aRule Literalsliteral sub-group, and theapplies_to/applied_byrelationship-attribute pair are seeded idempotently atinit/1;graphdb_rulesis the last child ofgraphdb_supsographdb_attrandgraphdb_classare ready. - Content vs deployment AVP split. Rule content (child/target class,
characterization, optional template) lives in the rule instance node's
AVPs. Rule deployment (
mode,multiplicity, and the owning class's defaultTemplateat AVP index 0) lives on the forwardapplies_toconnection arc from the owning class to the rule instance. - Attachment. Each rule is written in one Mnesia transaction: the
instance node, its instance↔class membership pair (chars 29/30), and
the
applies_to/applied_byconnection pair between owning class and rule.rules_for_class/2is direct-attachment only — it reads the owning class's outgoingapplies_toarcs.effective_rules_for_class/2additionally walks the class's taxonomy ancestors: a nearest-first, deployment-bearing gather of every rule attached to the class and its superclasses, grouped by attaching class. It resolves nothing — additive-vs-shadow is the firing engine's job (conflict precedence; see below). - Composition firing.
graphdb_instance:create_instance/3callsgraphdb_rules:plan_composition_firing/2to build an abstract plan tree, then executes it:mandatoryrules fire inside the same transaction as the parent;autorules fire post-commit. Return shape is{ok, Nref, Report}on success or{error, Reason, Report}on firing failure; pre- plan validation errors return{error, Reason}(2-tuple). The report is rule-centric:[#{rule, deployment, outcomes}]. - Propose mode.
propose-mode composition rules materialise nothing; they surface asproposedoutcomes in the same create report (always-in-report — no session flag). A caller accepts a proposal by issuing an ordinarycreate_instance/3for the proposed class. - Horizontal conflict precedence. When a class and its taxonomy
ancestors carry rules that reference the same concept (composition: the
same-or-descendant child class; connection: the same characterization +
same-or-descendant target class), a conflict resolver picks one
winner per group before firing: the nearest-level member by mode
priority (
mandatory>auto>propose), survivingMinis the winner's andMaxis the greatest across winner + dropped losers; a loser is demoted topropose(rather than dropped) only when both it and the winner carry a non-default template. The default policy is built bygraphdb_rules:default_conflict_resolver/0and applied per cascade level (composition) and per node (connection); it is deadlock-safe (reads only in-memory AVPs, dirtyrelationships, andgraphdb_class). Callers override the policy throughcreate_instance/5—/3and/4inject the default. - Scope. The API is scope-tagged (
environment|{project, _}). It serves theenvironmentscope;{project, _}creates are rejected and{project, _}reads return empty.
See designs/f4-graphdb-rules-design.md for the durable architectural
contract.