feat(edge): filters for edge list — --direction, --name, --to, --from (#337) - #340
Conversation
…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>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
There was a problem hiding this comment.
💡 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".
| if fl.To != "" && (e.Direction != "outgoing" || !endpointIs(e, fl.To)) { | ||
| return false | ||
| } | ||
| if fl.From != "" && (e.Direction != "incoming" || !endpointIs(e, fl.From)) { |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
edgeFilterwith--direction,--name,--to, and--fromflags, including validation for contradictory combinations. - Applies filtering to the
edge listcommand output while preserving table output and--jsonschema. - 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.
| func filterEdges(edges []edgeListDTO, fl edgeFilter) []edgeListDTO { | ||
| out := []edgeListDTO{} | ||
| for _, e := range edges { | ||
| if fl.match(e) { | ||
| out = append(out, e) | ||
| } | ||
| } | ||
| return out | ||
| } |
There was a problem hiding this comment.
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.
| 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> |
There was a problem hiding this comment.
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>
Closes #337.
The three scripts named in the issue are now single commands. Live, against the same memories:
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:
--to/--fromare directionalNot "far endpoint matches" —
--tonames a node this one points at (outgoing),--froma node pointing at this one (incoming). That's the reading that makes them useful for walking a graph, and it means--to Xneeds no accompanying--direction.Each matches the far endpoint by loc or id, since
--jsonprints 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":
Also
--towith--from(an edge has one far endpoint, and the two name opposite directions) and an unknown--direction.A redacted endpoint never matches
--to/--fromA 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, soendpointIsrequires a non-empty value on both sides. Those edges stay listable unfiltered and by--name, which is where they're still useful. Pinned byTestRedactedEndpointNeverMatches.Compatibility
Purely client-side over what
GetNodealready 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--jsonscripts keep working. The zero-value filter matches everything, so an unfilterededge listis byte-identical to before (verified live: 20 edges onpreflight, unchanged).filterEdgesalways returns a non-nil slice so an empty result renders[], notnull— per the repo's--jsoncontract.Verification
go test ./...— 18 packages green.make lint— 0 issues.gofmtclean.The filter engine is pure, so it unit-tests without a server: every flag alone and combined, case-insensitive
--name,--to/--fromby 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