Skip to content
Merged
25 changes: 18 additions & 7 deletions TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ SP1/SP2 without one). 15 call sites across 11 test functions in
found zero production callers, so this is an API-completeness gap, not a
live bug — pick it up when a project-side relationship-read caller appears.

**Open defect (Important) — query traversal silently truncates
**RESOLVED (query-traversal Home routing) — query traversal silently truncated
environment-only paths under a project-bound session.**
`graphdb_query:session_read_arcs/4` and `is_scaffold_node/2` push every
*arc-discovered* nref through `resolve_home/2`, which was designed for
Expand All @@ -552,12 +552,23 @@ shadows nref 6 and any with ≥35 shadows the whole bootstrap scaffold, so
this is high-likelihood, not theoretical. The result is silently wrong: no
error, only a collision warning in the log.

Fix needs a design decision, not a local patch: arc-discovered nrefs *do*
have characterization context, so they should route via
`graphdb_ns:target_namespace/2` on the arc's `target_kind` rather than
guessing — with the 29/30 class↔instance membership pair as an explicit
exception, since its source and target deliberately live in different
Homes. Deferred out of SP2 as a scoped design task.
**Implemented.** Half A: `graphdb_ns:arc_target_namespace(Home, Kind, Char)`
derives an arc-discovered nref's store from `#relationship.kind`, with the
29/30 membership pair split on characterization. Half B: the BFS frontier,
visited set, and target comparison are Home-qualified via
`home_id() :: environment | {project, Anchor}`; path edges disclose `home`
when a hop crosses stores; `resume/2` rejects a continuation carrying a
foreign project id. `resolve_home/2` and `session_read_arcs/4` are unchanged
— only `bfs_step/5` changed caller, to `session_read_arcs_home/5`. Design:
`docs/designs/query-traversal-home-routing-design.md`.

Still open, deliberately out of that scope: BFS at an environment-homed node
reads only the environment relationship table, so an environment class cannot
reach its project instances across arc 30 (`?ARC_CLASS_TO_INST`).
`#q_instances_of{}` keeps its `session_read_arcs_home/5` special case. Also
still open: backfilling `target_kind` onto bootstrap arc labels 21–30, which
would let `graphdb_instance:check_target_kind/3` drop its permissive legacy
arm.

