feat(search): guide a 0-result search and fill the single-match number

The next-step suggestion on `search issues`/`search prs` is now conditioned
on the in-repo match count. On a miss it pointed the agent at `view <number>`,
which is nonsensical when nothing matched; it now suggests the non-indexed
`issue list --state all` / `pr list --state all` fallback, which recovers from
both an over-narrow query and issue-indexer lag without naming the cause. On
exactly one match it fills the real number (`issue view 2`), applying AXI
Principle 9's single-id fill. Two or more matches keep the parameterized
placeholder.

Search stays a locator — it never auto-loads the detail even on a single
match; ADR 0017 records that decision (a deliberate narrowing of Principle 4)
and the CONTEXT.md search term is updated to match.
This commit is contained in:
2026-07-19 10:17:14 -04:00
parent 0b0186674e
commit 5422f49f38
4 changed files with 196 additions and 1 deletions

View File

@@ -107,6 +107,10 @@ interface SearchKind {
noun: string;
/** The command a matched number feeds into. */
viewCommand: string;
/** The list command suggested as the fallback when a search finds nothing. */
listCommand: string;
/** Human plural for the fallback note, e.g. "issues" or "pull requests". */
things: string;
/** The `--help` text for this variant. */
help: string;
}
@@ -116,6 +120,8 @@ const SEARCH_ISSUES: SearchKind = {
type: "issues",
noun: "issues",
viewCommand: "issue view",
listCommand: "issue list",
things: "issues",
help: SEARCH_ISSUES_HELP,
};
@@ -124,6 +130,8 @@ const SEARCH_PRS: SearchKind = {
type: "pulls",
noun: "pull_requests",
viewCommand: "pr view",
listCommand: "pr list",
things: "pull requests",
help: SEARCH_PRS_HELP,
};
@@ -217,11 +225,24 @@ async function runSearch(deps: CliDeps, args: string[], kind: SearchKind): Promi
extractRow(issue, [...SEARCH_FIELDS, ...extraFields], { now, host: context.host, full }),
);
// The next-step suggestion is conditioned on the match count. On a miss the
// `view` hint is nonsensical, so point at the non-indexed list as a fallback
// (it recovers from both an over-narrow query and index lag); on a single match
// fill the real number (Principle 9's single-id fill); otherwise leave the
// number parameterized. Search stays a locator either way — it never auto-loads
// the detail (see ADR 0017).
const suggestion =
total === 0
? suggestCommand(context, `${kind.listCommand} --state all`, `to list all ${kind.things} instead`)
: total === 1
? suggestCommand(context, `${kind.viewCommand} ${matches[0]!.number}`, "to see it in full")
: suggestCommand(context, `${kind.viewCommand} <number>`, "to see a match in full");
return renderList({
noun: kind.noun,
rows,
countLine: formatCountLine(rows.length, total, false),
help: [suggestCommand(context, `${kind.viewCommand} <number>`, "to see a match in full")],
help: [suggestion],
});
}