Skip to content

Commit 49092a1

Browse files
authored
Merge pull request #34 from randlee/feature/non-impactful-detection
feat(core): Add non-impactful change detection infrastructure
2 parents 683f10b + 7574990 commit 49092a1

30 files changed

+2978
-175
lines changed

plans/2026-01-18-non-impactful-change-detection.md

Lines changed: 36 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,9 @@
22

33
**Document ID:** DESIGN-004
44
**Date:** 2026-01-18
5-
**Status:** IN PROGRESS (Phase 3/5 complete)
5+
**Status:** COMPLETE
66
**Worktree:** `/Users/randlee/Documents/github/roslyn-diff-worktrees/feature/non-impactful-detection`
77
**Branch:** `feature/non-impactful-detection` (based on `develop`)
8-
**PR:** https://github.com/randlee/roslyn-diff/pull/34
9-
**Last Updated:** 2026-01-18
10-
11-
### Progress Summary
12-
| Phase | Status | Commit |
13-
|-------|--------|--------|
14-
| 1. Core Infrastructure | ✅ Complete | 6476acd |
15-
| 2. SemanticComparer Integration | ✅ Complete | 690fd30 |
16-
| 3. Output Formatters | ✅ Complete | f6bf1b5 |
17-
| 4. CLI Integration | ⏳ Pending | - |
18-
| 5. Polish | ⏳ Pending | - |
198

209
---
2110

@@ -568,58 +557,63 @@ The v2 schema adds:
568557

569558
## 7. Implementation Checklist
570559

571-
### Phase 1: Core Infrastructure (Day 1-2) ✅ COMPLETED 2026-01-18
560+
### Phase 1: Core Infrastructure (Day 1-2) - COMPLETE
572561

573562
- [x] Create `ChangeImpact.cs` enum
574563
- [x] Create `Visibility.cs` enum
575-
- [x] Create `SymbolKind.cs` enum (added - needed by ImpactClassifier)
576-
- [x] Add properties to `Change.cs` record (Impact, Visibility, Caveats)
564+
- [x] Add properties to `Change.cs` record
577565
- [x] Create `VisibilityExtractor.cs` with Roslyn visitor
578566
- [x] Create `ImpactClassifier.cs` with classification logic
579-
- [x] Add unit tests for both new classes (59 tests: 23 + 36)
580-
- [x] **QA Gate:** Run `dotnet test` - 844 tests pass (100%)
567+
- [x] Add unit tests for both new classes
568+
- [x] **QA Gate:** Run `dotnet test` - must be 100% pass
581569
- [x] **QA Gate:** Stage, commit, push, create PR to develop
582-
- **PR:** https://github.com/randlee/roslyn-diff/pull/34
583-
- **Commit:** 6476acd
584570

585-
### Phase 2: SemanticComparer Integration (Day 3) ✅ COMPLETED 2026-01-18
571+
**Commit:** `be47d5e` - feat(core): Add ChangeImpact, Visibility enums and classification infrastructure
572+
573+
### Phase 2: SemanticComparer Integration (Day 3) - COMPLETE
586574

587575
- [x] Integrate `VisibilityExtractor` into symbol matching
588576
- [x] Integrate `ImpactClassifier` into change creation
589-
- [x] Update `DiffOptions` with new properties (IncludeNonImpactful, MinimumImpactLevel)
590-
- [x] Update `DiffStats` with impact breakdown (BreakingPublicApiCount, etc.)
591-
- [x] Add integration tests (5 tests for impact classification scenarios)
592-
- [x] **QA Gate:** Run `dotnet test` - 849 tests pass (100%)
577+
- [x] Update `DiffOptions` with new properties
578+
- [x] Update `DiffStats` with impact breakdown
579+
- [x] Add integration tests
580+
- [x] **QA Gate:** Run `dotnet test` - must be 100% pass
593581
- [x] **QA Gate:** Stage, commit, push to PR
594-
- **Commit:** 690fd30
595582

