Repository navigation
Fix relationship get double-reversal for reverseName + --direction in - #2822
Merged
Merged
Conversation
resolve_relationship_name resolved a declared reverseName (or a built-in's fixed inverse) to its stored forward name, then get_related_nodes flipped the caller's --direction on top of that already-reversed name. Requesting --direction in on a reverse name therefore landed on the forward relationship's true outbound side, which structurally can never have edges for a normal (non self-referential) declaration, so the call silently returned count: 0, exit 0 — indistinguishable from "no edges exist". A reverse name only ever names one traversal (the forward relationship, read from the target end), so the caller's direction has nothing to select once resolution has already picked it. get_related_nodes now always queries the resolved forward name with direction "in" for ResolvedRelName::Reverse, ignoring input.direction entirely for that case. Every traversal reachable through the old flip is still reachable directly via the literal forward name, so nothing is lost. Updates the test that had asserted the flip as intended, and adds regression coverage for the exact reported repro (Invoice.billed_to -> Customer, reverseName "invoices") plus the equivalent built-in case (child_of). Documents the corrected behavior in references/cli.md, which previously only showed the flip working for the forward-name spelling. closes #2791 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
For a normal (cross-type) declaration, the old double-reversal bug always resolved to a structurally empty query, so it never proved much on its own. A self-referential relationship is the one case where the flipped query could be real and non-empty — if the node also sits on the outbound side of the same relationship, the old code would have silently returned a plausible-looking but wrong edge instead of the correct one. Adds coverage for that case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- GetRelatedInput.direction: document that it's silently ignored once the name resolves as ResolvedRelName::Reverse, not just at the get_related_nodes doc comment ~300 lines away. - Note in the two tests comparing related_nodes across two calls that both calls now run the identical resolved query, so the comparison isn't relying on an unstated store ordering guarantee; points future extensions at the sorted-ids pattern already used elsewhere in the file for multi-row comparisons. Addresses two Important findings from /pragmatic-code-review on PR #2822. Other findings evaluated and skipped with reasoning (posted to PR): already-tracked packages/agent consequence (#2823), pre-existing InboundForward/validation behavior unrelated to this diff, and refactor/helper suggestions disproportionate to this bug-fix PR. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A Reverse-resolved name computed its direction internally without
ever re-checking the caller's original value, so a malformed
direction ("In", "", "both") would silently succeed instead of
surfacing the same "Invalid direction" error a Forward/Builtin/
InboundForward name gets downstream in
NodeService::get_related_nodes. Not a regression from the prior
commits in this branch (the old flip logic had the identical
property), but a real, closeable gap: validate direction is exactly
"in" or "out" up front in get_related_nodes, before any resolution
or DB work, so every branch is covered the same way.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Contributor
Author
Review response (
|
mstomar125
deleted the
issue-2791-relationship-get-reverse-direction
branch
September 22, 2026 11:34
This was referenced Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
nodespace relationship get <id> --type <reverseName> --direction inresolved the reverse name to its declared forward form, then flipped the caller's--directionon top of that already-reversed name. Requesting--direction inon a reverse name therefore landed on the forward relationship's true outbound side, which structurally can never have edges for a normal (non-self-referential) declaration — so the call silently returnedcount: 0, exit 0, indistinguishable from "no edges exist".Repro from the issue:
Invoice.billed_to → CustomerwithreverseName: "invoices".--type billed_to --direction inand--type invoices(defaultout) both correctly returned 2;--type invoices --direction inincorrectly returned 0.Root cause
packages/core/src/ops/rel_ops.rs::get_related_nodesresolves the supplied name viaresolve_relationship_name, then for aResolvedRelName::Reversematch, flippedinput.direction("in"→"out", else"in"). Since the default/no-flag path ("out"→ flipped to"in") is the only one ever exercised in practice, this "flip the requested direction" design meant an explicit--direction indoubled the reversal instead of behaving as a no-op / intensifier, which is what a caller reasonably expects from a name that already reads as "the reverse".Fix
A reverse name — a declared
reverseNameor a built-in's fixed inverse (child_of, etc.) — names exactly one traversal: the forward relationship, read from the target end. There is no second, meaningful direction it could mean, so--directionis now ignored once a name resolves asResolvedRelName::Reverse:get_related_nodesalways queries the resolved forward name with direction"in", regardless of what was requested. Every traversal previously reachable through the old flip is still reachable directly via the literal forward name (--type billed_to/--direction out), so nothing is lost — this was also already howpackages/core/src/playbook/graph_resolver.rs's sibling resolution path (used by Play conditions) treated a reverse name, so this bringsrel_ops::get_related_nodesin line with it.This also fixes the identical double-reversal for a built-in's reverse spelling (e.g.
child_of --direction in), which shares the sameResolvedRelName::Reversecode path.Changes
packages/core/src/ops/rel_ops.rs— drop the direction-flip forResolvedRelName::Reverse; always resolve to(forward_name, "in"). Updated doc comments to explain why direction plays no part in this case.packages/core/tests/relationship_reverse_name_traversal_test.rs:reverse_name_flips_the_requested_direction(renamedreverse_name_ignores_the_requested_direction) — it previously asserted the buggy flip as intended behavior.reverse_name_with_direction_in_matches_the_issue_repro— the exact Invoice/Customer repro from the issue, asserting all three spellings (billed_to --direction in,invoices,invoices --direction in) return the same 2 nodes.builtin_reverse_name_ignores_the_requested_direction— the same fix for a built-in's reverse spelling.self_referential_reverse_name_with_direction_in_does_not_return_the_wrong_edge— for a normal cross-type declaration the old bug always flipped to a structurally empty query, which doesn't prove much on its own; for a self-referential relationship the flipped query can be real and non-empty, so the old code would have silently returned a plausible-looking but wrong edge instead of a bare zero. Covers that case specifically.packages/skill/references/cli.md— corrected the--directionoption docs and the "Traversing the reverse direction" section to state that--directionis ignored once--typeresolves to a reverse name, with a worked example.Test plan
cargo test -p nodespace-core --test relationship_reverse_name_traversal_test— 14 passed (including the 4 new/updated regression tests)cargo test -p nodespace-core --test schema_relationship_declarations_test— 15 passedcargo test -p nodespace-core --test playbook_engine_integration_test --test relationship_editing_test --test core_task_link_relationships_test --test collection_membership_test --test object_valued_property_roundtrip_test— all passed (confirms the playbook graph-resolver path, which already used the correct fixed-direction behavior, is unaffected)bun run quality:fixclean (cargo fmt,cargo clippy --all-targets -D warnings -D clippy::unused-async)test:all+cargo build --bin nodespaced+test:e2e) passed clean on pushKnown downstream consequence (tracked separately)
packages/agent'sget_related_nodestool has adirection: "both"fan-out that queries"out"and"in"separately and concatenates with no dedup. That only worked for a reverse-resolved name by accident, as a side effect of the bug this PR fixes (the"in"call used to return near-always-empty results). After this fix both calls return the identical, correct result for a reverse-resolved name, so"both"now double-reports each related node. This can't be fixed insiderel_opswithout breaking the CLI's own default (--directiondefaults to"out", indistinguishable from an explicit"out"by the time it reachesrel_ops::get_related_nodes, and the issue's own acceptance criteria require the default-out/no-flag spelling to keep returning real data) — it needs to be fixed inpackages/agent's own fan-out (dedup by node id, or don't fan out to both directions once a name resolves as reverse). Filed as #2823, unassigned —packages/agentis a separate ownership lane needing its own sign-off, out of scope here.Closes #2791
🤖 Generated with Claude Code