From 8053bb769b4c1c888b2e37f7c95da6cf38752406 Mon Sep 17 00:00:00 2001 From: Bruno Capuano Date: Wed, 1 Jul 2026 10:16:37 -0400 Subject: [PATCH] Add file progress updates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Graphify.Cli/PipelineRunner.cs | 37 ++++++++++++++++++- src/Graphify/Pipeline/WatchMode.cs | 8 ++++ .../Graphify.Tests/Cli/PipelineRunnerTests.cs | 30 +++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/Graphify.Cli/PipelineRunner.cs b/src/Graphify.Cli/PipelineRunner.cs index ca9a8de..b960101 100644 --- a/src/Graphify.Cli/PipelineRunner.cs +++ b/src/Graphify.Cli/PipelineRunner.cs @@ -15,6 +15,7 @@ public sealed class PipelineRunner private readonly TextWriter _output; private readonly bool _verbose; private readonly IChatClient? _chatClient; + private readonly SemaphoreSlim _outputLock = new(1, 1); public PipelineRunner(TextWriter output, bool verbose = false, IChatClient? chatClient = null) { @@ -66,7 +67,9 @@ public PipelineRunner(TextWriter output, bool verbose = false, IChatClient? chat var extractionBag = new ConcurrentBag(); int processed = 0; int skipped = 0; + int completed = 0; var verboseWarnings = new ConcurrentQueue(); + var extractionProgressStep = Math.Max(1, detectedFiles.Count / 10); await Parallel.ForEachAsync( detectedFiles, @@ -89,10 +92,21 @@ await Parallel.ForEachAsync( { Interlocked.Increment(ref skipped); } + + var finished = Interlocked.Increment(ref completed); + if (ShouldReportProgress(finished, detectedFiles.Count, extractionProgressStep)) + { + await WriteLineAsync($" Progress: {finished}/{detectedFiles.Count} files analyzed"); + } } catch (Exception ex) { Interlocked.Increment(ref skipped); + var finished = Interlocked.Increment(ref completed); + if (ShouldReportProgress(finished, detectedFiles.Count, extractionProgressStep)) + { + await WriteLineAsync($" Progress: {finished}/{detectedFiles.Count} files analyzed"); + } if (_verbose) { verboseWarnings.Enqueue($" Warning: Failed to extract {file.RelativePath}: {ex.Message}"); @@ -118,6 +132,8 @@ await Parallel.ForEachAsync( await WriteLineAsync("[2b/6] Running AI-enhanced semantic extraction..."); var semanticExtractor = new SemanticExtractor(_chatClient); int semanticProcessed = 0; + int semanticCompleted = 0; + var semanticProgressStep = Math.Max(1, detectedFiles.Count / 10); foreach (var file in detectedFiles) { @@ -136,6 +152,12 @@ await Parallel.ForEachAsync( if (_verbose) await WriteLineAsync($" Warning: Semantic extraction failed for {file.RelativePath}: {ex.Message}"); } + + var finished = ++semanticCompleted; + if (ShouldReportProgress(finished, detectedFiles.Count, semanticProgressStep)) + { + await WriteLineAsync($" Progress: {finished}/{detectedFiles.Count} files semantically analyzed"); + } } await WriteLineAsync($" AI extracted from {semanticProcessed} files"); @@ -333,7 +355,20 @@ await Parallel.ForEachAsync( private async Task WriteLineAsync(string message = "") { - await _output.WriteLineAsync(message); + await _outputLock.WaitAsync(); + try + { + await _output.WriteLineAsync(message); + } + finally + { + _outputLock.Release(); + } + } + + private static bool ShouldReportProgress(int completed, int total, int step) + { + return completed == total || completed % step == 0; } private static Dictionary BuildCommunityLabels(KnowledgeGraph graph) diff --git a/src/Graphify/Pipeline/WatchMode.cs b/src/Graphify/Pipeline/WatchMode.cs index c47983e..2688de7 100644 --- a/src/Graphify/Pipeline/WatchMode.cs +++ b/src/Graphify/Pipeline/WatchMode.cs @@ -188,6 +188,8 @@ private async Task ProcessChangesAsync( // Extract var extractor = new Extractor(); var newResults = new List(); + var progressStep = Math.Max(1, filesToProcess.Count / 10); + var completed = 0; foreach (var file in filesToProcess) { ct.ThrowIfCancellationRequested(); @@ -202,6 +204,12 @@ private async Task ProcessChangesAsync( if (_verbose) await _output.WriteLineAsync($" Warning: extraction failed for {file.RelativePath}: {ex.Message}"); } + + completed++; + if (completed == filesToProcess.Count || completed % progressStep == 0) + { + await _output.WriteLineAsync($" Progress: {completed}/{filesToProcess.Count} changed files analyzed"); + } } if (newResults.Count == 0) diff --git a/src/tests/Graphify.Tests/Cli/PipelineRunnerTests.cs b/src/tests/Graphify.Tests/Cli/PipelineRunnerTests.cs index 70c9447..9249271 100644 --- a/src/tests/Graphify.Tests/Cli/PipelineRunnerTests.cs +++ b/src/tests/Graphify.Tests/Cli/PipelineRunnerTests.cs @@ -72,4 +72,34 @@ public async Task RunAsync_LadybugFormat_RoutesToLadybugExporter() try { if (Directory.Exists(tempOutput)) Directory.Delete(tempOutput, recursive: true); } catch { /* best-effort cleanup */ } } } + + [Fact] + [Trait("Category", "Cli")] + public async Task RunAsync_ReportsFileProgressDuringExtraction() + { + var tempInput = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + var tempOutput = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempInput); + + try + { + await File.WriteAllTextAsync(Path.Combine(tempInput, "alpha.cs"), "namespace Demo; public sealed class Alpha { }"); + await File.WriteAllTextAsync(Path.Combine(tempInput, "beta.cs"), "namespace Demo; public sealed class Beta { }"); + + var sb = new StringBuilder(); + await using var writer = new StringWriter(sb); + var runner = new PipelineRunner(writer, verbose: false); + + await runner.RunAsync(tempInput, tempOutput, formats: ["json"], useCache: false); + + var log = sb.ToString(); + Assert.Contains("Progress:", log, StringComparison.OrdinalIgnoreCase); + Assert.Contains("2/2", log, StringComparison.OrdinalIgnoreCase); + } + finally + { + try { Directory.Delete(tempInput, recursive: true); } catch { /* best-effort cleanup */ } + try { if (Directory.Exists(tempOutput)) Directory.Delete(tempOutput, recursive: true); } catch { /* best-effort cleanup */ } + } + } }