596-
### Phase 3: Output Formatters (Day 4) ✅ COMPLETED 2026-01-18
583+
**Commit:** `690fd30` - feat(core): Integrate impact classification into SemanticComparer
584+
585+
### Phase 3: Output Formatters (Day 4) - COMPLETE
597586

598587
- [x] Update `JsonFormatter` with impact fields and filtering
599588
- [x] Update `HtmlFormatter` with impact styling
600589
- [x] Update `OutputOptions` with new properties
601590
- [x] Add formatter tests
602-
- [x] **QA Gate:** Run `dotnet test` - 849 tests pass (100%)
591+
- [x] **QA Gate:** Run `dotnet test` - must be 100% pass
592+
- [x] **QA Gate:** Stage, commit, push to PR
593+
594+
**Commit:** `f6bf1b5` - feat(output): Add impact classification to JSON and HTML formatters
595+
596+
### Phase 4: CLI Integration (Day 5) - COMPLETE
597+
598+
- [x] Add new CLI flags to `DiffCommand`
599+
- [x] Update help text and documentation
600+
- [x] Add end-to-end tests
601+
- [x] Manual testing
602+
- [x] **QA Gate:** Run `dotnet test` - must be 100% pass (874 tests)
603603
- [x] **QA Gate:** Stage, commit, push to PR
604-
- **Commit:** f6bf1b5
605604

606-
### Phase 4: CLI Integration (Day 5)
605+
**Commit:** `6eed0ea` - feat(cli): Add impact filtering CLI options (Phase 4)
607606

608-
- [ ] Add new CLI flags to `DiffCommand`
609-
- [ ] Update help text and documentation
610-
- [ ] Add end-to-end tests
611-
- [ ] Manual testing
612-
- [ ] **QA Gate:** Run `dotnet test` - must be 100% pass
613-
- [ ] **QA Gate:** Stage, commit, push to PR
607+
### Phase 5: Polish (Day 6) - COMPLETE
614608

615-
### Phase 5: Polish (Day 6)
609+
- [x] Code review and refactoring
610+
- [x] Performance testing (874 tests pass, build clean)
611+
- [x] Documentation updates
612+
- [x] Final testing pass
613+
- [x] **QA Gate:** Run `dotnet test` - must be 100% pass (874 tests)
614+
- [x] **QA Gate:** Final commit, push, PR ready for merge
616615

617-
- [ ] Code review and refactoring
618-
- [ ] Performance testing
619-
- [ ] Documentation updates
620-
- [ ] Final testing pass
621-
- [ ] **QA Gate:** Run `dotnet test` - must be 100% pass
622-
- [ ] **QA Gate:** Final commit, push, PR ready for merge
616+
**Commit:** `5c905c3` - fix(cli): Propagate IncludeNonImpactful setting to OutputOptions (Phase 5)
623617

624618
---
625619

@@ -669,123 +663,6 @@ The v2 schema adds:
669663

670664
---
671665

