What is left to build, grouped by area. None of these blocks the others in a strict gate the way earlier feature phases did; the rule-engine items build on one another, but the write-path, multi-project, and operational tracks are independent and can be picked up in any order.
The kernel is functional: bootstrap, the attribute library, the
taxonomic and compositional hierarchies with four-level inheritance,
templates, the multilingual overlay, the query language, and the rules
data model with its composition and connection firing engines have all
landed. Completed work — with its original phase labels and decision
logs — is archived in docs/archive/TASKS-DONE.md.
The canonical model is docs/TheKnowledgeNetwork.md;
the current architecture is docs/Architecture.md.
The rules data model, the composition firing engine (mandatory / auto /
propose modes), and the connection firing engine (resolver-driven
mandatory / auto / propose) are implemented in graphdb_rules and
graphdb_instance. The remaining work is the rest of the engine that
consumes the rule data: conflict resolution, the interactive
instantiation modes, and reactive learning. The durable design contract
is docs/designs/f4-graphdb-rules-design.md.
When a class and its taxonomy ancestors each attach a rule that touches
the same component type or connection, the effective-rules gather returns
them additively, nearest-first, and resolves nothing. Horizontal
conflict resolution is now applied at firing time by a conflict
resolver threaded through create_instance/5: the nearest-level member
of each conflict group wins by mode priority (mandatory > auto >
propose), surviving Min is the winner's and Max is the greatest across
winner + dropped losers, and a loser is demoted to propose only when it
and the winner both carry a non-default template. The default policy is
graphdb_rules:default_conflict_resolver/0 (injected by /3 and /4);
callers can override it via /5. Design
docs/designs/f4-phase-b5-conflict-precedence-design.md; the division is
also sketched in docs/designs/f4-graphdb-rules-design.md §11.
B5 follow-up — equidistant-diamond precedence. The nearest-level
resolution assumes a distinct owning class per taxonomic distance (a
linear ancestor chain). An equidistant multi-parent diamond — two
parents at the same taxonomic distance, each attaching a conflicting
rule on the same child — resolves by graphdb_class:ancestors/1 BFS
order rather than by mode-priority arbitration across the equidistant
parents. Revisit if equidistant-diamond ontologies become common.
Spec §9. Two creation modes, chosen by the ontology rather than the kernel:
- Guided — the engine presents valid options one attribute at a time, using ontology rules to constrain the choices at each step.
- Automatic — values are derived entirely from existing knowledge, with no user interaction.
Spec §11. The ontology grows from observed use, not only from explicit authoring:
- Naming-convention learning — when an attribute value is set, scan the other attribute-value pairs on the same instance for substring matches; when the new value is composed of existing attribute values, encode that pattern as a class-level rule so future instances populate the attribute automatically.
- Connection-pattern learning — when a connection is made, record
the
(source class, template, target class, connection type)tuple and accumulate the observations into connection rules that guide future connections of the same kind. - Report-driven learning — treat the firing report the engine
already emits (the
proposed/auto/required/connected/not_connectedoutcomes) as a feedback signal. Observe which proposals a caller accepts versus ignores, and whichrequiredconnections get satisfied after the fact, then feed the accumulated signal back into the rule set — adjusting a rule's mode or multiplicity, or promoting a recurring manually-made pattern into a new rule.
graphdb_mgr routes node and relationship creation to the workers. The
gaps that remain are node mutation, relationship mutation, the template
attribute list, and wiring the multilingual write path. They are
independent and broken into slices A–E below. graphdb_mgr owns the
generic low-level node/relationship CRUD; type-specific behaviour
delegates to the owning worker.
The decided convention for all write-path mutation: separate the Mnesia transaction boundary from the CRUD logic, so operations compose into one atomic transaction without nesting.
- Tier 1 — in-transaction primitives. Assume they already run inside
an Mnesia activity; do their reads + writes with bare Mnesia ops; signal
failure via
mnesia:abort/1. They never open a transaction, so they compose. - Tier 2 — single-op public API (e.g.
graphdb_mgr:delete_node/1). Owns the transaction: static guards, thenmnesia:transaction(fun() -> Primitive end), mapping{atomic, R}→{ok, R}and{aborted, Reason}→{error, Reason}. - Tier 3 — batch / composite (a future
mutate([Mutation]), or "delete an instance with its parts"). Wraps one transaction and calls the tier-1 primitives directly — never the tier-2 wrappers; no nested transactions.
This slice delivers the minimal seam only: the convention plus a
shared transaction-runner helper in graphdb_mgr, with tests proven
against a sample primitive. No existing write op changes. delete_node
and remove_relationship adopt it as their first consumers.
Tracked follow-ups (not in the seam spec):
-
Retrofit existing write ops — IMPLEMENTED. Full sweep: all 40
mnesia:transactionsites across the six workers + bootstrap now route throughgraphdb_mgr:transaction/1(the single{atomic,_}/{aborted,_}mapping point). Behaviour-preserving; existing tests unchanged, +2 new instance CT cases (characterization_not_found/reciprocal_not_foundarms). Designdocs/designs/transaction-seam-retrofit-design.md; plandocs/superpowers/plans/2026-06-20-transaction-seam-retrofit.md. -
Atomic
add_relationship— IMPLEMENTED.do_add_relationship/7's five separate transactions (validate endpoints → resolve classes → resolve template → validate scope → write) are collapsed into onegraphdb_mgr:transaction/1(TOCTOU isolation). The four single-use phase helpers were converted in place to in-txn (abort-based) form; a privateclass_of_in_txn/1was added (do_class_of/1keeps its own txn for its public caller);build_connection_rowswas split into/6(allocates) +/7(pure) so the rel-id pair is allocated up-front outside the transaction. Behaviour-preserving; existingadd_relationshipsuite unchanged, +2 new instance CT cases (source_has_no_class/target_has_no_class). Designdocs/designs/atomic-add-relationship-design.md; plandocs/superpowers/plans/2026-06-21-atomic-add-relationship.md. -
Batch
mutate([Mutation])— IMPLEMENTED. Tier-3 batch entry pointgraphdb_mgr:mutate/1: applies an ordered list ofadd_relationship/retire_node/unretire_nodemutations atomically in onegraphdb_mgr:transaction/1, composing tier-1 primitives directly. Opaque bare-reason contract ({ok, [ok, ...]}|{error, Reason}, whole-batch rollback,mutate([]) -> {ok, []}). Phase 2 resolves the seeded attr nrefs once and allocates one rel-id pair peradd_relationshipoutside the transaction; phase 3 folds the prepared list in order. Required one behaviour-preserving extraction —graphdb_instance:add_relationship_in_txn/9. Designdocs/designs/batch-mutate-design.md; plandocs/superpowers/plans/2026-06-24-batch-mutate.md.Deferred extensions (design §1.3): the mutation grammar now covers
add_relationship/retire_node/unretire_node/update_node_avps. Extend the grammar to the remaining mutation kinds —create_instance/create_class/create_attribute,delete_node(real hard delete),remove_relationship/update_relationship(slice E) — as each grows a tier-1 primitive (creates also need a txn-safe nref-allocation path, since today they allocate through a gen_server). Symbolic back-references between mutations (create A; relate A→B) are a further extension on top of those: they need the create primitives plus a bootstrap-style symbol table, and are out of scope until creates land. Per-mutation indexed error reporting was rejected on principle (design §3.3), not deferred — no entry needed. -
Converge default-template name search — IMPLEMENTED. The shared walk is now
graphdb_class:find_template_by_name_in_txn/2(exported tier-1 in-transaction primitive).default_template_in_txn/1delegates to it with?DEFAULT_TEMPLATE_NAME;do_find_template_by_name/2wraps onegraphdb_mgr:transaction/1around it (preserving the{error,_}->not_foundswallow). Behaviour-preserving; +3 CT cases. Designdocs/designs/converge-default-template-name-search-design.md; plandocs/superpowers/plans/2026-06-22-converge-default-template-name-search.md.
Design: docs/designs/delete-node-soft-retire-design.md. Delivered:
graphdb_mgr:retire_node/1 / unretire_node/1 (idempotent, permanent-tier
guard), graphdb_attr seeds the retired boolean marker, graphdb_instance
refuses retired nodes as new targets/parents/endpoints.
Decided policy: soft-retire, applied uniformly to all runtime nodes. Two
operations, graphdb_mgr:retire_node/1 and its inverse
graphdb_mgr:unretire_node/1, mark a node retired (a boolean retired
lifecycle AVP on the node row); the node and its arcs stay in Mnesia, and
the public get_node/1 returns {error, retired} for a retired node.
Because nothing is removed, no arc or cache is ever orphaned — so the
operation needs no environment-vs-project discriminator, and
refuse-if-referenced is not required for integrity. Retire additionally
blocks a retired node from taking on new participation (new instance
target/parent, new arc endpoint); existing structural participation is left
intact.
delete_node/1 is left untouched (still {error, not_implemented}) and
reserved for a future real (hard) delete; retire_node/unretire_node
refuse the whole permanent tier (nref < ?NREF_START) with a new
permanent_node_immutable atom. Built on the seam (transaction/1, merged
in PR #41).
This is forward-compatible with the planned history / versioning / bounded-lifetime feature: retirement is a degenerate lifetime bound, and a later purge pass under that feature reclaims retired nodes once it defines what is safe to forget — so mistakes are hidden now without being destroyed.
Superseded: the earlier refuse-if-referenced hard-delete policy. A
hard-delete fast-path for project instances — where dependencies are
local and knowable — is deferred behind the project-boundary work below
(it has no distinguishable node population until projects are physically
realized) and is where the reserved delete_node eventually lands.
Follow-ups this design adds:
- Retired rules must not fire. A retired
graphdb_rulesrule node is still reached through existing structure, so retiring it does not stop it firing. Exclude retired rule nodes at the firing read chokepoint (effective_rules_for_class/effective_connection_rules). Deferred from slice A to keep that slice scoped to the retire mechanism. - Unify permanent-tier immutability.
delete_node's category-only guard (category_nodes_are_immutable) is too narrow — categories are not the only permanent nodes. When the realdelete_nodelands, its guard (andupdate_node_avps') should refuse the whole permanent tier, consistent withretire_node'spermanent_node_immutable.
The environment/project split described in the knowledge model is not
physically realized. Today there is a single shared nodes /
relationships pair; instances draw nrefs from the environment runtime
allocator (graphdb_nref); and the Projects category (nref 5) is a bare
scaffold with nothing attached. Consequently a project instance is not
reliably distinguishable from an environment instance-kind node (e.g. a
rule), and there is no project-local identity space.
Until this exists, several things stay blocked or degraded:
- the delete hard-delete fast-path for project instances (slice A above);
- project-scoped rules (
graphdb_rulesreturnsproject_rules_not_yet_supported); - any per-project isolation, addressing, or lifecycle.
How projects are separated, identified, and addressed is an open architectural question to be brainstormed — this entry records the need and what it unblocks, not the solution.
Soft-retire (node deletion, slice A above) hides nodes without removing them, so retired rows accumulate. Reclaiming them is a separate asynchronous background operation — scheduled or explicitly triggered, never part of the synchronous delete path. It can run safely only once the planned history / versioning / bounded-lifetime feature defines what is safe to forget (e.g. a retired node past its lifetime bound with no live references). Scheduling, triggering, batching, and traversal are an open design — recorded here as a need, not a solution.
graphdb_mgr:update_node_avps/2 merges a list of AVP updates onto a node
atomically. Tier-2 wrapper owns one transaction/1; tier-1
update_node_avps_in_txn/3 does the in-txn work. Wired as the fourth
{update_node_avps, Nref, AVPs} kind in mutate/1. Design
docs/designs/slice-b-update-node-avps-design.md.
Follow-up (pre-existing, low priority) — category-node error-shape
divergence. A category node (nref 1–5) is rejected by the solo path with
{error, category_nodes_are_immutable} (the handle_call category guard)
but through mutate/1 with {error, permanent_node_immutable} (the static
tier_guard, since 1–5 < ?NREF_START). Both correctly refuse the write;
only the reason atom differs. The same divergence already exists for
retire_node / unretire_node through mutate/1. Normalising it would
mean teaching mutate/1's static validation to distinguish category nodes
from the rest of the permanent tier (a DB read in phase 1, which today does
no DB access) — not worth it unless a caller needs to branch on the
specific reason. Revisit if that need arises.
A class QC may be marked instance_only => true: the attribute is relevant
to instances, but binding a value at the class level is a category error.
Set via graphdb_class:add_qualifying_characteristic/3 (#{instance_only => true}) or a create_class/3 initial AVP. Enforced at three class-level
value-binding gates — bind_qc_value/3, create_class/3, and
update_node_avps/2 (the last covers mutate/1, both composing
update_node_avps_in_txn/3) — each returning {error, {instance_only_attribute, AttrNref}}. Enforcement is local to the class
node written. Design docs/designs/slice-c-instance-only-qc-design.md.
Deferred follow-ons (from slice C):
- Template attribute list — per-template subset/relevance scoping of
class attributes (
TheKnowledgeNetwork.md§7). A template currently carries only a name and its compositional arc into the owning class; there is no per-template list of which attributes it scopes. This is the per-class, per-template axis: the same attribute may be class-bindable in one class's template and instance-only in another's. - Template-bound (variant) values — templates carrying override values stamped into instances at instantiation (e.g. a later custom-colour phone variant whose colour is fixed in a template, not on the base class).
- Inherited instance-only enforcement (C2) — close the subclass-redeclare
bypass: a subclass can re-declare a parent's instance-only QC without the
flag via
add_qualifying_characteristic/2, then bind a value. Local gates do not consult the inherited QC set becausecollect_qc_avps/1flattens each QC to{AttrNref, Value}, dropping the marker. Closing it means carrying the flag throughcollect_qc_avps/1/inherited_qcs/1and having all three gates consult the effective (local + ancestor) QC set. - Marker mutability via the general update path — today
instance_onlyis settable only at QC declaration (add_qualifying_characteristic/3) orcreate_class/3; it can be neither set nor cleared throughupdate_node_avps/2/mutate/1. That restriction is a side effect of slice B's AVP well-formedness check (which rejects any update map whose key-set is not exactly[attribute]or[attribute, value]), not a deliberate long-term contract decision. Investigate whether toggling a QC's instance-only status — and QC-shape edits generally — should be a first-class mutation: a dedicated mutation kind, or a widened update grammar that admits marker keys, versus remaining declaration-time only. Decide and document the intended contract before any caller comes to depend on the current behaviour.
Design: docs/designs/slice-e-relationship-mutation-design.md; plan
docs/superpowers/plans/2026-06-28-slice-e-relationship-mutation.md.
Delivered, connection-arcs only (the exact mirror of add_relationship;
no parents/classes cache work — connection arcs are never cached):
remove_relationship/3,4— atomically delete both directed rows of a logical connection edge. (The earlier note that it "fixes the caches" and "shares the arc-removal primitive withdelete_node" was aspirational: slice A shipped soft-retire only, so no hard-delete primitive exists to share, and connection removal needs no cache fix.)update_relationship/4,5+update_relationship_both/4,5— AVP-only edit of an existing edge, reusing slice B'svalidate_avp_updates/1+apply_avp_updates/2. Single-direction is the one tier-1 primitive;*_bothcomposes it twice with independent{Fwd, Rev}lists. The?ARC_TEMPLATEscope AVP is protected from edit.
Because nothing dedups connection edges at write time, the identity contract
is: (S, C, T) (optionally narrowed by Template) matches a logical edge;
zero matches → relationship_not_found, more than one → ambiguous_relationship.
Remove is edge-level (both rows); AVP update is directed-row-level (one
row). Built on the transaction seam; all three kinds compose into
mutate/1.
Deferred (recorded in the design): structural rewiring (characterization /
target_nref / reciprocal — expressible as mutate([remove, add])); a
rel-id-keyed form (the only disambiguator for genuine duplicate edges).
Compositional-hierarchy ("part of") arc creators and removers between
instances: add_parent(Child, Parent) / add_child(Parent, Child) write a
kind=composition arc, and remove_parent(Child, Parent) /
remove_child(Parent, Child) delete it — all maintaining the child's
parents cache — the cache-touching counterpart that slice E
(connection-only) deliberately does not cover. The cache-maintenance core
here is also what a future delete_node hard-delete and a general
(kind-agnostic) arc remover would reuse. Built on the transaction seam;
tier-1 primitives + tier-2 wrappers + mutate/1 grammar, with
verify_caches/0 clean after each.
Now unblocked — the graphdb_mgr write-side is wired. When an
environment node is created, the write path must additionally:
- Create the node atomically in one Mnesia transaction.
- Post-commit and outside that transaction (best-effort), call every registered translation hook with the new nref and its English AVPs.
- If a session language list is supplied with labels, call
set_labels/3for each language.
Steps 2–3 are deliberately not atomically coupled to step 1: a failed hook or a missing language label does not roll back node creation. Do not auto-duplicate environment labels into dialect overlay tables — a dialect override is an explicit authoring decision, never inferred. Project- instance label writes depend on the multi-project work below (project- scoped overlay tables).
Two items deferred from the original multilingual work that have not yet landed.
lang_human (the root class for all human natural languages) is currently
a direct child of Classes (nref 3) with no intermediate superclass. The
architecture specifies a Language superclass node sitting above lang_human
under Classes. Two implementation paths:
- Option A — Add
Languageas a bootstrap node inbootstrap.termsand makelang_humana child of it there. Structurally cleanest; the node belongs to the permanent scaffold, not a worker'sinit/1. - Option B — Seed
Languageatgraphdb_language:init/1time and callgraphdb_class:add_superclass/2to placelang_humanunder it. No bootstrap change required.
Decide and implement one option.
When a language instance is created at runtime (e.g., French), it is not
automatically placed under the appropriate domain subcategory node (nrefs
32–35: Human Languages, Formal Languages, Diagram Languages, Renderers).
English is wired in bootstrap.terms directly; runtime-created languages
are not.
The connection firing engine is now implemented (graphdb_instance,
graphdb_rules). Add a connection rule to lang_human (and the equivalent
class nodes for the other language kinds) that fires at instance creation
and connects the new language instance to the correct subcategory. The
resolver is supplied via create_instance/4.
This is a four-sub-project program (design:
docs/designs/project-env-reference-namespace-model-design.md). SP1 (the
reference & namespace model) is done; SP2–SP4 remain.
At the API/code layer only, no node/relationship record changes:
graphdb_ns— pure module encoding the field-role namespace map (namespace_of/1,target_namespace/1): every nref reference resolves toenvironment | project | home. The code expression of design §3.graphdb_project— project registry (register_project/1,is_project/1) creating an anchor node underProjects(nref 5); project session (open_session/1,session_project/1,require_session/1); and the canonical project-scoped relationship API surface (env/project split).- Proxy representation contract — a seeded "Remote Reference" class (under
Classes, nref 3) +
remote_project/remote_nrefliteral attributes +graphdb_instance:is_proxy/1/proxy_coordinates/1recognizers. Cross- project links are local proxy nodes carrying remote coordinates as AVP payload; no structural reference ever crosses a project boundary. - Required project 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}. Behaviour-preserving against today's single store (the session is validated but inert until SP2).
SP1 deliberate deferrals (to SP2+):
mutate/1and the instance reads (get_instance/children/compositional_ancestors/resolve_value) stay namespace-agnostic in SP1 — likeget_node/get_relationships.mutate/1is mixed env/project (a project session would over-constrain env-only batches); the reads are consumed bygraphdb_query, so gating them would force the deferred query-session unification. Their per-namespace routing lands in SP2.proxy_coordinates/1assumes a well-formed proxy (both AVPs present); it can badmatch on a malformed proxy. Harmless until SP2 adds proxy creation; handle the missing-AVP case there.- Proxy-node creation API and dereference; private environment overlays
(a private overlay hiding a project's nref-5 anchor); session unification
with the
graphdb_querysession.
- Physical project store (SP2) — separate Mnesia table set / schema per project; per-project allocator from 1; the resolution seam gains real env-vs-project routing; the session binds to physical storage.
- Distribution & residency (SP3) — projects on separate nodes / locations; environment reachability or replication at each location; proxy dereference.
- Migration (SP4) — move existing instances out of the shared environment tables into project storage; reassign their nrefs.
- Session state may carry multiple
{ProjectId, AnchorNref}and a cross-project priority order; project-scoped overlay tables for rule instances and language labels (language_<code>_<anchor_nref>).
Open question — multi-class instance creation. create_instance
stays single-class: one primary driving class. Additional class
memberships are expressed as rules on the primary class. The
load-bearing question is whether the effective-rules gather should
recurse transitively into a conferred class's rules — which reframes
multi-class creation from an API-signature problem into a gather-
transitivity problem. A class-list / signature-widening framing was
considered and rejected; see docs/designs/f4-phase-b4-connection-firing-design.md §7.
No feature dependencies; interleave at any point.
Every write-path worker allocates node nrefs and relationship row IDs
outside the mnesia:transaction/1 that writes the rows, so the
transaction fun stays free of side-effects when Mnesia re-runs it on a
lock conflict. The deliberate cost is that an aborted transaction orphans
the already-allocated ids — harmless given the unbounded monotonic nref
space, but currently unmeasured.
Add a development helper that snapshots Mnesia's cumulative counters
(transaction_restarts, transaction_failures, transaction_commits,
transaction_log_writes via mnesia:system_info/1) around a fun and
returns the deltas. Decide whether the write paths warrant their own
per-callsite {atomic, _} / {aborted, _} counters or whether the
global counters suffice, and document the decision. Confirm the
allocate-outside-transaction rationale carries an inline comment at each
allocation site. Observability only — no behavioural change to the write
paths.
code_change/3 is unimplemented in every gen_server (nref_allocator,
nref_server, and all graphdb_* workers). Implement when the first
hot-upgrade deployment is planned — premature until there is a versioned
release to upgrade in place.
No .app.src defines start_phases, so start_phase/3 is never called.
Revisit when an externally-visible entry point (an API server or socket
listener) is added to seerstone that must not accept connections until
the full graphdb stack is bootstrapped — phased startup then closes the
window between port-open and data-ready.