**Open defect (Important, pre-existing, unrelated to SP2) —
`rel_id_server:seed_from_mnesia/0` calls a nonexistent function.**
Expand Down
36 changes: 24 additions & 12 deletions apps/graphdb/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
| `graphdb_sup.erl` | OTP `supervisor` behaviour callback module |
| `graphdb_nref.erl` | Switchable node-nref allocation facade gen_server (first child; permanent during init) |
| `graphdb_bootstrap.erl` | Bootstrap file loader + Mnesia schema creator (implemented) |
| `graphdb_ns.erl` | Pure namespace-resolution module (SP1+SP2) — `namespace_of/2`, `target_namespace/2` (home-relative), `node_table/1`, `rel_table/1`; the code expression of the field-role namespace map |
| `graphdb_ns.erl` | Pure namespace-resolution module (SP1+SP2) — `namespace_of/2`, `target_namespace/2` (home-relative), `node_table/1`, `rel_table/1`, `arc_target_namespace/3` (arc-discovered nref routing by `#relationship.kind`); the code expression of the field-role namespace map |
| `graphdb_project.erl` | Project registry + physical store (SP1+SP2) — `register_project/1` (also creates the project's three tables), `is_project/1`, `open/1`, `require_project/1`, `next_nref/1`, `next_rel_id_pair/1`; canonical project-scoped relationship API surface |
| `graphdb_mgr.erl` | Primary coordinator gen_server (implemented — bootstrap init, read API, category guard) |
| `graphdb_rules.erl` | Graph rules gen_server (implemented — F4 Phase A+B1+B2+B3+B4+B5: rule meta-ontology, create/retrieve, taxonomy walk, composition firing, propose mode, connection firing, conflict precedence) |
Expand Down Expand Up @@ -113,11 +113,13 @@ namespace model) and SP2 (physical project store) are both implemented:
`update_node_avps`) stay environment-only; `mutate/1` in particular stays
environment-only by design (a batch is inherently single-table once the
physical split exists).
- **`graphdb_query` sessions bind a `Project`** — `new_session/1`. Every
bare-nref read resolves its `Home` via `resolve_home/2`: try the bound
project's table first, fall back to the environment, and log if the nref
genuinely exists in both (a real collision, resolved in the project's
favor on the theory that a project-bound session is evidence of intent).
- **`graphdb_query` sessions bind a `Project`** — `new_session/1`.
Bare-nref **entry-point** reads resolve their `Home` via `resolve_home/2`:
try the bound project's table first, fall back to the environment, and
log if the nref genuinely exists in both (a real collision, resolved in
the project's favor on the theory that a project-bound session is
evidence of intent). Arc-discovered nrefs during BFS route
deterministically instead, via `graphdb_ns:arc_target_namespace/3`.
- **Proxy contract** — cross-project links are local nodes of the seeded
"Remote Reference" class carrying `remote_project` / `remote_nref` AVP
payload; no structural reference crosses a project boundary. Recognized by
Expand Down Expand Up @@ -437,17 +439,27 @@ Parses and executes graph queries. Public API:
- `new_session/0`, `new_session/1`, `refresh/1` — snapshot-semantics session lifecycle. `new_session/1` binds the session to a `Project`; `new_session/0` stays environment-only.
- `execute_query/1`, `execute_query/2` — ephemeral and session-threaded
- `resume/2` — continue a `#cont_path{}` (returns
`{error, snapshot_expired}` if the session has been refreshed since)
`{error, snapshot_expired}` if the session has been refreshed since,
`{error, invalid_project}` for a malformed session `Project` handle, or
`{error, session_project_mismatch}` for a continuation whose target/frontier
carry a home id foreign to the session)
- `find_path/3` — convenience wrapper for `#q_find_path{}`

Queries are represented as records defined in
`apps/graphdb/include/graphdb_query.hrl`. Every Mnesia read goes
through `session_read_node/2` or `session_read_arcs/4`; direct
`mnesia:dirty_*` calls outside those helpers are a code smell. Every
bare-nref read resolves its physical table via `resolve_home/2` (SP2):
try the session's bound `Project` first, fall back to the environment,
and log a warning if the nref genuinely exists in both — the project's
copy wins on the theory that a project-bound session is caller intent.
`mnesia:dirty_*` calls outside those helpers are a code smell. Bare-nref
**entry-point** reads (`#q_get_node{}`, `#q_get_arcs{}`, `#q_describe{}`,
and `#q_find_path{}`'s two endpoints) resolve `Home` via `resolve_home/2`
(SP2): try the session's bound `Project` first, fall back to the
environment, and log a warning if the nref genuinely exists in both — the
project's copy wins on the theory that a project-bound session is caller
intent. Arc-discovered nrefs found mid-traversal during BFS route
deterministically instead, via `graphdb_ns:arc_target_namespace/3`
(keyed on `#relationship.kind`, with the 29/30 membership pair split on
characterization) — no guessing. `#q_find_path{}` state (frontier,
visited set, target) is Home-qualified via `home_id()`, and a path edge
discloses `home` when a hop crosses stores.

See `docs/designs/f3-graphdb-query-design.md` for the architectural contract.

Expand Down
19 changes: 15 additions & 4 deletions apps/graphdb/include/graphdb_query.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,29 @@
arc_kinds :: [arc_kind()]
}).

%% -- Home identity ----------------------------------------------------
%% Compact, comparable form of a Home: the atom `environment`, or a
%% project named by its anchor nref. Deliberately NOT the full Project
%% handle -- that carries physical Mnesia table atoms, which have no
%% business in continuation state or in query results.
-type home_id() :: environment | {project, integer()}.

