diff --git a/PanoramicData.IssueAssessment.Tests/Rubrics/HeadingKeywordTests.cs b/PanoramicData.IssueAssessment.Tests/Rubrics/HeadingKeywordTests.cs
new file mode 100644
index 0000000..7cba656
--- /dev/null
+++ b/PanoramicData.IssueAssessment.Tests/Rubrics/HeadingKeywordTests.cs
@@ -0,0 +1,79 @@
+using PanoramicData.IssueAssessment.Mapping;
+using PanoramicData.IssueAssessment.Rubrics;
+
+namespace PanoramicData.IssueAssessment.Tests.Rubrics;
+
+///
+/// MS-23199 Section headings are credited whatever their level (h1 to h4), case or wording variant.
+///
+public class HeadingKeywordTests : TestBase
+{
+ private static readonly IssueAssessor Assessor = new(BuiltInRubrics.PanoramicDataTicketQuality);
+
+ [Theory]
+ [InlineData("Current Behavior", "Expected Behavior")]
+ [InlineData("Current Behaviour", "Expected Behaviour")]
+ [InlineData("Actual Behavior", "Expected Behavior")]
+ [InlineData("Actual Result", "Expected Result")]
+ [InlineData("Current Behavior", "Desired Behavior")]
+ [InlineData("current behavior", "EXPECTED BEHAVIOR")]
+ public async Task CurrentAndExpectedHeadings_ScoreFull_WhateverTheVariantOrCase(string current, string expected)
+ {
+ int score = await RawScore("CurrentVsExpected", $"h2. {current}\r\nRows with line breaks are missing.\r\n\r\nh2. {expected}\r\nEvery row is exported.");
+
+ score.Should().Be(100);
+ }
+
+ [Theory]
+ [InlineData("h1.")]
+ [InlineData("h2.")]
+ [InlineData("h3.")]
+ [InlineData("h4.")]
+ public async Task CurrentAndExpectedHeadings_ScoreFull_AtEveryJiraHeadingLevel(string marker)
+ {
+ int score = await RawScore("CurrentVsExpected", $"{marker} Current Behavior\r\nRows are missing.\r\n\r\n{marker} Expected Behavior\r\nEvery row is exported.");
+
+ score.Should().Be(100);
+ }
+
+ [Fact]
+ public async Task CurrentHeadingAlone_StillScoresHalf()
+ {
+ int score = await RawScore("CurrentVsExpected", "h2. Actual Result\r\nRows are missing.");
+
+ score.Should().Be(50);
+ }
+
+ [Theory]
+ [InlineData("Acceptance Criteria")]
+ [InlineData("Success Criteria")]
+ [InlineData("Definition of Done")]
+ [InlineData("ACCEPTANCE CRITERIA")]
+ public async Task CriteriaHeadings_RaiseTestabilityAboveADescriptionWithNone(string heading)
+ {
+ const string Body = "Rows with line breaks are missing from the export.";
+
+ int without = await RawScore("Testability", Body);
+ int with = await RawScore("Testability", $"{Body}\r\n\r\nh2. {heading}\r\n# Every row is exported\r\n# Line breaks are preserved");
+
+ with.Should().BeGreaterThan(without);
+ }
+
+ private static async Task RawScore(string name, string description)
+ {
+ NormalizedIssue issue = new()
+ {
+ Key = "OPS-1",
+ Project = "OPS",
+ Title = "Export to CSV drops rows that contain line breaks",
+ Description = description,
+ IssueType = "Bug",
+ Status = "In Progress",
+ Platform = "Jira",
+ Labels = [],
+ };
+ using JsonDocument document = issue.ToJsonDocument();
+ AssessmentReport report = AssessmentReport.From(await Assessor.AssessAsync(document, CancellationToken));
+ return (int)report.Categories.SelectMany(category => category.Assessments).Single(item => item.Name == name).RawScore!.Value;
+ }
+}
diff --git a/PanoramicData.IssueAssessment/Rubrics/panoramicdata-ticket-quality.json b/PanoramicData.IssueAssessment/Rubrics/panoramicdata-ticket-quality.json
index caea93f..02ec636 100644
--- a/PanoramicData.IssueAssessment/Rubrics/panoramicdata-ticket-quality.json
+++ b/PanoramicData.IssueAssessment/Rubrics/panoramicdata-ticket-quality.json
@@ -165,7 +165,7 @@
"measures": {
"scope": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "scope:", "h[1-6]\\.[^\\r\\n]*scope", "\\*\\*[^\\r\\n]*scope", "questions?\\s+to\\s+(answer|investigate)", "investigate\\s+(whether|if|how|why)", "determine\\s+(whether|if|how|why)", "find\\s+out", "understand\\s+(how|why|what)" ] },
"deliverables": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "deliverable", "findings", "recommendation", "report", "document", "conclusion", "outcome", "result\\s+of\\s+(investigation|research|analysis)" ] },
- "criteria": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "should\\s+(be|display|show|return|have|not)", "must\\s+(be|display|show|return|have|not)", "verify\\s+that", "acceptance\\s+criteria", "h[23]\\.\\s*acceptance\\s+criteria", "expected\\s+(result|behavior|behaviour|outcome)", "pass\\s+criteria", "success\\s+criteria" ] },
+ "criteria": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "should\\s+(be|display|show|return|have|not)", "must\\s+(be|display|show|return|have|not)", "verify\\s+that", "acceptance\\s+criteria", "h[1-4]\\.\\s*acceptance\\s+criteria", "expected\\s+(result|behavior|behaviour|outcome)", "pass\\s+criteria", "success\\s+criteria", "definition\\s+of\\s+done" ] },
"criteriaHeadings": { "type": "criteriaHeadingCount", "source": { "paths": [ "$.description" ] } }
},
"tiers": [
@@ -215,8 +215,8 @@
"type": "tiered",
"measures": {
"context": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "background:", "h[1-6]\\.[^\\r\\n]*background", "context:", "h[1-6]\\.[^\\r\\n]*context", "\\*\\*[^\\r\\n]*(background|context)", "motivation:", "why\\s+it\\s+matters", "why\\s+(we|this)", "reason\\s+for", "because" ] },
- "current": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "current(ly)?(\\s+behavior|\\s+behaviour)?:", "h[23]\\.\\s*current(\\s*/\\s*actual)?\\s*(behavior|behaviour)", "\\*\\*current\\s*(behavior|behaviour)\\*\\*", "\\*current(\\s*/\\s*actual)?\\s*(behavior|behaviour)?\\*", "\\*actual\\s*(behavior|behaviour)?\\*", "actual(\\s+behavior|\\s+behaviour)?:", "what\\s+happens:", "observed:", "bug:", "issue:", "problem:" ] },
- "expected": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "expected(\\s+behavior|\\s+behaviour)?:", "h[23]\\.\\s*expected\\s*(behavior|behaviour)", "\\*\\*expected\\s*(behavior|behaviour)\\*\\*", "\\*expected\\s*(behavior|behaviour)?\\*", "should\\s+be:", "desired:", "correct\\s+behavior:", "fix:", "solution:", "h2\\.\\s*acceptance\\s+criteria", "h3\\.\\s*acceptance\\s+criteria", "acceptance\\s+criteria:", "\\*\\*acceptance\\s+criteria\\*\\*" ] }
+ "current": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "current(ly)?(\\s+behavior|\\s+behaviour)?:", "h[1-4]\\.\\s*(current|actual)(\\s*/\\s*(actual|current))?\\s*(behavior|behaviour|result)", "\\*\\*current\\s*(behavior|behaviour)\\*\\*", "\\*current(\\s*/\\s*actual)?\\s*(behavior|behaviour)?\\*", "\\*actual\\s*(behavior|behaviour)?\\*", "actual(\\s+behavior|\\s+behaviour)?:", "what\\s+happens:", "observed:", "bug:", "issue:", "problem:" ] },
+ "expected": { "type": "patternMatchCount", "source": { "paths": [ "$.title", "$.description" ] }, "patterns": [ "expected(\\s+behavior|\\s+behaviour)?:", "h[1-4]\\.\\s*(expected|desired)\\s*(behavior|behaviour|result|outcome)", "\\*\\*expected\\s*(behavior|behaviour)\\*\\*", "\\*expected\\s*(behavior|behaviour)?\\*", "should\\s+be:", "desired:", "correct\\s+behavior:", "fix:", "solution:", "h2\\.\\s*acceptance\\s+criteria", "h3\\.\\s*acceptance\\s+criteria", "acceptance\\s+criteria:", "\\*\\*acceptance\\s+criteria\\*\\*" ] }
},
"tiers": [
{ "when": { "type": "all", "conditions": [ { "type": "ref", "name": "isInvestigation" }, { "type": "measure", "measure": "context", "atLeast": 2 } ] }, "score": 100, "finding": "✓ Investigation Context: Well documented" },