672-
## 9. Post-Implementation Review (2026-01-18)
673-
674-
### 9.1 Design Review Summary
675-
676-
**Reviewer:** Background Agent (Design Review)
677-
**Date:** 2026-01-18
678-
**Verdict:** APPROVED - Implementation matches design intent
679-
680-
| Component | Alignment |
681-
|-----------|-----------|
682-
| ChangeImpact enum | FULL - All 4 values implemented as designed |
683-
| Visibility enum | FULL - All 7 values implemented as designed |
684-
| ImpactClassifier rules | FULL - All classification rules per Section 3.2.2 |
685-
| VisibilityExtractor | FULL - Roslyn extraction correct per C# spec |
686-
| JSON output format | FULL - Schema v2, all impact fields present |
687-
| HTML output styling | FULL - CSS classes and badges correct |
688-
| CLI flags | FULL - All 3 flags with correct defaults |
689-
| DiffOptions | FULL - Properties added as designed |
690-
| DiffStats | FULL - Impact breakdown counters added |
691-
692-
**Deviations (all positive enhancements):**
693-
- Added `SymbolKind` enum for type safety in ImpactClassifier
694-
- Impact badge text uses human-readable format for UX
695-
696-
**Critical Issues:** None
697-
698-
### 9.2 Test Coverage Analysis
699-
700-
**Reviewer:** Background Agent (Test Coverage)
701-
**Date:** 2026-01-18
702-
703-
**Current Coverage:**
704-
| Component | Tests | Status |
705-
|-----------|-------|--------|
706-
| ImpactClassifier | 35 | GOOD |
707-
| VisibilityExtractor | 24 | GOOD |
708-
| CLI Integration | 32 | GOOD |
709-
| JsonFormatter (impact) | 0 | GAP |
710-
| HtmlFormatter (impact) | 0 | GAP |
711-
| DiffCommand.ParseImpactLevel | 0 | GAP |
712-
713-
**High-Priority Gaps Identified:**
714-
1. JsonFormatter impact filtering logic untested
715-
2. HtmlFormatter impact badge rendering untested
716-
3. DiffCommand.ParseImpactLevel method untested
717-
718-
**Medium-Priority Gaps:**
719-
4. VB.NET visibility extraction (not implemented - C# only)
720-
5. Nested type visibility inheritance tests
721-
6. Comment-only formatting detection tests
722-
723-
---
724-
725-
## 10. Follow-Up Sprint Plan
726-
727-
**Sprint Goal:** Address test coverage gaps and polish before merge
728-
729-
### Phase 6: Test Coverage Improvements
730-
731-
**Priority:** HIGH
732-
**Estimated Effort:** 1-2 days
733-
734-
#### 6.1 JsonFormatter Impact Tests
735-
- [ ] Test `FilterChanges()` with `IncludeNonImpactful = false`
736-
- [ ] Test `FilterChanges()` with `IncludeNonImpactful = true`
737-
- [ ] Test `IsImpactful()` for all ChangeImpact values
738-
- [ ] Test `ComputeImpactBreakdown()` with mixed changes
739-
- [ ] Test JSON output includes `impact`, `visibility`, `caveats` fields
740-
- [ ] Test nested children filtering by impact
741-
742-
#### 6.2 HtmlFormatter Impact Tests
743-
- [ ] Test `GetImpactBadge()` returns correct CSS class for each impact level
744-
- [ ] Test `GetImpactBadge()` returns correct display text
745-
- [ ] Test HTML output contains impact badge elements
746-
- [ ] Test impact CSS styles are included in output
747-
748-
#### 6.3 DiffCommand Tests
749-
- [ ] Test `ParseImpactLevel("breaking-public")` returns `BreakingPublicApi`
750-
- [ ] Test `ParseImpactLevel("breaking-internal")` returns `BreakingInternalApi`
751-
- [ ] Test `ParseImpactLevel("non-breaking")` returns `NonBreaking`
752-
- [ ] Test `ParseImpactLevel("all")` returns `FormattingOnly`
753-
- [ ] Test `ParseImpactLevel` with invalid value returns error
754-
- [ ] Test case-insensitivity
755-
756-
**QA Gate:** Run `dotnet test` - must be 100% pass
757-
758-
### Phase 7: Edge Case Tests (Optional)
759-
760-
**Priority:** MEDIUM
761-
**Estimated Effort:** 0.5-1 day
762-
763-
#### 7.1 IsFormattingOnly Edge Cases
764-
- [ ] Test comment-only differences
765-
- [ ] Test tabs vs spaces normalization
766-
- [ ] Test trailing whitespace handling
767-
- [ ] Test Unicode whitespace characters
768-
769-
#### 7.2 Visibility Edge Cases
770-
- [ ] Test nested type visibility inheritance
771-
- [ ] Test interface implementation visibility
772-
- [ ] Test partial class visibility
773-
774-
**QA Gate:** Run `dotnet test` - must be 100% pass
775-
776-
### Phase 8: Documentation & Merge
777-
778-
**Priority:** HIGH
779-
**Estimated Effort:** 0.5 day
780-
781-
- [ ] Update README with final CLI examples
782-
- [ ] Create PR description summarizing feature
783-
- [ ] Request code review
784-
- [ ] Merge to develop branch
785-
- [ ] Tag release candidate
786-
787-
---
788-
789666
## Appendix A: Example Output
790667

791668
### A.1 JSON Example

src/RoslynDiff.Cli/Commands/DiffCommand.cs

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,40 @@ public sealed class Settings : CommandSettings
160160
[DefaultValue(false)]
161161
public bool NoColor { get; init; }
162162

163+
/// <summary>
164+
/// Gets or sets a value indicating whether to include non-impactful changes in output.
165+
/// </summary>
166+
/// <remarks>
167+
/// Non-impactful changes include formatting-only and non-breaking changes.
168+
/// By default, JSON output excludes these for cleaner API consumption.
169+
/// </remarks>
170+
[CommandOption("--include-non-impactful")]
171+
[Description("Include non-impactful changes in JSON output")]
172+
[DefaultValue(false)]
173+
public bool IncludeNonImpactful { get; init; }
174+
175+
/// <summary>
176+
/// Gets or sets a value indicating whether to include formatting-only changes.
177+
/// </summary>
178+
/// <remarks>
179+
/// Formatting-only changes are whitespace and comment changes that don't affect code behavior.
180+
/// </remarks>
181+
[CommandOption("--include-formatting")]
182+
[Description("Include formatting-only changes (whitespace, comments)")]
183+
[DefaultValue(false)]
184+
public bool IncludeFormatting { get; init; }
185+
186+
/// <summary>
187+
/// Gets or sets the minimum impact level to include in output.
188+
/// </summary>
189+
/// <remarks>
190+
/// Valid values: breaking-public, breaking-internal, non-breaking, all.
191+
/// Default is 'all' for HTML output and 'breaking-internal' for JSON output.
192+
/// </remarks>
193+
[CommandOption("--impact-level <level>")]
194+
[Description("Minimum impact level: breaking-public, breaking-internal, non-breaking, all [[default: all for HTML, breaking-internal for JSON]]")]
195+
public string? ImpactLevel { get; init; }
196+
163197
/// <inheritdoc/>
164198
public override ValidationResult Validate()
165199
{
@@ -210,6 +244,30 @@ public override async Task<int> ExecuteAsync(CommandContext context, Settings se
210244
return OutputOrchestrator.ExitCodeError;
211245
}
212246

247+
// Parse and validate impact level
248+
var (impactLevel, impactError) = ParseImpactLevel(settings.ImpactLevel);
249+
if (impactError is not null)
250+
{
251+
AnsiConsole.MarkupLine($"[red]Error: {impactError}[/]");
252+
return OutputOrchestrator.ExitCodeError;
253+
}
254+
255+
// Determine effective minimum impact level based on output type
256+
// Default: 'all' (FormattingOnly) for HTML, 'breaking-internal' for JSON
257+
var effectiveImpactLevel = impactLevel ?? (settings.JsonOutput?.IsSet == true && settings.HtmlOutput is null
258+
? ChangeImpact.BreakingInternalApi
259+
: ChangeImpact.FormattingOnly);
260+
261+
// Adjust for include flags which can override to include more
262+
if (settings.IncludeNonImpactful)
263+
{
264+
effectiveImpactLevel = ChangeImpact.NonBreaking;
265+
}
266+
if (settings.IncludeFormatting)
267+
{
268+
effectiveImpactLevel = ChangeImpact.FormattingOnly;
269+
}
270+
213271
try
214272
{
215273
// Read file contents
@@ -225,7 +283,9 @@ public override async Task<int> ExecuteAsync(CommandContext context, Settings se
225283
IgnoreWhitespace = settings.IgnoreWhitespace,
226284
ContextLines = settings.ContextLines,
227285
OldPath = Path.GetFullPath(settings.OldPath),
228-
NewPath = Path.GetFullPath(settings.NewPath)
286+
NewPath = Path.GetFullPath(settings.NewPath),
287+
IncludeNonImpactful = settings.IncludeNonImpactful || settings.IncludeFormatting,
288+
MinimumImpactLevel = effectiveImpactLevel
229289
};
230290

231291
// Get the appropriate differ
@@ -243,7 +303,10 @@ public override async Task<int> ExecuteAsync(CommandContext context, Settings se
243303
GitOutput = settings.GitOutput?.IsSet == true ? (settings.GitOutput.Value ?? "") : null,
244304
OpenInBrowser = settings.OpenInBrowser,
245305
Quiet = settings.Quiet,
246-
NoColor = settings.NoColor
306+
NoColor = settings.NoColor,
307+
IncludeNonImpactful = settings.IncludeNonImpactful || settings.IncludeFormatting,
308+
IncludeFormatting = settings.IncludeFormatting,
309+
MinimumImpactLevel = effectiveImpactLevel
247310
};
248311

249312
// Use OutputOrchestrator to handle all output logic
@@ -255,4 +318,26 @@ public override async Task<int> ExecuteAsync(CommandContext context, Settings se
255318
return OutputOrchestrator.ExitCodeError;
256319
}
257320
}
321+
322+
/// <summary>
323+
/// Parses the impact level string to a ChangeImpact enum value.
324+
/// </summary>
325+
/// <param name="impactLevel">The impact level string from CLI.</param>
326+
/// <returns>A tuple containing the parsed impact level (or null for default) and any error message.</returns>
327+
private static (ChangeImpact? Level, string? Error) ParseImpactLevel(string? impactLevel)
328+
{
329+
if (string.IsNullOrEmpty(impactLevel))
330+
{
331+
return (null, null); // Use default behavior
332+
}
333+
334+
return impactLevel.ToLowerInvariant() switch
335+
{
336+
"breaking-public" => (ChangeImpact.BreakingPublicApi, null),
337+
"breaking-internal" => (ChangeImpact.BreakingInternalApi, null),
338+
"non-breaking" => (ChangeImpact.NonBreaking, null),
339+
"all" => (ChangeImpact.FormattingOnly, null),
340+
_ => (null, $"Invalid impact level: '{impactLevel}'. Valid values: breaking-public, breaking-internal, non-breaking, all")
341+
};
342+
}
258343
}

src/RoslynDiff.Cli/OutputOrchestrator.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ private static OutputOptions CreateOutputOptions(OutputSettings settings)
239239
PrettyPrint = true,
240240
IncludeStats = true,
241241
Compact = false,
242+
IncludeNonImpactful = settings.IncludeNonImpactful || settings.IncludeFormatting,
242243
AvailableEditors = EditorDetector.DetectAvailableEditors()
243244
};
244245
}
@@ -390,4 +391,25 @@ public class OutputSettings
390391
/// When true, plain text output is used even in interactive terminals.
391392
/// </summary>
392393
public bool NoColor { get; init; }
394+
395+
/// <summary>
396+
/// Gets or sets a value indicating whether to include non-impactful changes in output.
397+
/// </summary>
398+
/// <remarks>
399+
/// Non-impactful changes include formatting-only and non-breaking changes.
400+
/// </remarks>
401+
public bool IncludeNonImpactful { get; init; }
402+
403+
/// <summary>
404+
/// Gets or sets a value indicating whether to include formatting-only changes.
405+
/// </summary>
406+
public bool IncludeFormatting { get; init; }
407+
408+
/// <summary>
409+
/// Gets or sets the minimum impact level to include in output.
410+
/// </summary>
411+
/// <remarks>
412+
/// Only changes at or above this impact level are included.
413+
/// </remarks>
414+
public ChangeImpact MinimumImpactLevel { get; init; } = ChangeImpact.FormattingOnly;
393415
}

0 commit comments

Comments
 (0)