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
8 changes: 4 additions & 4 deletions DEVELOPER_GUIDE.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Use the full suite by default. Use targeted filters only while iterating locally
- Path-containment coverage for #5091 pairs platform-independent `PathCasingTests` seams for mixed parent-namespace case policies and directory identities with `FileIndexerTests` integration through an internal file symlink. Keep both checks when changing internal-symlink policy: a case-insensitive project mount must not make a distinct case-only sibling in its case-sensitive parent namespace appear internal, while genuine same-namespace aliases remain accepted.
- `WorkspaceCheckTruncationIssue5055Tests` owns the bounded `workspace_check` list contract. Keep zero, below-limit, exact-limit, and over-limit coverage for all six categories, plus the 1260/20/1240 example and one shared filesystem fixture for raw JSON, list-only projection, compact output, byte-budget trimming, deterministic order, and human sample labeling.
- Recipe issue-draft summary coverage in `QueryCommandRunnerSearchTests` keeps the dedicated compact DTO separate from full draft bodies, asserts returned/total/omitted row cardinality (including `count == drafts.length` and positive queries hidden by `--total-limit`), preserves pre-limit evidence-path counts with authority/lower-bound metadata, measures the complete UTF-8 document with multibyte paths and its final newline, verifies whole-row truncation and typed `E028` audit identity, and preserves the full-output contract.
- Issue #5191 compact audit replay coverage lives in `QueryCommandRunnerIssue5191Tests`. Keep shared single- and multi-query fixtures with metacharacters in recipe names, query names, path filters, database paths, and data directories; compare audit/search compact state, assert POSIX sh and PowerShell rendering from the same structured argv, and execute every underlying per-query cursor and results-only replay while preserving selectors, filters, database/data-directory selection, limits, cursors, and byte budgets.
- Marker-gate reference coverage keeps cross-language positive syntax in `ReferenceExtractorMarkerGateTests.cs`, with dedicated GraphQL/HTML/Markdown and XAML/XML cases proving that markerless continuation lines still advance parser state. `ReferenceExtractorPerformanceBudgetTests` owns three complementary regressions: Kotlin infix detection must not enumerate a large known-name set, Docker instruction classification must allocate nothing and scale with the instruction prefix rather than irrelevant line length, and warmed C#/Java/Kotlin files with 10,000 markerless declaration lines each must produce no references while staying below language-specific allocation ceilings of 20,000,000 / 12,500,000 / 40,000,000 bytes and the 3-second aggregate primary-target runaway guard. `Extract_ReusedContainerResolverDelegates_RemainLineAndExtractionScoped`, the cancellation follow-up, and the sequential Swift-property case pin synchronous delegate lifetime, extraction-local concurrency, token propagation, and per-line container reset. Keep the broad allocation/time guards on `net8.0`; keep the focused semantic and state-preservation tests cross-target.
- Framework: xUnit
- Target frameworks: `net8.0` and `net9.0`
Expand Down Expand Up @@ -1167,6 +1168,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
- #5091 の path containment coverage は、親 directory namespace の case policy と directory identity が混在する状況を OS 非依存で再現する `PathCasingTests` の seam と、内部 file symlink を通す `FileIndexerTests` の integration を対にします。internal-symlink policy を変更するときは両方を維持し、case-insensitive な project mount の policy で、case-sensitive な親 namespace にある別 identity の case-only sibling を内部 path と誤認しないこと、および同一 namespace の正当な alias は引き続き受理されることを検証してください。
- `WorkspaceCheckTruncationIssue5055Tests` は上限付き `workspace_check` 一覧の契約を担当します。6カテゴリすべての0件・上限未満・上限一致・上限超過、1260/20/1240 の例、および raw JSON、一覧単独 projection、compact 出力、byte budget による短縮、決定的順序、human 向け sample 表示を1つの共有 filesystem fixture で検証する coverage を維持してください。
- `QueryCommandRunnerSearchTests` の recipe issue-draft summary coverage は、専用 compact DTO を完全版 draft body から分離し、返却 / total / omitted row の cardinality(`count == drafts.length` と `--total-limit` に隠れた positive query を含む)、limit 適用前の evidence-path count と authority / lower-bound metadata の維持、multibyte path と末尾改行を含む UTF-8 document 全体の計測、完全な row 単位の truncation、型付き `E028` の audit identity、完全版 output contract の維持を検証します。
- Issue #5191 の compact audit replay coverage は `QueryCommandRunnerIssue5191Tests` が担当します。recipe 名、query 名、path filter、database path、data directory にメタ文字を含む単一 query / 複数 query の共有 fixture を維持し、audit / search の compact state を比較し、同じ構造化 argv からの POSIX sh / PowerShell rendering を検証し、selector、filter、database / data-directory 選択、limit、cursor、byte budget を保持したすべての query 別 cursor replay と results-only replay の underlying argv を実行してください。
- marker gate の reference coverage は、言語横断の正しい構文を `ReferenceExtractorMarkerGateTests.cs` にまとめ、GraphQL/HTML/Markdown と XAML/XML の専用 case で marker のない継続行でも parser state が更新されることを固定します。`ReferenceExtractorPerformanceBudgetTests` は相補的な3つの回帰契約を所有します。Kotlin infix 検出では大きな既知名集合を列挙しないこと、Docker instruction 分類では allocation が0で無関係な行長ではなく instruction prefix に応じて scale すること、warmup 済みの C#/Java/Kotlin それぞれ10,000行の markerless declaration file では reference が0件のまま、言語別 allocation 上限 20,000,000 / 12,500,000 / 40,000,000 byte未満かつ primary target の合計3秒 runaway guard内であることを検証します。`Extract_ReusedContainerResolverDelegates_RemainLineAndExtractionScoped`、cancellation 後の再実行、連続する Swift property case は、delegate の同期 lifetime、extraction-local な並行性、token 伝播、行ごとの container reset を固定します。広い allocation / time guard は `net8.0` に限定し、focused な意味論とstate維持のテストはcross-targetのままにしてください。
- フレームワーク: xUnit
- メインのテストプロジェクト: `tests/CodeIndex.Tests/CodeIndex.Tests.csproj`
Expand Down
4 changes: 2 additions & 2 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1435,7 +1435,7 @@ cdidx search --recipe risky-code --show-excluded --json # include recipe sc
cdidx search --recipe risky-code/raw-diagnostic-echo --format compact --limit 20 # summary-first compact JSON with next_cursor
cdidx search --recipe risky-code/raw-diagnostic-echo --format sarif --limit 20 # bounded SARIF audit findings
cdidx search --recipe risky-code/raw-diagnostic-echo --format compact --cursor <next_cursor>
cdidx audit risky-code --results-only --search-fields path,line,query_name,recipe --json=ndjson --max-json-bytes 65536 # minimal audit rows
cdidx search --recipe risky-code --results-only --search-fields path,line,query_name,recipe --json=ndjson --max-json-bytes 65536 # minimal audit rows
cdidx search --recipe risky-code --format count --summary-only --max-json-bytes 20000 # compact recipe counts
cdidx search --named-query pack="dotnet pack" --named-query push="nuget push" --format compact # named ad hoc batch with compact snippets
cdidx search "catch (Exception" --group-by file --count --json # rank broad audit hits by file
Expand Down Expand Up @@ -5142,7 +5142,7 @@ cdidx search --recipe risky-code --show-excluded --json # recipe scope / ex
cdidx search --recipe risky-code/raw-diagnostic-echo --format compact --limit 20 # summary-first compact JSON と next_cursor
cdidx search --recipe risky-code/raw-diagnostic-echo --format sarif --limit 20 # 上限付き SARIF audit finding
cdidx search --recipe risky-code/raw-diagnostic-echo --format compact --cursor <next_cursor>
cdidx audit risky-code --results-only --search-fields path,line,query_name,recipe --json=ndjson --max-json-bytes 65536 # 最小限の audit row
cdidx search --recipe risky-code --results-only --search-fields path,line,query_name,recipe --json=ndjson --max-json-bytes 65536 # 最小限の audit row
cdidx search --recipe risky-code --format count --summary-only --max-json-bytes 20000 # compact な recipe count
cdidx search --named-query pack="dotnet pack" --named-query push="nuget push" --format compact # 名前付き ad hoc batch と compact snippet
cdidx search "catch (Exception" --group-by file --count --json # 広い audit hit を file 別にランク付け
Expand Down
24 changes: 24 additions & 0 deletions changelog.d/unreleased/5191.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
category: fixed
issues:
- 5191
affected:
- src/CodeIndex/Cli/ExcerptRecoveryCommandFormatter.cs
- src/CodeIndex/Cli/QueryCommandRunner.cs
- src/CodeIndex/Cli/QueryCommandRunner.OptionValidation.cs
- src/CodeIndex/Cli/QueryCommandRunner.SearchRecipes.cs
- tests/CodeIndex.Tests/ExcerptRecoveryCommandFormatterTests.cs
- tests/CodeIndex.Tests/QueryCommandRunnerIssue5191Tests.cs
- tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs
- DEVELOPER_GUIDE.md
- USER_GUIDE.md
- TESTING_GUIDE.md
---

