diff --git a/CodeSmellAuditor.Core.Tests/AuditPromptBuilderTests.cs b/CodeSmellAuditor.Core.Tests/AuditPromptBuilderTests.cs index 9bd0120..687ebb7 100644 --- a/CodeSmellAuditor.Core.Tests/AuditPromptBuilderTests.cs +++ b/CodeSmellAuditor.Core.Tests/AuditPromptBuilderTests.cs @@ -17,7 +17,10 @@ public void BuildSystemPrompt_IncludesRulesAndOutputTemplate() Assert.Contains("[RULE: test.mdc]", prompt); Assert.Contains("Prefer interfaces at boundaries.", prompt); - Assert.Contains("Status: COMPLIANT or REVIEW REQUIRED", prompt); + Assert.Contains("Status: COMPLIANT", prompt); + Assert.Contains("REVIEW REQUIRED", prompt); + Assert.Contains("EXACTLY one of", prompt); + Assert.Contains("omit Missing Context", prompt); Assert.Contains("No analysis traces", prompt); } diff --git a/CodeSmellAuditor.Core.Tests/AuditReportParserTests.cs b/CodeSmellAuditor.Core.Tests/AuditReportParserTests.cs index 8fb9b0d..8ab649d 100644 --- a/CodeSmellAuditor.Core.Tests/AuditReportParserTests.cs +++ b/CodeSmellAuditor.Core.Tests/AuditReportParserTests.cs @@ -10,15 +10,47 @@ public class AuditReportParserTests [InlineData("Status: REVIEW REQUIRED", false)] [InlineData("# Report\nStatus: COMPLIANT\nScore: 90", true)] [InlineData("# Report\nStatus: REVIEW REQUIRED\nScore: 40", false)] - public void ParsePassStatus_ReadsStatusLine(string body, bool expectedPass) + [InlineData("status: compliant", true)] + [InlineData("Status: Review Required", false)] + public void ParsePassStatus_ReadsExactStatusLine(string body, bool expectedPass) { Assert.Equal(expectedPass, AuditReportParser.ParsePassStatus(body)); } [Fact] - public void ParsePassStatus_FallsBackWhenNoStatusLine() + public void ParsePassStatus_MissingStatusLine_FailsClosed() { Assert.False(AuditReportParser.ParsePassStatus("Some text with REVIEW REQUIRED in it")); - Assert.True(AuditReportParser.ParsePassStatus("All checks passed.")); + Assert.False(AuditReportParser.ParsePassStatus("All checks passed.")); + Assert.False(AuditReportParser.ParsePassStatus("This report looks COMPLIANT overall.")); + } + + [Fact] + public void ParsePassStatus_EmptyOrWhitespace_FailsClosed() + { + Assert.False(AuditReportParser.ParsePassStatus("")); + Assert.False(AuditReportParser.ParsePassStatus(" \n\t ")); + Assert.False(AuditReportParser.ParsePassStatus(null!)); + } + + [Fact] + public void ParsePassStatus_UnknownStatusValue_FailsClosed() + { + Assert.False(AuditReportParser.ParsePassStatus("Status: PASS")); + Assert.False(AuditReportParser.ParsePassStatus("Status: COMPLIANT WITH NOTES")); + Assert.False(AuditReportParser.ParsePassStatus("Status:")); + } + + [Fact] + public void ParsePassStatus_TruncatedReportWithoutStatus_FailsClosed() + { + string truncated = """ + # Audit Report: SampleService.cs + + ## Top Findings + - Domain logic references UI directly + """; + + Assert.False(AuditReportParser.ParsePassStatus(truncated)); } } diff --git a/CodeSmellAuditor.Core/AuditPromptBuilder.cs b/CodeSmellAuditor.Core/AuditPromptBuilder.cs index 58d0b36..0abab8c 100644 --- a/CodeSmellAuditor.Core/AuditPromptBuilder.cs +++ b/CodeSmellAuditor.Core/AuditPromptBuilder.cs @@ -16,8 +16,8 @@ public static class AuditPromptBuilder # Audit Report: {filename} - Status: COMPLIANT or REVIEW REQUIRED - Score: 0-100 + Status: COMPLIANT + Score: 85 ## Top Findings - (max 5 bullets, one line each) @@ -27,6 +27,11 @@ public static class AuditPromptBuilder ## Recommended Next Step One sentence. + + Contract rules: + - The Status line must appear exactly once and use EXACTLY one of: COMPLIANT or REVIEW REQUIRED (nothing else on that line after Status:). + - The Score line must appear exactly once as an integer from 0 to 100. + - Emit Status, Score, and Top Findings first. If the output budget is tight, omit Missing Context and/or Recommended Next Step rather than truncating or omitting Status. """; public static string BuildSystemPrompt(IEnumerable rules) @@ -67,8 +72,16 @@ public static void ValidateBudget(string systemPrompt, string userPrompt, AuditC public static class AuditReportParser { + /// + /// Pass/fail from a dedicated Status line only. Missing or unparseable Status fails closed. + /// public static bool ParsePassStatus(string reportBody) { + if (string.IsNullOrWhiteSpace(reportBody)) + { + return false; + } + foreach (string line in reportBody.Split('\n')) { string trimmed = line.Trim(); @@ -78,17 +91,21 @@ public static bool ParsePassStatus(string reportBody) } string statusValue = trimmed["Status:".Length..].Trim(); - if (statusValue.Contains("REVIEW REQUIRED", StringComparison.OrdinalIgnoreCase)) + if (statusValue.Equals("REVIEW REQUIRED", StringComparison.OrdinalIgnoreCase)) { return false; } - if (statusValue.Contains("COMPLIANT", StringComparison.OrdinalIgnoreCase)) + if (statusValue.Equals("COMPLIANT", StringComparison.OrdinalIgnoreCase)) { return true; } + + // Status line present but not an allowed value: fail closed. + return false; } - return !reportBody.Contains("REVIEW REQUIRED", StringComparison.OrdinalIgnoreCase); + // No Status line (truncated or drifted report): fail closed. + return false; } }