Skip to content

feat(edge): filters for edge list — --direction, --name, --to, --from (#337) - #340

Merged
shadowbrush merged 2 commits into
mainfrom
claude/edge-ls-filters
Aug 1, 2026
Merged

feat(edge): filters for edge list — --direction, --name, --to, --from (#337)#340
shadowbrush merged 2 commits into
mainfrom
claude/edge-ls-filters

Conversation

@shadowbrush

Copy link
Copy Markdown
Member

Closes #337.

The three scripts named in the issue are now single commands. Live, against the same memories:

edge ls review    -m …::mmdata --direction incoming             → 55 edges
edge ls preflight -m …::dev    --name routes-to                  → 15 edges
edge ls preflight -m …::dev    --to findings:prisma-upsert-…     → 019ed2e5327b7d10967208e01879be43

That last id is exactly the one I extracted by script when filing hadron-server#850, which is a decent check that the filter agrees with the hand-rolled pass it replaces.

Human output stays a table:

$ hadron edge ls preflight -m hadronmemory.com::dev --to findings:prisma-upsert-not-race-safe
DIR  REL        NODE                                  EDGE-ID
→    routes-to  findings:prisma-upsert-not-race-safe  019ed2e5327b7d10967208e01879be43

--to / --from are directional

Not "far endpoint matches" — --to names a node this one points at (outgoing), --from a node pointing at this one (incoming). That's the reading that makes them useful for walking a graph, and it means --to X needs no accompanying --direction.

Each matches the far endpoint by loc or id, since --json prints both and #339 just made ids first-class refs.

Contradictions are usage errors

Combinations that can only ever match nothing fail up front, rather than returning an empty list that reads like "no such edges":

$ hadron edge ls preflight -m …::dev --to x --direction incoming
hadron: --to selects outgoing edges, so it cannot be combined with --direction incoming

Also --to with --from (an edge has one far endpoint, and the two name opposite directions) and an unknown --direction.

A redacted endpoint never matches --to/--from

A cross-memory edge whose far endpoint the server redacted (#781) carries neither loc nor id. Matching on "" would have made every such edge look like the one asked for, so endpointIs requires a non-empty value on both sides. Those edges stay listable unfiltered and by --name, which is where they're still useful. Pinned by TestRedactedEndpointNeverMatches.

Compatibility

Purely client-side over what GetNode already returns — no new query, no server change. Filters only reduce rows; the row shape is untouched, and a test asserts a filtered row carries every field an unfiltered one does, so existing --json scripts keep working. The zero-value filter matches everything, so an unfiltered edge list is byte-identical to before (verified live: 20 edges on preflight, unchanged).

filterEdges always returns a non-nil slice so an empty result renders [], not null — per the repo's --json contract.

Verification

go test ./... — 18 packages green. make lint — 0 issues. gofmt clean.

The filter engine is pure, so it unit-tests without a server: every flag alone and combined, case-insensitive --name, --to/--from by both loc and id, the directional asymmetry (an incoming edge from X must not satisfy --to X), the redacted-endpoint guard, non-nil results, and each rejected combination with its message. Plus a command-level test over a fake server covering all eight filter cases end-to-end and the row-shape invariant.

agentic-usage.md's surface line lists the new flags.

🤖 Generated with Claude Code

…om (#337)

`edge list` returned every edge in both directions with no way to narrow, so
any question smaller than "all edges of this node" meant piping --json into a
script. Building and running `coding review|preflight lint` I wrote the same
shape of filter a dozen times, and filing hadron-server#845/#850 meant
extracting edge ids and labels by hand.

The three scripts named in #337 are now single commands:

  edge ls review    -m …::mmdata --direction incoming            → 55 edges
  edge ls preflight -m …::dev    --name routes-to                → 15 edges
  edge ls preflight -m …::dev    --to findings:prisma-upsert-…   → 019ed2e5327b7d1…

That last id matches what I extracted by script for hadron-server#850.

--to and --from are directional by construction: --to names a node this one
points AT (outgoing), --from a node pointing at THIS one (incoming). Each
matches the far endpoint by loc OR id, since --json prints both and #339 just
made ids first-class refs. Combinations that can only ever match nothing —
--to with --direction incoming, --to plus --from, an unknown --direction — are
usage errors rather than an empty list that reads like "no such edges".

A redacted cross-memory endpoint (#781) carries neither loc nor id, so it never
satisfies --to/--from; matching on "" would have made every such edge look like
the one asked for. It stays listable unfiltered and by --name.

Purely client-side over what GetNode already returns: no new query, no server
change, and the row shape is unchanged so existing scripts keep working — a
test asserts a filtered row carries every field an unfiltered one does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb92156afb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cmd/edge/ls.go
Comment on lines +64 to +67
if fl.To != "" && (e.Direction != "outgoing" || !endpointIs(e, fl.To)) {
return false
}
if fl.From != "" && (e.Direction != "incoming" || !endpointIs(e, fl.From)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject explicitly empty endpoint filters

When a shell variable expands to empty, such as --to "$TARGET" with TARGET unset, Cobra records an explicitly supplied empty value but these checks treat it as if the flag were absent. The command consequently returns every edge—including redacted endpoints—instead of rejecting the invalid reference or returning no matches; --from "" behaves the same way. Check whether each flag was supplied and reject an empty value before filtering.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 4c2bef4. I reproduced it before changing anything:

$ TARGET=""
$ hadron edge ls preflight -m hadronmemory.com::dev --to "$TARGET" --json | jq length
20
$ hadron edge ls preflight -m hadronmemory.com::dev --json | jq length
20        # identical — the filter silently vanished

Returning everything for a filter the caller meant to narrow with is the worst shape this could take, and it's silent. validate now takes the set of flags cobra saw as Changed, so an explicitly-supplied empty (or whitespace-only) value on any of --direction/--name/--to/--from is a usage error:

hadron: --to was given an empty value — omit the flag to not filter on it (a shell variable that expanded to nothing?)

I passed a provided map[string]bool rather than the pflag FlagSet so the rule stays unit-testable without pulling cobra into the pure layer. Covered at both levels: TestEmptyFilterValueIsRejected for the rule, TestEdgeListRejectsEmptyFilterValue end-to-end for all four flags.

While there I also trimmed filter values on apply, so --to " findings:x " from shell quoting still matches — TestFilterValuesAreTrimmed.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds client-side filtering capabilities to hadron edge ls so callers can narrow results without piping --json through scripts, while keeping the output row shape stable for existing automation.

Changes:

  • Introduces an edgeFilter with --direction, --name, --to, and --from flags, including validation for contradictory combinations.
  • Applies filtering to the edge list command output while preserving table output and --json schema.
  • Adds unit + command-level tests covering filter behavior, redacted endpoints, and error cases; updates agent-facing command-surface docs.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
internal/cmd/edge/ls.go Implements filter model/validation and applies filters to edge listing output.
internal/cmd/edge/ls_test.go Adds pure unit tests for filtering semantics and validation.
internal/cmd/edge_test.go Adds end-to-end command tests to pin behavior and row-shape invariants.
internal/cmd/agentic/agentic-usage.md Updates the agentic command-surface line to include new edge list flags.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/cmd/edge/ls.go
Comment on lines +82 to +90
func filterEdges(edges []edgeListDTO, fl edgeFilter) []edgeListDTO {
out := []edgeListDTO{}
for _, e := range edges {
if fl.match(e) {
out = append(out, e)
}
}
return out
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'd rather not, and I think the numbers support leaving it.

filterEdges runs once per command invocation, over one node's edges. The largest routers in these memories are 75 edges (mmdata preflight) and 55 (mmdata review); the dev router is 20. One slice of a few dozen small structs, once, is not a cost worth designing around — and edge list is a human/agent-facing command whose runtime is dominated by the GraphQL round trip that precedes it by orders of magnitude.

The short-circuit would also change the function's contract in a way that isn't free: returning the caller's slice on the zero-value filter makes filterEdges sometimes-aliasing and sometimes-copying. That's fine at today's single call site, which discards the input, but it's the kind of subtlety that bites whoever reuses it later — and the alternative (copy anyway) buys nothing.

So the trade reads as: a guaranteed-copy contract that's trivial to reason about, versus saving one allocation on a path that just did a network read. Happy to add it if you'd still prefer — it's three lines — but I don't think it earns the ambiguity.

Comment thread internal/cmd/agentic/agentic-usage.md Outdated
hadron search <query> [-m <memory>]... [--mode hybrid|keyword|vector|regex] [--prefix <loc>] [--type <type>] [--object-type <t>] [--tag <t>]... [--where <json>] [--sort-property <json>] [--limit N] [--offset N] [-l|--long] [--json]
hadron replace text <old> <new> --field <f> (--node <urn> | -m <memory>) [--prefix <loc>] [--regex] [-i] [--dry-run] [--yes] [--max-nodes N]
hadron edge list <node-urn> | add | update <edge-id> | rm <edge-id>
hadron edge list <node-urn> [--direction incoming|outgoing] [--name <substr>] [--to <ref>] [--from <ref>] | add | update <edge-id> | rm <edge-id>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 4c2bef4 — you're right that the line should be an accurate at-a-glance contract, and it was understating what the command takes. Now:

hadron edge list <node-urn> | <loc> -m <memory> | <node-id> [--direction …] [--name …] [--to …] [--from …] | add | update <edge-id> | rm <edge-id>

<loc> -m <memory> has worked all along and the line never said so; <node-id> is new as of #339. This also matches how the node line already spells out its accepted forms (get <urn>… | get <loc>… -m <memory>), so it's now consistent with its neighbour.

… line

Review follow-ups on #340.

An empty value alone can't be told from an absent flag, so `--to "$TARGET"`
with TARGET unset reached the filter as "" and was treated as "no filter":
the command returned every edge — 20 of 20 on the dev preflight router,
identical to unfiltered — which is the opposite of what the caller asked for,
and silent. validate now takes the set of flags cobra saw as Changed and
rejects an explicitly-supplied empty (or whitespace-only) value on any of
--direction/--name/--to/--from:

  hadron: --to was given an empty value — omit the flag to not filter on it
  (a shell variable that expanded to nothing?)

Filter values are also trimmed on apply, so a ref padded by shell quoting
still matches.

agentic-usage.md's surface line named only <node-urn>; `edge list` has taken
`<loc> -m <memory>` all along and a bare <node-id> since #339. That line is
meant to be an accurate at-a-glance contract, so it now lists all three.

validate takes a provided-set rather than the pflag FlagSet so the rule stays
unit-testable without cobra.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@shadowbrush
shadowbrush merged commit 635ff27 into main Aug 1, 2026
4 checks passed
@shadowbrush
shadowbrush deleted the claude/edge-ls-filters branch August 1, 2026 19:39
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.

edge list: no filters — every question needs a client-side pass over all of a node's edges

2 participants