From d112b787fe0bd4906fb070091f23128ea41cc985 Mon Sep 17 00:00:00 2001 From: Hiro-Chiba <203865699+Hiro-Chiba@users.noreply.github.com> Date: Sun, 30 Aug 2026 18:02:11 +0900 Subject: [PATCH] fix(cli): fail unresolved graph commands --- .../cli/__tests__/unresolved-machine-output.test.ts | 10 ++++++++++ ix-cli/src/cli/commands/callers.ts | 12 ++++-------- ix-cli/src/cli/commands/contains.ts | 4 ++-- ix-cli/src/cli/commands/depends.ts | 5 +++-- ix-cli/src/cli/commands/history.ts | 5 +++-- ix-cli/src/cli/commands/impact.ts | 8 +++----- ix-cli/src/cli/commands/imports.ts | 10 +++------- ix-cli/src/cli/commands/overview.ts | 7 +++---- ix-cli/src/cli/commands/trace.ts | 8 ++++---- 9 files changed, 35 insertions(+), 34 deletions(-) diff --git a/ix-cli/src/cli/__tests__/unresolved-machine-output.test.ts b/ix-cli/src/cli/__tests__/unresolved-machine-output.test.ts index f2f62357..9a8d0f47 100644 --- a/ix-cli/src/cli/__tests__/unresolved-machine-output.test.ts +++ b/ix-cli/src/cli/__tests__/unresolved-machine-output.test.ts @@ -43,6 +43,16 @@ describe("unresolved targets in machine formats", () => { ["context", async () => (await import("../commands/context.js")).registerContextCommand], ["explain", async () => (await import("../commands/explain.js")).registerExplainCommand], ["read", async () => (await import("../commands/read.js")).registerReadCommand], + ["overview", async () => (await import("../commands/overview.js")).registerOverviewCommand], + ["impact", async () => (await import("../commands/impact.js")).registerImpactCommand], + ["contains", async () => (await import("../commands/contains.js")).registerContainsCommand], + ["callers", async () => (await import("../commands/callers.js")).registerCallersCommand], + ["callees", async () => (await import("../commands/callers.js")).registerCallersCommand], + ["imports", async () => (await import("../commands/imports.js")).registerImportsCommand], + ["imported-by", async () => (await import("../commands/imports.js")).registerImportsCommand], + ["depends", async () => (await import("../commands/depends.js")).registerDependsCommand], + ["trace", async () => (await import("../commands/trace.js")).registerTraceCommand], + ["history", async () => (await import("../commands/history.js")).registerHistoryCommand], ] as const)("returns JSON and a non-zero status from ix %s", async (command, loadRegister) => { const result = await run(await loadRegister(), [command, "DefinitelyMissing", "--format", "json"]); diff --git a/ix-cli/src/cli/commands/callers.ts b/ix-cli/src/cli/commands/callers.ts index 034a7527..26172240 100644 --- a/ix-cli/src/cli/commands/callers.ts +++ b/ix-cli/src/cli/commands/callers.ts @@ -8,12 +8,8 @@ import { formatEdgeResults, relativePath } from "../format.js"; import { parsePickOption } from "../options.js"; import { resolveFileOrEntity, printResolved } from "../resolve.js"; import { stderr } from "../stderr.js"; -import { llmLine, llmError } from "../llm.js"; - -/** Emit a structured llm error for a failed resolution, or nothing for other formats. */ -function llmUnresolved(format: string, symbol: string): void { - if (format === "llm") console.log(llmError("unresolved_target", `No entity resolved for "${symbol}".`)); -} +import { llmLine } from "../llm.js"; +import { reportUnresolvedTarget } from "../ui.js"; const execFileAsync = promisify(execFile); @@ -31,7 +27,7 @@ export function registerCallersCommand(program: Command): void { const limit = parseInt(opts.limit, 10); const resolveOpts = { kind: opts.kind, pick: opts.pick }; const target = await resolveFileOrEntity(client, symbol, resolveOpts); - if (!target) { llmUnresolved(opts.format, symbol); return; } + if (!target) { reportUnresolvedTarget(symbol, opts.format); return; } if (opts.format === "text") printResolved(target); // Use expand by entity ID to avoid aggregating results across all same-named entities const result = await client.expand(target.id, { @@ -134,7 +130,7 @@ export function registerCallersCommand(program: Command): void { const calleeLimit = parseInt(opts.limit, 10); const resolveOpts = { kind: opts.kind, pick: opts.pick }; const target = await resolveFileOrEntity(client, symbol, resolveOpts); - if (!target) { llmUnresolved(opts.format, symbol); return; } + if (!target) { reportUnresolvedTarget(symbol, opts.format); return; } if (opts.format === "text") printResolved(target); // Use expand by entity ID to avoid aggregating results across all same-named entities const result = await client.expand(target.id, { diff --git a/ix-cli/src/cli/commands/contains.ts b/ix-cli/src/cli/commands/contains.ts index 830bbc3d..096be3cc 100644 --- a/ix-cli/src/cli/commands/contains.ts +++ b/ix-cli/src/cli/commands/contains.ts @@ -4,7 +4,7 @@ import { getEndpoint } from "../config.js"; import { formatEdgeResults } from "../format.js"; import { parsePickOption } from "../options.js"; import { resolveFileOrEntity, printResolved } from "../resolve.js"; -import { llmError } from "../llm.js"; +import { reportUnresolvedTarget } from "../ui.js"; export function registerContainsCommand(program: Command): void { program @@ -22,7 +22,7 @@ export function registerContainsCommand(program: Command): void { const resolveOpts = { kind: opts.kind, path: opts.path, pick: opts.pick }; const target = await resolveFileOrEntity(client, symbol, resolveOpts); if (!target) { - if (opts.format === "llm") console.log(llmError("unresolved_target", `No entity resolved for "${symbol}".`)); + reportUnresolvedTarget(symbol, opts.format); return; } if (opts.format === "text") printResolved(target); diff --git a/ix-cli/src/cli/commands/depends.ts b/ix-cli/src/cli/commands/depends.ts index 9d2d9049..57a38fa4 100644 --- a/ix-cli/src/cli/commands/depends.ts +++ b/ix-cli/src/cli/commands/depends.ts @@ -4,8 +4,9 @@ import { IxClient } from "../../client/api.js"; import { getEndpoint } from "../config.js"; import { resolveFileOrEntity, printResolved, isRawId } from "../resolve.js"; import { compactTreeNode, relativePath } from "../format.js"; -import { llmLine, llmError } from "../llm.js"; +import { llmLine } from "../llm.js"; import { parsePickOption } from "../options.js"; +import { reportUnresolvedTarget } from "../ui.js"; // ── Tree types ────────────────────────────────────────────────────── @@ -216,7 +217,7 @@ export function registerDependsCommand(program: Command): void { }; const target = await resolveFileOrEntity(client, symbol, resolveOpts); if (!target) { - if (opts.format === "llm") console.log(llmError("unresolved_target", `No entity resolved for "${symbol}".`)); + reportUnresolvedTarget(symbol, opts.format); return; } diff --git a/ix-cli/src/cli/commands/history.ts b/ix-cli/src/cli/commands/history.ts index 284e13e0..40fd2b44 100644 --- a/ix-cli/src/cli/commands/history.ts +++ b/ix-cli/src/cli/commands/history.ts @@ -4,8 +4,9 @@ import { IxClient } from "../../client/api.js"; import { getEndpoint } from "../config.js"; import { resolveFileOrEntity, printResolved } from "../resolve.js"; import { relativePath } from "../format.js"; -import { llmLine, llmError } from "../llm.js"; +import { llmLine } from "../llm.js"; import { parsePickOption } from "../options.js"; +import { reportUnresolvedTarget } from "../ui.js"; /** Render an entity's provenance chain as llm records: a header then one `patch` row per revision. */ export function renderHistoryLlm( @@ -41,7 +42,7 @@ export function registerHistoryCommand(program: Command): void { const resolveOpts = { kind: opts.kind, path: opts.path, pick: opts.pick }; const resolved = await resolveFileOrEntity(client, target, resolveOpts); if (!resolved) { - if (opts.format === "llm") console.log(llmError("unresolved_target", `No entity resolved for "${target}".`)); + reportUnresolvedTarget(target, opts.format); return; } diff --git a/ix-cli/src/cli/commands/impact.ts b/ix-cli/src/cli/commands/impact.ts index c30ff0f1..586f1041 100644 --- a/ix-cli/src/cli/commands/impact.ts +++ b/ix-cli/src/cli/commands/impact.ts @@ -1,13 +1,13 @@ import type { Command } from "commander"; import chalk from "chalk"; -import { renderSection, renderKeyValue, renderNote, renderResolvedHeader, colorizeKind } from "../ui.js"; +import { renderSection, renderKeyValue, renderNote, renderResolvedHeader, colorizeKind, reportUnresolvedTarget } from "../ui.js"; import { IxClient } from "../../client/api.js"; import { getEndpoint } from "../config.js"; import { resolveFileOrEntity, printResolved } from "../resolve.js"; import { bucketByHierarchy, getSystemPath, formatSystemPath, hasMapData, type SystemPath } from "../hierarchy.js"; import { inferRiskSemantics, humanizeLabel, type ImpactFacts, type RiskSemantics } from "../impact/risk-semantics.js"; import { stripNulls } from "../format.js"; -import { llmLine, llmError } from "../llm.js"; +import { llmLine } from "../llm.js"; import { parsePickOption } from "../options.js"; const CONTAINER_KINDS = new Set(["class", "module", "file", "object", "trait", "interface"]); @@ -37,9 +37,7 @@ export function registerImpactCommand(program: Command): void { const resolveOpts = { kind: opts.kind, pick: opts.pick }; const target = await resolveFileOrEntity(client, symbol, resolveOpts); if (!target) { - // The resolver already printed human guidance to stderr; for llm - // consumers emit a structured error record on stdout as well. - if (opts.format === "llm") console.log(llmError("unresolved_target", `No entity resolved for "${symbol}".`)); + reportUnresolvedTarget(symbol, opts.format); return; } diff --git a/ix-cli/src/cli/commands/imports.ts b/ix-cli/src/cli/commands/imports.ts index f5454cd8..6257c761 100644 --- a/ix-cli/src/cli/commands/imports.ts +++ b/ix-cli/src/cli/commands/imports.ts @@ -3,12 +3,8 @@ import { IxClient } from "../../client/api.js"; import { getEndpoint } from "../config.js"; import { formatEdgeResults } from "../format.js"; import { resolveFileOrEntity, printResolved } from "../resolve.js"; -import { llmError } from "../llm.js"; import { parsePickOption } from "../options.js"; - -function llmUnresolved(format: string, symbol: string): void { - if (format === "llm") console.log(llmError("unresolved_target", `No entity resolved for "${symbol}".`)); -} +import { reportUnresolvedTarget } from "../ui.js"; export function registerImportsCommand(program: Command): void { program @@ -24,7 +20,7 @@ export function registerImportsCommand(program: Command): void { const limit = parseInt(opts.limit, 10); const resolveOpts = { kind: opts.kind, pick: opts.pick }; const target = await resolveFileOrEntity(client, symbol, resolveOpts); - if (!target) { llmUnresolved(opts.format, symbol); return; } + if (!target) { reportUnresolvedTarget(symbol, opts.format); return; } if (opts.format === "text") printResolved(target); const result = await client.expand(target.id, { direction: "out", predicates: ["IMPORTS"] }); formatEdgeResults(result.nodes.slice(0, limit), "imports", target.name, opts.format, target, "graph"); @@ -43,7 +39,7 @@ export function registerImportsCommand(program: Command): void { const limit = parseInt(opts.limit, 10); const resolveOpts = { kind: opts.kind, pick: opts.pick }; const target = await resolveFileOrEntity(client, symbol, resolveOpts); - if (!target) { llmUnresolved(opts.format, symbol); return; } + if (!target) { reportUnresolvedTarget(symbol, opts.format); return; } if (opts.format === "text") printResolved(target); const result = await client.expand(target.id, { direction: "in", predicates: ["IMPORTS"] }); formatEdgeResults(result.nodes.slice(0, limit), "imported-by", target.name, opts.format, target, "graph"); diff --git a/ix-cli/src/cli/commands/overview.ts b/ix-cli/src/cli/commands/overview.ts index 5de53891..b595600c 100644 --- a/ix-cli/src/cli/commands/overview.ts +++ b/ix-cli/src/cli/commands/overview.ts @@ -5,9 +5,9 @@ import { resolveFileOrEntity, printResolved } from "../resolve.js"; import { getEffectiveSystemPath, getSystemPath, hasMapData } from "../hierarchy.js"; import { humanizeLabel } from "../impact/risk-semantics.js"; import { relativePath } from "../format.js"; -import { llmLine, llmError, type LlmValue } from "../llm.js"; +import { llmLine, type LlmValue } from "../llm.js"; import { parsePickOption } from "../options.js"; -import { renderSection, renderKeyValue, renderNote, renderBreadcrumb } from "../ui.js"; +import { renderSection, renderKeyValue, renderNote, renderBreadcrumb, reportUnresolvedTarget } from "../ui.js"; const CONTAINER_KINDS = new Set(["class", "module", "file", "trait", "object", "interface"]); const STRUCTURAL_CONTAINER_KINDS = new Set(["class", "object", "trait", "interface"]); @@ -58,8 +58,7 @@ Examples: const resolveOpts = { kind: opts.kind, path: opts.path, pick: opts.pick }; const target = await resolveFileOrEntity(client, symbol, resolveOpts); if (!target) { - // Resolver printed human guidance to stderr; add a structured record for llm. - if (opts.format === "llm") console.log(llmError("unresolved_target", `No entity resolved for "${symbol}".`)); + reportUnresolvedTarget(symbol, opts.format); return; } diff --git a/ix-cli/src/cli/commands/trace.ts b/ix-cli/src/cli/commands/trace.ts index 9a4c1026..a525e28b 100644 --- a/ix-cli/src/cli/commands/trace.ts +++ b/ix-cli/src/cli/commands/trace.ts @@ -4,9 +4,9 @@ import { IxClient } from "../../client/api.js"; import { getEndpoint } from "../config.js"; import { resolveFileOrEntity, isRawId, activeReadScope, ensureReadScope } from "../resolve.js"; import type { ResolvedEntity } from "../resolve.js"; -import { renderSection, renderKeyValue, renderResolvedHeader, colorizeKind } from "../ui.js"; +import { renderSection, renderKeyValue, renderResolvedHeader, colorizeKind, reportUnresolvedTarget } from "../ui.js"; import { compactTreeNode, relativePath } from "../format.js"; -import { llmLine, llmError, type LlmValue } from "../llm.js"; +import { llmLine, type LlmValue } from "../llm.js"; import { parsePickOption } from "../options.js"; // ── Types ──────────────────────────────────────────────────────────── @@ -463,7 +463,7 @@ export function registerTraceCommand(program: Command): void { resolveFileOrEntity(client, opts.to, toResolveOpts), ]); if (!fromTarget || !toTarget) { - if (opts.format === "llm") console.log(llmError("unresolved_target", `Could not resolve ${!fromTarget ? symbol : opts.to}.`)); + reportUnresolvedTarget(!fromTarget ? symbol : opts.to, opts.format); return; } @@ -541,7 +541,7 @@ export function registerTraceCommand(program: Command): void { // ── Directional mode ──────────────────────────────────────── const resolvedTarget = await resolveFileOrEntity(client, symbol, resolveOpts); if (!resolvedTarget) { - if (opts.format === "llm") console.log(llmError("unresolved_target", `No entity resolved for "${symbol}".`)); + reportUnresolvedTarget(symbol, opts.format); return; } let target: ResolvedEntity = resolvedTarget;