Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ See [Exit codes](USER_GUIDE.md#exit-codes) in USER_GUIDE.

## Error code taxonomy

Process exit codes are coarse (`0` success, `1` usage, `2` not-found, `3` db, `4` feature-unavailable, `5` stale). Scripts, oncall runbooks, and AI agents that need to react to *which* failure happened — not just the bucket — should read the stable `Exxx_NAME` taxonomy emitted on every CLI / MCP error path. The user-facing table lives in [Error codes](USER_GUIDE.md#error-codes); this section captures the developer contract.
Process exit codes are coarse (`0` success including valid zero-row queries, `1` usage, `2` not-found or strict zero-row query, `3` db, `4` feature-unavailable, `5` stale). Query commands return `0` for genuine zero-row results by default and reserve `2` for missing indexed data or callers that opt into `--strict-not-found`. Scripts, oncall runbooks, and AI agents that need to react to *which* failure happened — not just the bucket — should read the stable `Exxx_NAME` taxonomy emitted on every CLI / MCP error path. The user-facing table lives in [Error codes](USER_GUIDE.md#error-codes); this section captures the developer contract.

**Where it surfaces**

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ file completion.
| DB maintenance | New indexes use SQLite incremental auto-vacuum. `cdidx vacuum` reclaims free pages from existing DBs, including a one-time full `VACUUM` conversion for legacy no-autovacuum DBs, and `status --json` reports metrics under `db_pragma_settings`. |
| Security defaults | On POSIX systems, `.cdidx` is created with `0700` permissions and `status --json` reports the effective `data_dir_mode` when available. |
| Diagnostics | `status --explain <field>` describes readiness fields and remediation. Read commands support `--profile`, `--slow-query-ms <n>`, and <code>--trace=stderr&#124;file&#124;none</code>; file traces write daily `query-trace-YYYYMMDD.jsonl` files next to the lifecycle log. |
| Query exit codes | Valid zero-result query commands exit `0` by default. Pass `--strict-not-found` when scripts should treat zero rows as exit code `2`. |
| Drift checks | `cdidx diff <db1> <db2>` compares schema, file, symbol, and reference deltas with stable exit codes: `0` identical, `1` drift, `2` schema mismatch, `3` unreadable DB. |
| Extensibility and feedback | Post-extraction hooks from `~/.config/cdidx/hooks/*.dll` or `CDIDX_HOOKS_DIR` can enrich symbols and references. `cdidx suggestions` lists, inspects, and exports local suggestion history, with fuzzy MCP suggestion deduplication controlled by CLI, env, or `.cdidxrc.json`. |
| Language coverage | 78 detected languages, with symbol and graph support where available. |
Expand Down
5 changes: 3 additions & 2 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,7 @@ cdidx report --output report.tgz --json
| `--since <datetime>` | `search`, `definition`, `symbols`, `files` | Filter to files modified since this ISO 8601 timestamp. Offsetless values (e.g. `2024-01-01T00:00:00`) are treated as UTC so the same flag resolves to the same instant in every timezone; append `Z` or an explicit offset (`+09:00`) to be explicit. |
| `--no-dedup` | `search` | Disable overlapping-chunk deduplication for raw results |
| `--reverse` | `deps` | Reverse lookup: show files that depend ON the matched path |
| `--strict-not-found` | Query commands | Return exit code `2` when a valid query produces zero rows. Without this flag, zero-result queries exit `0` and keep their normal empty/zero-result output. |
| `--top <n>` | Query commands | Alias for `--limit` |
| `--color <when>` | All commands | Control ANSI color output. Accepts `auto` (default), `always`, or `never`. Precedence: `--color` flag > `CLICOLOR_FORCE` > `NO_COLOR` > `CLICOLOR=0` > terminal capability auto-detect. Auto mode treats redirected stdout and StringWriter-style test capture as non-ANSI; on Windows it also accepts ConPTY/Windows Terminal virtual-terminal support and terminal hints such as `WT_SESSION`, `WT_PROFILE_ID`, `TERM_PROGRAM`, or non-`dumb` `TERM`. Use `--color=always` to keep colored kind labels through a pager such as `cdidx symbols Foo \| less -R`; use `--color=never` (or `NO_COLOR=1`) to suppress ANSI even on a TTY. |
| `--palette <name>` | All commands | Choose the ANSI palette used when color output is enabled. Accepts `basic` (8-color SGR 30–37, the default fallback for minimal SSH/CI terminals), `256` (256-color `\x1b[38;5;Nm`), or `truecolor` (24-bit RGB `\x1b[38;2;R;G;Bm`). Precedence: `--palette` flag > `CDIDX_COLOR_PALETTE` env var > `COLORTERM` / `TERM` auto-detect. The basic palette avoids `\x1b[90m` (bright-black / dim), which is unreadable on many minimal terminals. |
Expand All @@ -1000,9 +1001,9 @@ If a query itself begins with `-`, pass it as `--query <query>` or `-- <query>`.

| Code | Meaning |
|---|---|
| `0` | Success |
| `0` | Success, including valid queries that produce zero rows |
| `1` | Usage error (missing command, missing required positional input, or command-shape error) |
| `2` | Not found (no search results, missing directory) |
| `2` | Not found (missing indexed path or zero-result query when `--strict-not-found` is set) |
| `3` | Permanent database error |
| `4` | Feature unavailable on this build (for example CLI `--json` on a manually trimmed custom build) |
| `5` | Stale index (`status --check` found DB/workspace differences) |
Expand Down
20 changes: 20 additions & 0 deletions changelog.d/unreleased/1425.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
category: changed
issues:
- 1425
affected:
- src/CodeIndex/Cli/QueryCommandRunner.cs
- src/CodeIndex/Cli/CliFlagSchema.cs
- tests/CodeIndex.Tests/QueryCommandRunnerTests.cs
- README.md
- USER_GUIDE.md
- DEVELOPER_GUIDE.md
---

## English

- **Zero-result query exit codes are consistent by default (#1425)** — valid query commands now exit `0` for genuine zero-row results, and `--strict-not-found` restores exit code `2` for scripts that require non-zero empty results.

## 日本語

- **0 件クエリの終了コードを既定で統一しました (#1425)** — 有効な query コマンドが本当に 0 件を返す場合は終了コード `0` になり、空結果を非 0 として扱いたいスクリプト向けに `--strict-not-found` で終了コード `2` を返せます。
6 changes: 6 additions & 0 deletions src/CodeIndex/Cli/CliFlagSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ internal static class CliFlagSchema
"search", "definition", "references", "callers", "callees", "symbols",
"files", "find", "impact", "unused", "hotspots",
];
private static readonly string[] StrictNotFoundCommands =
[
"search", "definition", "references", "callers", "callees", "symbols",
"files", "find", "excerpt", "map", "inspect", "deps", "impact", "unused", "hotspots",
];

private static readonly string[] KindCommands =
[
Expand Down Expand Up @@ -206,6 +211,7 @@ private static IReadOnlyList<CliFlag> BuildAll()
new() { Name = "--rank-by", ValuePlaceholder = "<weighted|count|kind>", Description = "Rank callers/callees by weighted structural score, raw count, or kind bucket", Commands = Set(RankByCommands) },
new() { Name = "--raw-kinds", Description = "Show raw reference kinds instead of logical graph kinds", Commands = Set(RawKindsCommands) },
new() { Name = "--count", Description = "Count only", Commands = Set(CountCommands) },
new() { Name = "--strict-not-found", Description = "Return exit code 2 when a valid query has zero rows", Commands = Set(StrictNotFoundCommands) },
new() { Name = "--since", ValuePlaceholder = "<datetime>", Description = "Filter by modified-since timestamp", Commands = Set(SinceCommands) },
new() { Name = "--bytes", Description = "Show raw byte counts in human output", Commands = Set(ByteFormatCommands) },
new() { Name = "--query", ValuePlaceholder = "<query>", Description = "Literal query", Commands = Set(QueryCommands) },
Expand Down
52 changes: 35 additions & 17 deletions src/CodeIndex/Cli/QueryCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ private sealed record StatusReadinessField(
"--fts",
"--body",
"--count",
"--strict-not-found",
"--no-dedup",
"--no-visibility-rank",
"--exact",
Expand Down Expand Up @@ -407,7 +408,7 @@ public static int RunSearch(string[] cmdArgs, JsonSerializerOptions jsonOptions)
WriteLangHint(options.Lang, reader);
WriteZeroResultHints(options, reader);
}
return CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.Json)
Expand Down Expand Up @@ -562,7 +563,7 @@ public static int RunDefinition(string[] cmdArgs, JsonSerializerOptions jsonOpti
WriteLangHint(options.Lang, reader);
WriteZeroResultHints(options, reader, "Try 'search' for full-text matches instead of symbol lookup.");
}
return CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.Json)
Expand Down Expand Up @@ -711,7 +712,7 @@ public static int RunReferences(string[] cmdArgs, JsonSerializerOptions jsonOpti
WriteLangHint(options.Lang, reader);
WriteDegradedGraphZeroResult(reader, "references", json: false, graphAvailable: reader._hasReferencesTable, jsonOptions);
}
return CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.Json)
Expand Down Expand Up @@ -837,7 +838,7 @@ public static int RunCallers(string[] cmdArgs, JsonSerializerOptions jsonOptions
WriteLangHint(options.Lang, reader);
WriteDegradedGraphZeroResult(reader, "callers", json: false, graphAvailable: reader._hasReferencesTable, jsonOptions);
}
return CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.Json)
Expand Down Expand Up @@ -963,7 +964,7 @@ public static int RunCallees(string[] cmdArgs, JsonSerializerOptions jsonOptions
WriteLangHint(options.Lang, reader);
WriteDegradedGraphZeroResult(reader, "callees", json: false, graphAvailable: reader._hasReferencesTable, jsonOptions);
}
return CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.Json)
Expand Down Expand Up @@ -1136,7 +1137,7 @@ public static int RunSymbols(string[] cmdArgs, JsonSerializerOptions jsonOptions
WriteLangHint(options.Lang, reader);
WriteZeroResultHints(options, reader);
}
return CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.Json)
Expand Down Expand Up @@ -1211,7 +1212,7 @@ public static int RunFiles(string[] cmdArgs, JsonSerializerOptions jsonOptions)
WriteLangHint(options.Lang, reader);
WriteZeroResultHints(options, reader);
}
return CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.Json)
Expand Down Expand Up @@ -1327,7 +1328,7 @@ public static int RunExcerpt(string[] cmdArgs, JsonSerializerOptions jsonOptions
{
if (!options.Json)
Console.Error.WriteLine("No excerpt found.");
return CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.Json)
Expand Down Expand Up @@ -1448,7 +1449,7 @@ public static int RunFind(string[] cmdArgs, JsonSerializerOptions jsonOptions)
WriteZeroResultHints(options, reader, filterHint: "try broadening --path or adding another --path value; --path is required for find.");
}
}
return CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.Json)
Expand Down Expand Up @@ -1638,7 +1639,7 @@ public static int RunMap(string[] cmdArgs, JsonSerializerOptions jsonOptions)
{
Console.Error.WriteLine("No files found matching the given filters.");
}
return CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.Json)
Expand Down Expand Up @@ -1793,7 +1794,7 @@ public static int RunInspect(string[] cmdArgs, JsonSerializerOptions jsonOptions
WriteRepoMapSection("Callees", analysis.Callees.Select(item => $"{item.CallerName ?? "<top-level>"} -> {item.CalleeName} ({item.ReferenceCount} refs)"));
}

return CommandExitCodes.Success;
return IsEmptySymbolAnalysis(analysis) ? ZeroResultExitCode(options) : CommandExitCodes.Success;
});
}

Expand Down Expand Up @@ -2485,7 +2486,7 @@ public static int RunImpact(string[] cmdArgs, JsonSerializerOptions jsonOptions)
WriteGraphSupportHint(options.Lang);
WriteDegradedGraphZeroResult(reader, "callers", json: false, graphAvailable: reader._hasReferencesTable, jsonOptions);
}
return options.CountOnly ? CommandExitCodes.Success : CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.CountOnly)
Expand Down Expand Up @@ -2651,7 +2652,7 @@ public static int RunDeps(string[] cmdArgs, JsonSerializerOptions jsonOptions)
WriteSqlGraphContractWarningIfNeeded(json: false, sqlGraphSignal, reader, options);
WriteDegradedGraphZeroResult(reader, "edges", json: false, graphAvailable: reader._hasReferencesTable, jsonOptions);
}
return CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.Json)
Expand Down Expand Up @@ -2900,7 +2901,7 @@ public static int RunHotspots(string[] cmdArgs, JsonSerializerOptions jsonOption
WriteSqlGraphContractWarningIfNeeded(json: false, effectiveSqlGraphSignal, reader, options);
WriteDegradedGraphZeroResult(reader, "hotspots", json: false, graphAvailable: reader._hasReferencesTable, jsonOptions);
}
return options.CountOnly ? CommandExitCodes.Success : CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

var definitionSiteTotal = groupedResults.Sum(g => g.DefinitionSites);
Expand Down Expand Up @@ -3046,7 +3047,7 @@ public static int RunHotspots(string[] cmdArgs, JsonSerializerOptions jsonOption
WriteSqlGraphContractWarningIfNeeded(json: false, effectiveSqlGraphSignal, reader, options);
WriteDegradedGraphZeroResult(reader, "hotspots", json: false, graphAvailable: reader._hasReferencesTable, jsonOptions);
}
return options.CountOnly ? CommandExitCodes.Success : CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.CountOnly)
Expand Down Expand Up @@ -3173,7 +3174,7 @@ public static int RunHotspots(string[] cmdArgs, JsonSerializerOptions jsonOption
WriteSqlGraphContractWarningIfNeeded(json: false, sqlGraphSignal, reader, options);
WriteDegradedGraphZeroResult(reader, "hotspots", json: false, graphAvailable: reader._hasReferencesTable, jsonOptions);
}
return options.CountOnly ? CommandExitCodes.Success : CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.CountOnly)
Expand Down Expand Up @@ -3334,7 +3335,7 @@ public static int RunUnused(string[] cmdArgs, JsonSerializerOptions jsonOptions)
WriteSqlGraphContractWarningIfNeeded(json: false, sqlGraphSignal, reader, options);
WriteDegradedGraphZeroResult(reader, "symbols", json: false, graphAvailable: reader._hasReferencesTable, jsonOptions);
}
return options.Json ? CommandExitCodes.Success : CommandExitCodes.NotFound;
return ZeroResultExitCode(options);
}

if (options.Json)
Expand Down Expand Up @@ -3660,6 +3661,7 @@ public static QueryCommandOptions ParseArgs(string[] args, bool jsonDefault, boo
bool rawFts = false;
bool includeBody = false;
bool countOnly = false;
bool strictNotFound = false;
int? startLine = null;
int? endLine = null;
int contextBefore = 0;
Expand Down Expand Up @@ -3921,6 +3923,9 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
case "--count":
countOnly = true;
break;
case "--strict-not-found":
strictNotFound = true;
break;
case "--by-bucket":
break;
case "--no-dedup":
Expand Down Expand Up @@ -4301,6 +4306,7 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
ExcludeTests = excludeTests,
IncludeGenerated = includeGenerated,
CountOnly = countOnly,
StrictNotFound = strictNotFound,
Since = since,
NoDedup = noDedup,
NoVisibilityRank = noVisibilityRank,
Expand Down Expand Up @@ -4609,6 +4615,17 @@ private static bool TryRejectMultipleExactFlags(QueryCommandOptions options, out
// preview 系オプションの検証はコマンド別 allowlist に寄せたため、この shim は常に null を返す。
private static string? ValidatePreviewOptions(string commandName, string[] args, bool allowMaxLineWidth, bool allowFocusOptions) => null;

private static int ZeroResultExitCode(QueryCommandOptions options)
=> options.StrictNotFound ? CommandExitCodes.NotFound : CommandExitCodes.Success;

private static bool IsEmptySymbolAnalysis(SymbolAnalysisResult analysis)
=> analysis.File == null
&& analysis.Definitions.Count == 0
&& analysis.NearbySymbols.Count == 0
&& analysis.References.Count == 0
&& analysis.Callers.Count == 0
&& analysis.Callees.Count == 0;

private static int WithDb(QueryCommandOptions options, JsonSerializerOptions jsonOptions, Func<DbReader, int> action, Action<int>? afterProfile = null)
{
var dbPath = options.DbPath;
Expand Down Expand Up @@ -6754,6 +6771,7 @@ public sealed class QueryCommandOptions
public bool ExcludeTests { get; init; }
public bool IncludeGenerated { get; init; }
public bool CountOnly { get; init; }
public bool StrictNotFound { get; init; }
public DateTime? Since { get; init; }
public bool NoDedup { get; init; }
public bool NoVisibilityRank { get; init; }
Expand Down
2 changes: 1 addition & 1 deletion tests/CodeIndex.Tests/IndexCommandRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7149,7 +7149,7 @@ public void Call(Api api)
var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db");
var (hotspotsExitCode, hotspotsJson) = RunHotspotsJsonWithPaths(dbPath, "csharp", "function", ["projA/", "projB/"]);

Assert.Equal(CommandExitCodes.NotFound, hotspotsExitCode);
Assert.Equal(CommandExitCodes.Success, hotspotsExitCode);
Assert.True(hotspotsJson.GetProperty("hotspot_family_ready").GetBoolean());
Assert.Equal(0, hotspotsJson.GetProperty("count").GetInt32());
Assert.Empty(hotspotsJson.GetProperty("hotspots").EnumerateArray());
Expand Down
Loading
Loading