Skip to content

Query traversal: Home routing for arc-discovered nrefs - #55

Merged
david-w-t merged 9 commits into
davidwt-com:mainfrom
david-w-t:develop
Aug 9, 2026
Merged

Query traversal: Home routing for arc-discovered nrefs#55
david-w-t merged 9 commits into
davidwt-com:mainfrom
david-w-t:develop

Conversation

@david-w-t

Copy link
Copy Markdown
Contributor

Closes the defect filed against PR #53 (comment) and recorded in TASKS.md: #q_find_path{} silently returned {ok, no_path} for a path that exists.

The defect

resolve_home/2 resolves a bare nref by trying the session's bound project first and falling back to the environment. That guess is correct and deliberate for entry points, where the query language has no characterization context to route on. But session_read_arcs/4 and is_scaffold_node/2 applied it to arc-discovered nrefs too, re-deciding the store for every node BFS reached.

Project allocators start at 1, so any project with ≥6 instances shadows bootstrap nref 6 and ≥35 shadows the whole scaffold. Two runtime environment attributes whose only route runs through nref 6:

environment session     => {ok, [A1 -23-> 6, 6 -24-> A2]}
project,  1 instance    => {ok, [A1 -23-> 6, 6 -24-> A2]}
project, 21 instances   => {ok, no_path}          <-- silently wrong

Expanding 6, the walk resolved it to the project's instance 6, read relationships_<Anchor>, found no taxonomy arcs, and the frontier emptied.

A second failure, not in the original report

Tracing turned up a worse case. Visited was keyed by bare nref and the target test compared bare integers, so with to = 6 under a shadowing project a walk reaching the environment's attribute 6 reported {found, Path} — a fabricated path claiming to have reached the project's instance 6. A wrong answer rather than a missing one.

The fix

Half A — arc-discovered nrefs stop being ambiguous. New pure graphdb_ns:arc_target_namespace(Home, Kind, Char) derives the target's store from the arc traversed, keyed on #relationship.kind. The instance↔class membership pair is the one arc shape whose ends deliberately live in different Homes, and its characterization is exactly what says which direction is being walked:

arc_target_namespace(_Home, taxonomy,      _C) -> environment;
arc_target_namespace( Home, composition,   _C) -> Home;
arc_target_namespace( Home, connection,    _C) -> 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) -> %% logged; degrades to same store

The catch-all is deliberate: this is reachable from the graphdb_query singleton, where a function_clause would take the query server down for every session over one malformed row.

Half B — traversal state is Home-qualified. home_id() :: environment | {project, Anchor} now keys the visited set, the frontier, #cont_path{}, and the public result. resume/2 gains a caller-side validate_cont_homes/2 returning {error, session_project_mismatch}.

A path edge gains home iff the hop crosses stores — absent means "same store as the previous hop". The value is a home_id(), never the raw project handle, so physical table atoms stay out of query results.

Correction to the fix direction I filed on #53

That comment said to route via graphdb_ns:target_namespace/2 on the arc label's target_kind AVP. That does not work. Bootstrap arc labels 21–30 carry no target_kindbootstrap.terms:122-131 creates all ten with [], and graphdb_attr:init/1 retro-stamps only attribute_type. Arcs 23/24 are the decisive hops in the repro, so the lookup fails precisely where routing matters. docs/Architecture.md asserted the same false premise and is corrected here.

Not changed, deliberately

resolve_home/2, session_read_arcs/4, and session_read_node/2 are untouched. Entry points still use the intent-following guess, and resolve_home_prefers_project_and_logs_on_collision plus the whole q6_find_path group pass unmodified — the executable proof that entry-point behaviour is intact. Only bfs_step/5 changed caller, to the pre-existing session_read_arcs_home/5.

Still open, filed in TASKS.md

  • BFS at an environment-homed node reads only the environment relationship table, so an environment class cannot reach its project instances across arc 30. #q_instances_of{} keeps its special case.
  • Backfilling target_kind onto arc labels 21–30, which would let graphdb_instance:check_target_kind/3 drop its permissive legacy arm.

Verification

551 CT + 151 EUnit, rebar3 xref clean with no new suppressions, compile and as test compile zero warnings.