## English

- **Compact audit output now emits executable results-only replay commands (#5191)** — generated replays use the canonical `search --recipe` entry point, preserve selectors, filters, cursors, data locations, limits, and byte budgets, and safely quote structured arguments for the active shell.

## 日本語

- **compact audit 出力が実行可能な results-only replay command を生成するようになりました (#5191)** — 生成 replay は canonical な `search --recipe` entry point を使用し、selector、filter、cursor、data location、limit、byte budget を保持しながら、構造化された引数を active shell 向けに安全に quote します。
118 changes: 117 additions & 1 deletion src/CodeIndex/Cli/ExcerptRecoveryCommandFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using CodeIndex.Database;
using CodeIndex.Diagnostics;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;

namespace CodeIndex.Cli;

Expand Down Expand Up @@ -79,6 +81,9 @@ internal static string RenderDisplayCommand(IReadOnlyList<string> argv, Recovery
: rendered;
}

internal static string RenderDisplayCommandForCurrentShell(IReadOnlyList<string> argv)
=> RenderDisplayCommand(argv, ResolveCurrentShell());

private static List<string> BuildArgv(
string path,
int startLine,
Expand Down Expand Up @@ -182,7 +187,118 @@ private static bool IsDotnetHost(string processPath)
StringComparison.OrdinalIgnoreCase);

private static RecoveryCommandShell ResolveCurrentShell()
=> OperatingSystem.IsWindows() ? RecoveryCommandShell.PowerShell : RecoveryCommandShell.PosixSh;
=> ResolveShell(
TryGetParentProcessName(),
OperatingSystem.IsWindows(),
hasMsysEnvironment: !string.IsNullOrWhiteSpace(CdidxEnvironment.GetEnvironmentVariable("MSYSTEM")));

internal static RecoveryCommandShell ResolveShell(
string? parentProcessName,
bool isWindows,
bool hasMsysEnvironment)
{
var normalizedParent = Path.GetFileNameWithoutExtension(parentProcessName?.Replace('\\', '/') ?? string.Empty)
.ToLowerInvariant();
if (normalizedParent is "pwsh" or "powershell" or "powershell_ise")
return RecoveryCommandShell.PowerShell;
if (normalizedParent is "sh" or "bash" or "zsh" or "dash" or "ksh" or "fish" or "git-bash")
return RecoveryCommandShell.PosixSh;

if (hasMsysEnvironment)
return RecoveryCommandShell.PosixSh;
return isWindows ? RecoveryCommandShell.PowerShell : RecoveryCommandShell.PosixSh;
}

private static string? TryGetParentProcessName()
{
try
{
var parentProcessId = OperatingSystem.IsWindows()
? TryGetWindowsParentProcessId(Environment.ProcessId)
: GetParentProcessId();
if (!parentProcessId.HasValue || parentProcessId.Value <= 0)
return null;

using var parent = Process.GetProcessById(parentProcessId.Value);
return parent.ProcessName;
}
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or System.ComponentModel.Win32Exception)
{
return null;
}
}

private static int? TryGetWindowsParentProcessId(int processId)
{
if (!OperatingSystem.IsWindows())
return null;

var snapshot = CreateToolhelp32Snapshot(SnapshotProcesses, 0);
if (snapshot == InvalidHandleValue)
return null;

try
{
var entry = new ProcessEntry32
{
Size = (uint)Marshal.SizeOf<ProcessEntry32>(),
};
if (!Process32First(snapshot, ref entry))
return null;

do
{
if (entry.ProcessId == processId)
return checked((int)entry.ParentProcessId);
}
while (Process32Next(snapshot, ref entry));
return null;
}
finally
{
CloseHandle(snapshot);
}
}

private const uint SnapshotProcesses = 0x00000002;
private static readonly IntPtr InvalidHandleValue = new(-1);

[DllImport("libc")]
private static extern int getppid();

private static int GetParentProcessId() => getppid();

[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateToolhelp32Snapshot(uint flags, uint processId);

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Process32First(IntPtr snapshot, ref ProcessEntry32 entry);

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Process32Next(IntPtr snapshot, ref ProcessEntry32 entry);

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr handle);

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct ProcessEntry32
{
internal uint Size;
internal uint Usage;
internal uint ProcessId;
internal IntPtr DefaultHeapId;
internal uint ModuleId;
internal uint Threads;
internal uint ParentProcessId;
internal int BasePriority;
internal uint Flags;

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
internal string ExecutableFile;
}

private static string FormatShell(RecoveryCommandShell shell)
=> shell == RecoveryCommandShell.PowerShell ? "powershell" : "posix-sh";
Expand Down
6 changes: 4 additions & 2 deletions src/CodeIndex/Cli/QueryCommandRunner.OptionValidation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ private static bool TryWriteParseError(QueryCommandOptions options, string comma
commandName,
commandName,
RecipeNameIsPositional: false,
StructuredMachineUsageErrors: false),
StructuredMachineUsageErrors: false,
SupportsRecipeResultsOnlyNdjson: false),
jsonOptions);

private static bool TryWriteParseError(
Expand Down Expand Up @@ -417,7 +418,8 @@ private static bool TryWriteUnsupportedOptionError(
commandName,
commandName,
RecipeNameIsPositional: false,
StructuredMachineUsageErrors: false),
StructuredMachineUsageErrors: false,
SupportsRecipeResultsOnlyNdjson: false),
cmdArgs,
supportedOptions,
queryLiteral: queryLiteral,
Expand Down
54 changes: 49 additions & 5 deletions src/CodeIndex/Cli/QueryCommandRunner.SearchRecipes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1539,9 +1539,30 @@ private static string BuildSearchRecipeCompactReplayCommand(
string? cursor,
bool resultsOnly,
bool includeRecipeQuerySelectors)
{
var args = BuildSearchRecipeCompactReplayArguments(
recipeSelector,
options,
cursor,
resultsOnly,
includeRecipeQuerySelectors);
return ExcerptRecoveryCommandFormatter.RenderDisplayCommandForCurrentShell(args);
}

private static List<string> BuildSearchRecipeCompactReplayArguments(
string recipeSelector,
QueryCommandOptions options,
string? cursor,
bool resultsOnly,
bool includeRecipeQuerySelectors)
{
var args = new List<string>();
options.InvocationContext.AddRecipeCommandPrefix(args, recipeSelector);
options.InvocationContext.AddRecipeCommandPrefix(
args,
recipeSelector,
resultsOnly
? RecipeReplayOutputCapability.ResultsOnlyNdjson
: RecipeReplayOutputCapability.Default);
if (resultsOnly)
{
args.Add("--json=ndjson");
Expand All @@ -1556,10 +1577,33 @@ private static string BuildSearchRecipeCompactReplayCommand(
AddReplayValueOption(args, "--cursor", cursor);
AddReplayValueOption(args, "--limit", options.Limit.ToString(CultureInfo.InvariantCulture));
AddSearchRecipeCompactReplayOptions(args, options, includeRecipeQuerySelectors);
var command = string.Join(" ", args.Select(QuoteReplayShellArg));
return resultsOnly && !options.MaxJsonBytes.HasValue
? command + " --max-json-bytes <bytes>"
: command;
if (resultsOnly && !options.MaxJsonBytes.HasValue)
AddReplayValueOption(args, "--max-json-bytes", "<bytes>");
return args;
}

internal static (
IReadOnlyList<string> Argv,
string PosixSh,
string PowerShell,
string CurrentShell) BuildSearchRecipeCompactReplayCommandForTests(
string recipeSelector,
QueryCommandOptions options,
string? cursor,
bool resultsOnly,
bool includeRecipeQuerySelectors)
{
var argv = BuildSearchRecipeCompactReplayArguments(
recipeSelector,
options,
cursor,
resultsOnly,
includeRecipeQuerySelectors);
return (
argv,
ExcerptRecoveryCommandFormatter.RenderDisplayCommand(argv, RecoveryCommandShell.PosixSh),
ExcerptRecoveryCommandFormatter.RenderDisplayCommand(argv, RecoveryCommandShell.PowerShell),
ExcerptRecoveryCommandFormatter.RenderDisplayCommandForCurrentShell(argv));
}

private static void AddSearchRecipeCompactReplayOptions(
Expand Down
Loading
Loading