diff --git a/Tests/Unit/Core/Aggregators/FileCouplingAggregatorTests.cs b/Tests/Unit/Core/Aggregators/FileCouplingAggregatorTests.cs
index 63b19ba..ecc1ce2 100644
--- a/Tests/Unit/Core/Aggregators/FileCouplingAggregatorTests.cs
+++ b/Tests/Unit/Core/Aggregators/FileCouplingAggregatorTests.cs
@@ -1,5 +1,6 @@
using ChangeTrace.Core.Aggregators;
using ChangeTrace.Core.Events.Semantic;
+using ChangeTrace.Core.Options;
using Xunit;
namespace ChangeTrace.Tests.Core.Aggregators;
@@ -43,4 +44,22 @@ public void Process_DoesNotEmitPairsForSmallBundles(int fileCount)
Assert.Equal(0, writer.Count);
}
+
+ /// Process skips bundles that exceed the configured file threshold.
+ [Fact]
+ public void Process_SkipsLargeBundlesAboveConfiguredThreshold()
+ {
+ using var writer = new SemanticEventWriter();
+ var aggregator = new FileCouplingAggregator(
+ writer,
+ new FileCouplingAggregatorOptions(MaxFilesPerCommit: 4));
+
+ aggregator.Process(new CommitBundleEvent(
+ "commit-1",
+ "rian",
+ 123,
+ new[] { "a.cs", "b.cs", "c.cs", "d.cs", "e.cs" }));
+
+ Assert.Equal(0, writer.Count);
+ }
}
diff --git a/src/Core/Aggregators/FileCouplingAggregator.cs b/src/Core/Aggregators/FileCouplingAggregator.cs
index a8ce25f..dcf5ac0 100644
--- a/src/Core/Aggregators/FileCouplingAggregator.cs
+++ b/src/Core/Aggregators/FileCouplingAggregator.cs
@@ -1,5 +1,6 @@
using ChangeTrace.Core.Events.Semantic;
using ChangeTrace.Core.Interfaces;
+using ChangeTrace.Core.Options;
namespace ChangeTrace.Core.Aggregators;
@@ -17,9 +18,14 @@ namespace ChangeTrace.Core.Aggregators;
/// architectural coupling between files in repository over time.
///
///
-internal sealed class FileCouplingAggregator(SemanticEventWriter writer)
+internal sealed class FileCouplingAggregator(
+ SemanticEventWriter writer,
+ FileCouplingAggregatorOptions? options = null)
: IEventAggregator
{
+ private readonly FileCouplingAggregatorOptions _options =
+ options ?? new FileCouplingAggregatorOptions();
+
///
/// Processes single and emits
/// for all file pairs modified together.
@@ -44,7 +50,12 @@ public void Process(CommitBundleEvent bundle)
if (files.Length < 2)
return;
+ if (files.Length > _options.MaxFilesPerCommit)
+ return;
+
var timestamp = bundle.Timestamp;
+ var pairCount = files.Length * (files.Length - 1) / 2;
+ writer.EnsureAdditionalCapacity(pairCount);
for (var i = 0; i < files.Length; i++)
{
@@ -68,4 +79,4 @@ public void Flush()
{
// nothing buffered
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/Events/Semantic/SemanticEventWriter.cs b/src/Core/Events/Semantic/SemanticEventWriter.cs
index cf24b8d..7cd454c 100644
--- a/src/Core/Events/Semantic/SemanticEventWriter.cs
+++ b/src/Core/Events/Semantic/SemanticEventWriter.cs
@@ -53,6 +53,25 @@ public void Write(T evt)
_buffer[_count++] = evt;
}
+ ///
+ /// Ensures the writer has room for an additional number of events without repeated growth.
+ ///
+ /// The number of additional events expected to be written.
+ /// Thrown if the writer has been disposed.
+ public void EnsureAdditionalCapacity(int additionalCount)
+ {
+ if (_disposed) throw new ObjectDisposedException(nameof(SemanticEventWriter));
+ if (additionalCount <= 0)
+ return;
+
+ var required = _count + additionalCount;
+
+ if (required <= _buffer.Length)
+ return;
+
+ Grow(required);
+ }
+
///
/// Returns snapshot of the written events as .
///
@@ -90,8 +109,18 @@ public void Dispose()
/// Doubles buffer size when capacity is exceeded.
///
private void Grow()
+ => Grow(_buffer.Length * 2);
+
+ ///
+ /// Grows buffer to satisfy a required capacity.
+ ///
+ private void Grow(int requiredCapacity)
{
var newSize = _buffer.Length * 2;
+
+ if (newSize < requiredCapacity)
+ newSize = requiredCapacity;
+
var newBuffer = ArrayPool.Shared.Rent(newSize);
Array.Copy(_buffer, newBuffer, _count);
ArrayPool.Shared.Return(_buffer, clearArray: false);
diff --git a/src/Core/Options/FileCouplingAggregatorOptions.cs b/src/Core/Options/FileCouplingAggregatorOptions.cs
new file mode 100644
index 0000000..cd3adbe
--- /dev/null
+++ b/src/Core/Options/FileCouplingAggregatorOptions.cs
@@ -0,0 +1,12 @@
+namespace ChangeTrace.Core.Options;
+
+///
+/// Controls how file coupling pairs are generated from commit bundles.
+///
+///
+/// Maximum number of files in a commit bundle that will still produce full coupling pairs.
+/// Commits above this threshold are skipped to bound quadratic pair generation cost.
+///
+internal sealed record FileCouplingAggregatorOptions(
+ int MaxFilesPerCommit = 24
+);