Reverting Half A reproduces the filed no_path and surfaces the cache entry {arcs, #{anchor => …}, 6, outgoing, [taxonomy]} => [] — the environment's nref 6 read from the project's table. Reverting Half B's target qualification reproduces the fabricated one-edge path. Neither new test passes vacuously.

Design: docs/designs/query-traversal-home-routing-design.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF

david-w-t and others added 9 commits August 8, 2026 21:24
Specs the fix for the SP2 defect filed in PR davidwt-com#53: graphdb_query's BFS
re-guesses which store each frontier node lives in, so a project with >=6
instances shadows bootstrap nref 6 and an environment-only path returns
{ok, no_path}.

Two halves. (A) arc-discovered nrefs route through a new pure
graphdb_ns:arc_target_namespace(Home, Kind, Char) keyed on
#relationship.kind, with the 29/30 membership pair distinguished by
characterization. (B) the BFS frontier, visited set, and target comparison
become Home-qualified -- without B, project-6 and environment-6 collapse
into one visited entry and the bare-nref target test reports a false found.

Corrects the fix direction recorded in the PR davidwt-com#53 comment and in TASKS.md,
which said to route via graphdb_ns:target_namespace/2 on the arc's
target_kind. Bootstrap arc labels 21-30 carry no target_kind AVP, and arcs
23/24 are the decisive hops in the repro, so that lookup fails exactly
where routing matters most.

Design only -- no code changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Five tasks, TDD throughout. T1 graphdb_ns:arc_target_namespace/3 + EUnit;
T2 Home-qualified BFS (the headline invariant + the false-found case);
T3 home key on cross-store edges; T4 resume/2 continuation-Home gate;
T5 docs + verification gate.

Records one deliberate deviation from the design: its T3 (visiting
project-6 must not suppress environment-6) is not constructible under
this scope, since reaching both keys in one walk needs a store crossing
and the only one available lands on a class, from which the environment
attribute subtree is unreachable by taxonomy. Replaced by T3', which
asserts the same Half-B property observably via #cont_path{} state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Adds validate_cont_homes/2, a caller-side gate on resume/2 that runs
after validate_session_home/1 and before the gen_server:call. Every
home_id() a #cont_path{} carries (target + frontier) must resolve
against the resuming session's own bound project; a mismatch returns
{error, session_project_mismatch} instead of reaching home_of_id/2
inside the graphdb_query singleton.

Closes two review findings tracked from Task 2:
- a continuation resumed under a project-less session no longer risks
  maps:get(project, Session) raising {badkey, project} inside
  handle_call/3 and killing the singleton
- a continuation resumed under a *different* project is rejected
  instead of home_of_id/2 silently discarding the anchor and reading
  the wrong project's tables

Adds three regression tests: t3_continuation_state_is_home_qualified
and t6_resume_round_trip_under_project_session (pass on arrival --
Task 2 already delivered this shape) and
resume_rejects_foreign_project_continuation (drives this fix).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
…frontier gate

Six findings from the whole-branch final review:

1. docs/Architecture.md: correct the false claim that bootstrap arc labels
   21-30 carry target_kind (they don't -- only runtime-created relationship-
   attribute pairs do); note arc-discovered traversal routes via
   graphdb_ns:arc_target_namespace/3 instead, and that target_namespace/2
   has no production caller.
2. graphdb_query.erl: rewrite resume/2's stale header comment -- it no
   longer dispatches through resolve_home/2; validate_session_home/1 is
   still load-bearing because validate_cont_homes/2 depends on it running
   first.
3. apps/graphdb/CLAUDE.md: document resume/2's {error, invalid_project}
   and {error, session_project_mismatch} returns alongside the existing
   {error, snapshot_expired}.
4. graphdb_query.erl: validate_cont_homes/2 folds a frontier-element shape
   check into its lists:all/2 pass, closing a gap where a wrong-arity
   frontier tuple was silently dropped by the old list-comprehension
   generator instead of failing the gate. Adds
   resume_rejects_malformed_frontier_continuation/1 to
   graphdb_query_SUITE's sp2_traversal_home_routing group, asserting both
   the clean error and that the graphdb_query singleton pid survives.
5. graphdb_ns.erl: Rev PA3 revision-history entry for arc_target_namespace/3.
6. graphdb_query.erl: remove the dead `[] -> []` arm in bfs/8's
   depth-exhausted clause -- unreachable because the first bfs/8 clause
   already matches an empty frontier for any depth.

Verification: rebar3 compile / as test compile zero warnings, rebar3 xref
clean, make test-ct-parallel 551/551, rebar3 eunit 151/151.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF

@david-w-t david-w-t left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok.

@david-w-t
david-w-t merged commit 59beb97 into davidwt-com:main Aug 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant