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
7 changes: 7 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ cdidx search AuthService --db /artifacts/codeindex.db --immutable
Mutating commands such as `index`, `backfill-fold`, `optimize`, and `vacuum`
require writable storage and reject read-only database opens.

`backfill-fold --dry-run` previews the folded-key rows that would be rewritten
without mutating the DB or stamping FoldReady. The MCP `backfill_fold` tool
accepts the same preview as `dry_run: true`, and also accepts `force: true` to
rewrite all folded keys when an operator needs to recover from suspicious fold
metadata or row state even though the stored version/fingerprint appears
current.

## Filesystem Permissions

On POSIX filesystems, cdidx creates `.cdidx/` with mode `0700` and applies mode
Expand Down
24 changes: 24 additions & 0 deletions changelog.d/unreleased/1613.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
category: fixed
issues:
- 1613
- 1703
- 1919
affected:
- src/CodeIndex/Database/DbWriter.cs
- src/CodeIndex/Cli/IndexCommandRunner.Maintenance.cs
- src/CodeIndex/Cli/JsonOutputContracts.cs
- src/CodeIndex/Mcp/McpToolHandlers.cs
- src/CodeIndex/Mcp/McpToolDefinitions.cs
- src/CodeIndex/Mcp/McpServer.cs
- tests/CodeIndex.Tests/IndexCommandRunnerTests.cs
- tests/CodeIndex.Tests/McpServerTests.cs
---

## English

- **Backfill folded-key maintenance is now previewable and retry-aware (#1613, #1703, #1919)** — `backfill-fold --dry-run` and MCP `backfill_fold` `dry_run` preview affected rows without writing, MCP responses report FoldReady before/after plus already-complete state, and `force` can rewrite all keys when recovery requires bypassing metadata short-circuits.

## 日本語

- **folded-key の保守処理をプレビュー可能かつ再試行判定しやすくしました (#1613, #1703, #1919)** — `backfill-fold --dry-run` と MCP `backfill_fold` の `dry_run` は書き込まず対象行数を返し、MCP 応答は FoldReady の前後状態と完了済み状態を示し、復旧時には `force` で metadata の短絡判定を避けて全 key を再生成できます。
2 changes: 1 addition & 1 deletion src/CodeIndex/Cli/CliFlagSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ private static IReadOnlyList<CliFlag> BuildAll()
new() { Name = "--integrity-check", Description = "Run PRAGMA integrity_check on the database", Commands = Set("db") },
new() { Name = "--rebuild", Description = "Delete existing DB and rebuild from scratch", Commands = Set("index") },
new() { Name = "--optimize", Description = "Optimize the existing FTS5 table without scanning files", Commands = Set("index") },
new() { Name = "--dry-run", Description = "Scan files without writing", Commands = Set("index") },
new() { Name = "--dry-run", Description = "Preview without writing", Commands = Set("index", "backfill-fold") },
new() { Name = "--force", Description = "Bypass the per-database index lock", Commands = Set("index") },
new() { Name = "--duration-format", ValuePlaceholder = "<auto|seconds|hms>", Description = "Index elapsed time display format", Commands = Set("index") },
new() { Name = "--max-file-bytes", ValuePlaceholder = "<bytes>", Description = "Override the per-file indexing size limit", Commands = Set("index") },
Expand Down
2 changes: 1 addition & 1 deletion src/CodeIndex/Cli/ConsoleUi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines =
[
("index", "cdidx index <projectPath> [--db <path>] [--rebuild] [--optimize] [--verbose] [--dry-run] [--force] [--quiet] [--json] [--memory-trace] [--duration-format <auto|seconds|hms>] [--max-file-bytes <bytes>] [--follow-symlinks <none|internal|all>] [--include-symbol-kind <kind>[,<kind>]] [--exclude-symbol-kind <kind>[,<kind>]] [--watch [--debounce <ms>]]"),
("hooks", "cdidx hooks <install|uninstall|status> [--project <path>] [--force] [--json]"),
("backfill-fold", "cdidx backfill-fold [--db <path>] [--json]"),
("backfill-fold", "cdidx backfill-fold [--db <path>] [--dry-run] [--json]"),
("optimize", "cdidx optimize [--db <path>] [--json]"),
("vacuum", "cdidx vacuum [--db <path>] [--json]"),
("index-commits", "cdidx index <projectPath> --commits <id> [id ...] [--db <path>] [--verbose] [--dry-run] [--json] [--memory-trace] [--duration-format <auto|seconds|hms>] [--max-file-bytes <bytes>] [--include-symbol-kind <kind>[,<kind>]] [--exclude-symbol-kind <kind>[,<kind>]]"),
Expand Down
94 changes: 67 additions & 27 deletions src/CodeIndex/Cli/IndexCommandRunner.Maintenance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public static partial class IndexCommandRunner
{
private static readonly string[] AcceptedBackfillFoldFlags =
[
"--db", "--json", "--help",
"--db", "--json", "--dry-run", "--help",
];

public static int RunBackfillFold(string[] cmdArgs, JsonSerializerOptions jsonOptions) =>
Expand Down Expand Up @@ -167,57 +167,91 @@ internal static int RunBackfillFold(
var writer = new DbWriter(db);

var userVersionBefore = db.GetUserVersion();
var foldReadyBefore = (userVersionBefore & DbContext.FoldReadyFlag) != 0;
var currentFoldVersion = NameFold.Version.ToString(System.Globalization.CultureInfo.InvariantCulture);
var currentFoldFingerprint = NameFold.Fingerprint();
var storedFoldVersion = db.GetMetaString("fold_key_version");
var storedFoldFingerprint = db.GetMetaString("fold_key_fingerprint");
var foldMetadataCurrentBefore = storedFoldVersion == currentFoldVersion
&& storedFoldFingerprint == currentFoldFingerprint;
foldReadyBefore = foldReadyBefore && foldMetadataCurrentBefore;
// Missing or mismatched fold metadata means persisted keys may have been generated
// by a different fold algorithm/runtime, so refresh every row from source names.
// fold metadata 未記録 / 不一致時は全行再計算して version/runtime skew を解消する。
var rewriteAll = storedFoldVersion != currentFoldVersion
|| storedFoldFingerprint != currentFoldFingerprint;
var rewriteAll = !foldMetadataCurrentBefore;

var (symbols, symbolReferences) = writer.BackfillFoldedColumns(
rewriteAll,
backfillCancellation.Token);
// MarkFoldReady re-verifies inside a BEGIN IMMEDIATE so a concurrent writer cannot
// insert NULL-folded rows between the verify and the stamp. Issue #1535.
// MarkFoldReady は BEGIN IMMEDIATE 内で再検証するため、concurrent writer による
// NULL 行差し込みで fold_ready が嘘になるのを防ぐ。Issue #1535。
var verified = writer.MarkFoldReady();
if (!verified)
var symbols = 0;
var symbolReferences = 0;
var verified = false;
var userVersionAfter = userVersionBefore;

if (options.DryRun)
{
return WriteCommandError(
options.Json,
jsonOptions,
"folded-name backfill verification failed: some rows still have NULL folded values",
CommandExitCodes.DatabaseError,
"Retry `cdidx backfill-fold`. If the DB still does not verify, rebuild it with `cdidx index <projectPath> --rebuild`.",
CommandErrorCodes.DbError);
(symbols, symbolReferences) = writer.CountBackfillFoldedColumns(rewriteAll);
}
else
{
using var transaction = writer.BeginTransaction();
(symbols, symbolReferences) = writer.BackfillFoldedColumns(
rewriteAll,
backfillCancellation.Token);
// MarkFoldReady re-verifies in the same transaction so rows, metadata, and
// FoldReady stamp commit or roll back together.
// 同一 transaction 内で再検証し、行・metadata・FoldReady stamp を原子的に扱う。
verified = writer.MarkFoldReady();
if (!verified)
{
return WriteCommandError(
options.Json,
jsonOptions,
"folded-name backfill verification failed: some rows still have NULL folded values",
CommandExitCodes.DatabaseError,
"Retry `cdidx backfill-fold`. If the DB still does not verify, rebuild it with `cdidx index <projectPath> --rebuild`.",
CommandErrorCodes.DbError);
}

var userVersionAfter = db.GetUserVersion();
transaction.Commit();
userVersionAfter = db.GetUserVersion();
}
var foldMetadataCurrentAfter = options.DryRun
? foldMetadataCurrentBefore
: true;
var foldReadyAfter = (userVersionAfter & DbContext.FoldReadyFlag) != 0
&& foldMetadataCurrentAfter;
var wasAlreadyComplete = foldReadyBefore && !rewriteAll && symbols == 0 && symbolReferences == 0;

if (options.Json)
{
Console.WriteLine(JsonSerializer.Serialize(new BackfillFoldJsonResult(
symbols,
symbolReferences,
rewriteAll,
options.DryRun,
wasAlreadyComplete,
foldReadyBefore,
foldReadyAfter,
verified,
userVersionBefore,
userVersionAfter,
true), jsonContext.BackfillFoldJsonResult));
foldReadyAfter), jsonContext.BackfillFoldJsonResult));
}
else
{
Console.WriteLine("Backfilling folded-name columns ...");
Console.WriteLine($" symbols: {ConsoleUi.Counted(symbols, "row", format: "N0")} rewritten");
Console.WriteLine($" symbol_references: {ConsoleUi.Counted(symbolReferences, "row", format: "N0")} rewritten");
Console.WriteLine(options.DryRun
? "Previewing folded-name column backfill ..."
: "Backfilling folded-name columns ...");
var verb = options.DryRun ? "would be rewritten" : "rewritten";
Console.WriteLine($" symbols: {ConsoleUi.Counted(symbols, "row", format: "N0")} {verb}");
Console.WriteLine($" symbol_references: {ConsoleUi.Counted(symbolReferences, "row", format: "N0")} {verb}");
if (rewriteAll)
Console.WriteLine(" mode: full folded-key refresh (fold metadata missing or mismatched)");
Console.WriteLine($" verified: {(verified ? "yes" : "no")}");
Console.WriteLine($" stamp: FoldReady bit set (user_version: {userVersionBefore} -> {userVersionAfter})");
Console.WriteLine($" already complete: {(wasAlreadyComplete ? "yes" : "no")}");
Console.WriteLine($" fold_ready: {foldReadyBefore} -> {foldReadyAfter}");
if (!options.DryRun)
{
Console.WriteLine($" verified: {(verified ? "yes" : "no")}");
Console.WriteLine($" stamp: FoldReady bit set (user_version: {userVersionBefore} -> {userVersionAfter})");
}
}

return CommandExitCodes.Success;
Expand Down Expand Up @@ -286,6 +320,7 @@ private static BackfillFoldCommandOptions ParseBackfillFoldArgs(string[] args)
{
var dbPath = Path.Combine(".cdidx", "codeindex.db");
var json = false;
var dryRun = false;

for (int i = 0; i < args.Length; i++)
{
Expand All @@ -297,8 +332,11 @@ private static BackfillFoldCommandOptions ParseBackfillFoldArgs(string[] args)
case "--json":
json = true;
break;
case "--dry-run":
dryRun = true;
break;
case "--help" or "-h":
return new BackfillFoldCommandOptions { ShowHelp = true, DbPath = dbPath, Json = json };
return new BackfillFoldCommandOptions { ShowHelp = true, DbPath = dbPath, Json = json, DryRun = dryRun };
default:
if (args[i].StartsWith('-'))
{
Expand All @@ -310,6 +348,7 @@ private static BackfillFoldCommandOptions ParseBackfillFoldArgs(string[] args)
{
DbPath = dbPath,
Json = json,
DryRun = dryRun,
ParseError = $"backfill-fold does not accept positional arguments: '{args[i]}'"
};
break;
Expand All @@ -320,6 +359,7 @@ private static BackfillFoldCommandOptions ParseBackfillFoldArgs(string[] args)
{
DbPath = dbPath,
Json = json,
DryRun = dryRun,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/CodeIndex/Cli/IndexCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,7 @@ public sealed class BackfillFoldCommandOptions
public bool ShowHelp { get; init; }
public string DbPath { get; init; } = Path.Combine(".cdidx", "codeindex.db");
public bool Json { get; init; }
public bool DryRun { get; init; }
public string? ParseError { get; init; }
}

Expand Down
4 changes: 4 additions & 0 deletions src/CodeIndex/Cli/JsonOutputContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ internal sealed record BackfillFoldJsonResult(
[property: JsonPropertyName("symbols")] int Symbols,
[property: JsonPropertyName("symbol_references")] int SymbolReferences,
[property: JsonPropertyName("rewrite_all")] bool RewriteAll,
[property: JsonPropertyName("dry_run")] bool DryRun,
[property: JsonPropertyName("was_already_complete")] bool WasAlreadyComplete,
[property: JsonPropertyName("fold_ready_before")] bool FoldReadyBefore,
[property: JsonPropertyName("fold_ready_after")] bool FoldReadyAfter,
[property: JsonPropertyName("verified")] bool Verified,
[property: JsonPropertyName("user_version_before")] int UserVersionBefore,
[property: JsonPropertyName("user_version_after")] int UserVersionAfter,
Expand Down
24 changes: 24 additions & 0 deletions src/CodeIndex/Database/DbWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3253,6 +3253,30 @@ private bool SymbolExtractorVersionMatchesCurrent(string? lang)
return (symbols, symbolReferences);
}

public (int Symbols, int SymbolReferences) CountBackfillFoldedColumns(bool rewriteAll = false)
{
using var symbols = _conn.CreateCommand();
symbols.CommandText = rewriteAll
? "SELECT COUNT(*) FROM symbols WHERE name IS NOT NULL"
: "SELECT COUNT(*) FROM symbols WHERE name IS NOT NULL AND name_folded IS NULL";

using var references = _conn.CreateCommand();
references.CommandText = rewriteAll
? "SELECT COUNT(*) FROM symbol_references WHERE symbol_name IS NOT NULL OR container_name IS NOT NULL"
: @"SELECT COUNT(*)
FROM symbol_references
WHERE (symbol_name IS NOT NULL AND symbol_name_folded IS NULL)
OR (container_name IS NOT NULL AND container_name_folded IS NULL)";

return (ToInt32Count(symbols.ExecuteScalar()), ToInt32Count(references.ExecuteScalar()));
}

private static int ToInt32Count(object? value)
{
var count = value is long l ? l : (value is int i ? i : 0);
return count > int.MaxValue ? int.MaxValue : (int)count;
}

private int BackfillSymbolFoldedRows(bool rewriteAll, CancellationToken cancellationToken)
{
var rows = new List<(long Id, string Name)>();
Expand Down
4 changes: 2 additions & 2 deletions src/CodeIndex/Mcp/McpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2190,7 +2190,7 @@ private async Task<JsonNode> HandleToolsCallAsync(JsonNode? id, JsonNode? callPa
"symbol_hotspots" => ExecuteSymbolHotspots(id, args),
"ping" => ExecutePing(id),
"index" => ExecuteIndex(id, args, progressToken),
"backfill_fold" => ExecuteBackfillFold(id, progressToken),
"backfill_fold" => ExecuteBackfillFold(id, args, progressToken),
"suggest_improvement" => await ExecuteSuggestImprovementAsync(id, args).ConfigureAwait(false),
_ => CreateErrorResponse(hasId: true, id: id, code: -32602, message: $"Unknown tool: {toolName}",
category: McpErrorEnvelope.CategoryToolUnknown,
Expand Down Expand Up @@ -2970,7 +2970,7 @@ private static JsonArray BuildToolExamples(string name)
},
},
"index" => new JsonObject { ["path"] = ".", ["rebuild"] = false },
"backfill_fold" => new JsonObject(),
"backfill_fold" => new JsonObject { ["dry_run"] = false, ["force"] = false },
"symbol_hotspots" => new JsonObject { ["lang"] = "csharp", ["limit"] = 10 },
"unused_symbols" => new JsonObject { ["lang"] = "csharp", ["limit"] = 10 },
"suggest_improvement" => new JsonObject
Expand Down
Loading
Loading