fix: Exact-ID Match Precedence in RequireSingle#22
Conversation
When a query produces multiple matches, an exact case-insensitive ID match (strings.EqualFold) among the candidates now wins over incidental substring text matches, instead of aborting with "Multiple matches". Zero or >1 exact-ID hits fall through to the existing ambiguity error unchanged. Match/FindAll keep pure substring semantics for list/search. Fixes all five commands sharing the resolver: edit/rm/show/done/reopen. Adds regression test TestRequireSingle_ExactIDBeatsSubstring and updates the cli memory docs.
There was a problem hiding this comment.
Pull request overview
This PR fixes an ambiguity bug in the shared idea resolver (RequireSingle) by adding exact-ID precedence so that a query matching exactly one idea ID (case-insensitively) wins over incidental substring matches in other ideas’ text. This improves behavior across resolver-backed commands (edit/rm/show/done/reopen) without changing Match/FindAll search semantics.
Changes:
- Add exact-ID tiebreaking in
RequireSinglewhenlen(matches) > 1. - Add a regression unit test covering the exact-ID-beats-substring scenario.
- Update CLI memory docs to document the split between substring search (
Match/FindAll) and resolver precedence (RequireSingle).
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/internal/idea/idea.go | Adds exact-ID precedence in RequireSingle using strings.EqualFold before emitting ambiguity errors. |
| src/internal/idea/idea_test.go | Adds regression test for exact-ID precedence behavior. |
| fab/changes/260615-m2qx-exact-id-match-precedence/plan.md | Captures the planned requirements/tasks/acceptance for the change. |
| fab/changes/260615-m2qx-exact-id-match-precedence/intake.md | Documents the bug report, rationale, and agreed approach. |
| fab/changes/260615-m2qx-exact-id-match-precedence/.status.yaml | Tracks pipeline/stage status for the change. |
| fab/changes/260615-m2qx-exact-id-match-precedence/.history.jsonl | Logs stage transitions/commands for the change. |
| docs/memory/cli/structure.md | Documents the query-resolution contract: substring matching vs exact-ID precedence in RequireSingle. |
| docs/memory/cli/index.md | Updates the memory index description for structure.md to include query resolution. |
| docs/memory/cli/edit.md | Notes that idea edit benefits from the RequireSingle exact-ID tiebreaker. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| | `idea edit <query>` | Opens the resolved editor on a temp file containing the idea's **decoded** text (real newlines, real backslashes — `Idea.Text`); on clean exit the buffer is post-processed and persisted via the existing `idea.Edit`. | | ||
|
|
||
| **Resolve before launch.** The one-arg form resolves the match via the existing `idea.Show` (LoadFile + `RequireSingle`, `FilterAll`) **before** launching the editor — an ambiguous or unmatched query is refused with the match list and the editor never opens. This reuses `idea.Edit`'s exact match semantics (substring, case-insensitive, ID or text) with zero new resolver code. | ||
| **Resolve before launch.** The one-arg form resolves the match via the existing `idea.Show` (LoadFile + `RequireSingle`, `FilterAll`) **before** launching the editor — an ambiguous or unmatched query is refused with the match list and the editor never opens. This reuses `idea.Edit`'s match semantics (substring, case-insensitive, ID or text) with zero new resolver code. Since `260615-m2qx-exact-id-match-precedence`, `RequireSingle` also applies an **exact-ID tiebreaker**: when a query produces multiple matches but exactly one of them is an exact (case-insensitive) ID match, that idea wins over incidental substring text matches instead of aborting — so `idea edit jznd` resolves cleanly even when `jznd` also appears as cross-reference text inside another idea. See `structure.md` § Query resolution for the full contract; `Match`/`FindAll` keep pure substring semantics for `list`/search. |
There was a problem hiding this comment.
Fixed — rephrased to clarify that no edit-specific resolver code was added (resolution flows through the shared RequireSingle), while explicitly noting the shared resolver behavior did change with the exact-ID tiebreaker. (38788a6)
| { | ||
| name: "exact id wins over substring in another idea's text", | ||
| query: "jznd", | ||
| ideas: []Idea{ | ||
| {ID: "jznd", Text: "the idea to edit"}, | ||
| {ID: "qg64", Text: "see related [jznd] for context"}, | ||
| }, | ||
| wantID: "jznd", | ||
| wantIdx: 0, | ||
| }, | ||
| } | ||
| for _, tt := range tests { |
There was a problem hiding this comment.
Fixed — added a second table entry with an uppercase query (JZND) against a lowercase id (jznd) plus a [JZND] substring in another idea, so the case-insensitive (EqualFold) tiebreaker is now covered. (38788a6)
Meta
Pipeline: intake ✓ → apply ✓ → review ✓ → hydrate ✓ → ship → review-pr
Impact: +48/−0 code (excluding
fab/,docs/) · +347/−3 totalSummary
The shared idea resolver
RequireSingleaborted with "Multiple matches" whenever a query produced more than one hit, with no notion of precedence between match kinds. Passing an exact 4-char ID that also happened to appear as a substring inside another idea's text (e.g. a cross-reference[jznd]) matched both ideas and failed — even though the error told the user to "use the exact ID", which is exactly what they did. The only workaround was hand-editingfab/backlog.md, bypassing the tool entirely. This fix adds exact-ID precedence at the single resolver seam shared byedit/rm/show/done/reopen.Changes
RequireSingleexact-ID precedence (src/internal/idea/idea.go) — when a query yields multiple matches, an exact case-insensitive ID match (strings.EqualFold) among the candidates wins over incidental substring text matches. Zero or >1 exact-ID hits fall through to the existing ambiguity error unchanged.MatchandFindAllkeep pure substring semantics forlist/search; no CLI/cmdchanges, no new flags, no new dependencies.exactCount > 1falls through to the existing "Multiple matches" error (no silent pick); the scan runs over the already-filtered match set, so filter semantics are preserved.src/internal/idea/idea_test.go) — adds table-drivenTestRequireSingle_ExactIDBeatsSubstringcovering the exact-ID-beats-substring case for all five resolver-backed commands.