%% -- Continuation -----------------------------------------------------
%% Returned by bounded queries (currently only Q6). Tagged with the
%% snapshot it was issued against; resuming with a mismatched session
%% returns {error, snapshot_expired}.
%%
%% Every nref here is Home-qualified: a bare nref is unique only within
%% a Home, so project-6 and environment-6 must never collapse into one
%% visited entry.
-record(cont_path, {
snapshot_at :: erlang:timestamp(),
target :: integer(),
target :: {home_id(), integer()},
arc_kinds :: [arc_kind()],
remaining_depth :: non_neg_integer(),
visited :: #{integer() => true},
%% [{Nref, PathToHere}] — frontier nodes to expand on resume
frontier :: [{integer(), [map()]}]
visited :: #{{home_id(), integer()} => true},
%% [{HomeId, Nref, PathToHere}] — frontier nodes to expand on resume
frontier :: [{home_id(), integer(), [map()]}]
}).

-endif.
53 changes: 52 additions & 1 deletion apps/graphdb/src/graphdb_ns.erl
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@
%% SP2: home-relative routing. namespace_of/1 and target_namespace/1
%% replaced by /2 forms taking a Home (environment | project handle).
%% node_table/1 and rel_table/1 added.
%% Rev PA3 Date: 2026-08-09 Author: David W. Thomas
%% arc_target_namespace/3 added -- routes an arc-discovered nref's Home
%% off #relationship.kind/characterization instead of target_kind, so
%% graphdb_query's BFS can stop re-guessing Home mid-traversal.
%%---------------------------------------------------------------------

-module(graphdb_ns).
-include_lib("graphdb/include/graphdb_nrefs.hrl").

-export([namespace_of/2, target_namespace/2, node_table/1, rel_table/1]).
-export([namespace_of/2, target_namespace/2, arc_target_namespace/3,
node_table/1, rel_table/1]).

%%---------------------------------------------------------------------
%% NYI / UEM Macros
Expand Down Expand Up @@ -70,6 +76,51 @@ target_namespace(_Home, class) -> environment;
target_namespace(Home, instance) -> Home.


%%---------------------------------------------------------------------
%% arc_target_namespace(Home, ArcKind, Characterization)
%% -> environment | Home
%%
%% The arc-row analogue of namespace_of/2, for a traversal that has an
%% arc in hand. `Home` is the store the arc row was READ from.
%%
%% Unlike a bare nref, an nref reached BY TRAVERSING AN ARC is not
%% ambiguous: #relationship.kind and .characterization are present on
%% every row and together determine the target's store outright. This
%% is what lets graphdb_query stop guessing mid-traversal.
%%
%% taxonomy -- class/attribute refinement; projects hold only
%% instances, so the target is always environment.
%% composition -- home-relative: category/class composition is
%% environment<->environment, instance composition is
%% project<->project.
%% connection -- instance<->instance inside one store.
%% instantiation -- the 29/30 membership pair. BOTH rows live in the
%% project's table, but 29 (instance->class) targets
%% an environment class while 30 (class->instance)
%% targets a project instance. This is the one arc
%% shape whose ends deliberately differ in Home, and
%% the characterization is exactly what says which
%% direction we are walking.
%%
%% Deliberately total. A caller of this module is the graphdb_query
%% singleton; a function_clause here would take the query server down
%% for every session over one malformed row, so an unrecognised shape
%% logs and degrades to same-store (which is what that row did before
%% this function existed).
%%---------------------------------------------------------------------
arc_target_namespace(_Home, taxonomy, _Char) -> environment;
arc_target_namespace(Home, composition, _Char) -> Home;
arc_target_namespace(Home, connection, _Char) -> Home;
arc_target_namespace(_Home, instantiation, ?ARC_INST_TO_CLASS) -> environment;
arc_target_namespace(Home, instantiation, ?ARC_CLASS_TO_INST) -> Home;
arc_target_namespace(Home, Kind, Char) ->
logger:warning(
"graphdb_ns: unroutable arc kind ~p (characterization ~p) -- "
"defaulting to same store ~p",
[Kind, Char, Home]),
Home.


%%---------------------------------------------------------------------
%% node_table(Home) -> atom()
%% rel_table(Home) -> atom()
Expand Down
Loading
Loading