From b70a6534c04c8852cad73c2a7586cfc1714c5a46 Mon Sep 17 00:00:00 2001 From: ElizabethWhyman Date: Wed, 28 Oct 2026 13:11:01 +0000 Subject: [PATCH] fix: Release Notes check and its cap apply from Ready for Test onwards Before Ready for Test an empty Release Notes field scores 100 with 'Not required yet (status: ...)', as Fix Version does, and the Release notes mandatory cap no longer applies, so a ticket in Ready for Progress can reach EXCELLENT. Notes written early are still checked but do not cap. Reported in panoramicdata/PanoramicData.Skills#1910. --- CHANGELOG.md | 4 ++ .../Rubrics/ReleaseNotesAssessmentTests.cs | 43 ++++++++++++++++--- .../Rubrics/panoramicdata-ticket-quality.json | 10 ++++- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34612fa..9e168e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,10 @@ the new checks apply. ### Fixed +- ReleaseNotes and EpicReleaseNotes apply from Ready for Test onwards, the status at which Release Notes are set. Before + then an empty field scores 100 with "○ Release Notes: Not required yet (status: ...)", as Fix Version does, and the + "Release notes" mandatory cap does not apply, so a ticket in Ready for Progress can reach EXCELLENT. Notes already + written before Ready for Test are still checked and reported, but do not cap the score. - TicketContract: a Verification Evidence table headed Criterion | Evidence (not only Criterion | Test) is evidence, not restated criteria, so it no longer triggers the duplication cap; a bold sub-heading inside the evidence section (such as *MUST criteria*) is not counted as a criteria heading; and an acceptance-criteria item struck through in full (-text-, optionally followed by a note or moved to KEY) is no longer counted as a criterion of the ticket. ### Changed diff --git a/PanoramicData.IssueAssessment.Tests/Rubrics/ReleaseNotesAssessmentTests.cs b/PanoramicData.IssueAssessment.Tests/Rubrics/ReleaseNotesAssessmentTests.cs index 1ab9d20..6141274 100644 --- a/PanoramicData.IssueAssessment.Tests/Rubrics/ReleaseNotesAssessmentTests.cs +++ b/PanoramicData.IssueAssessment.Tests/Rubrics/ReleaseNotesAssessmentTests.cs @@ -4,7 +4,7 @@ namespace PanoramicData.IssueAssessment.Tests.Rubrics; /// -/// The MS-only ReleaseNotes assessment: customer-facing release notes are mandatory, so a failing tier caps the score. +/// The MS-only ReleaseNotes assessment: customer-facing release notes are mandatory from Ready for Test, so a failing tier caps the score from then on. /// public class ReleaseNotesAssessmentTests : TestBase { @@ -212,10 +212,43 @@ public async Task ResolutionFixed_IsAssessed(string resolution) report.Caps.Select(cap => cap.Name).Should().Contain(CapName); } + [Theory] + [InlineData("Improvement", "Ready for Progress")] + [InlineData("Improvement", "In Progress")] + [InlineData("Epic", "In Progress")] + public async Task EmptyBeforeReadyForTest_NotRequiredYet_NoCap(string issueType, string status) + { + AssessmentReport report = await AssessAsync(notes: null, issueType: issueType, status: status); + + NotesItem(report).RawScore.Should().Be(100); + NotesItem(report).Finding.Should().Be($"○ Release Notes: Not required yet (status: {status})"); + report.Caps.Select(cap => cap.Name).Should().NotContain(CapName); + } + + [Fact] + public async Task FlawedBeforeReadyForTest_StillReported_NoCap() + { + AssessmentReport report = await AssessAsync("TBD", status: "Ready for Progress"); + + NotesItem(report).Finding.Should().Be("✗ Release Notes: Placeholder"); + report.Caps.Select(cap => cap.Name).Should().NotContain(CapName); + } + + [Theory] + [InlineData("In Test")] + [InlineData("Closed")] + public async Task EmptyFromReadyForTestOn_Caps(string status) + { + AssessmentReport report = await AssessAsync(notes: null, status: status); + + NotesItem(report).Finding.Should().Be("✗ Release Notes: Empty"); + report.Caps.Select(cap => cap.Name).Should().Contain(CapName); + } + private static AssessmentReportItem NotesItem(AssessmentReport report) => report.Categories.SelectMany(category => category.Assessments).Single(item => item.Name.EndsWith("ReleaseNotes", StringComparison.Ordinal) && item.Outcome != AssessmentOutcome.NotApplicable); - private static NormalizedIssue Issue(string? notes, string title = "Keep schedule run times across timezone changes", string issueType = "Improvement", string? resolution = null, ImmutableList? labels = null) + private static NormalizedIssue Issue(string? notes, string title = "Keep schedule run times across timezone changes", string issueType = "Improvement", string? resolution = null, ImmutableList? labels = null, string status = "Ready for Test") => new() { Key = "MS-1", @@ -223,7 +256,7 @@ private static NormalizedIssue Issue(string? notes, string title = "Keep schedul Title = title, Description = Description, IssueType = issueType, - Status = "In Progress", + Status = status, Platform = "Jira", ReleaseNotes = notes, Resolution = resolution, @@ -236,6 +269,6 @@ private static async Task ReportAsync(IssueAssessor assessor, return AssessmentReport.From(await assessor.AssessAsync(document, CancellationToken)); } - private static Task AssessAsync(string? notes, string title = "Keep schedule run times across timezone changes", string issueType = "Improvement", string? resolution = null, ImmutableList? labels = null) - => ReportAsync(Assessor, Issue(notes, title, issueType, resolution, labels)); + private static Task AssessAsync(string? notes, string title = "Keep schedule run times across timezone changes", string issueType = "Improvement", string? resolution = null, ImmutableList? labels = null, string status = "Ready for Test") + => ReportAsync(Assessor, Issue(notes, title, issueType, resolution, labels, status)); } diff --git a/PanoramicData.IssueAssessment/Rubrics/panoramicdata-ticket-quality.json b/PanoramicData.IssueAssessment/Rubrics/panoramicdata-ticket-quality.json index 40c40b8..7b11847 100644 --- a/PanoramicData.IssueAssessment/Rubrics/panoramicdata-ticket-quality.json +++ b/PanoramicData.IssueAssessment/Rubrics/panoramicdata-ticket-quality.json @@ -14,6 +14,7 @@ "conditions": [ { "type": "fieldPresent", "path": "$.resolution" }, { "type": "not", "condition": { "type": "fieldEquals", "path": "$.resolution", "values": [ "Fixed", "Done" ] } } ] }, "contractEvidenceDue": { "type": "fieldEquals", "path": "$.status", "values": [ "Ready for Test", "In Test", "Test Blocked", "Ready for Customer", "With Customer", "Ready for Release", "Closed", "Done", "Resolved" ] }, + "releaseNotesDue": { "type": "ref", "name": "contractEvidenceDue" }, "isInvestigation": { "type": "all", "conditions": [ @@ -382,6 +383,7 @@ "definition": { "type": "tiered", "tiers": [ + { "when": { "type": "all", "conditions": [ { "type": "not", "condition": { "type": "ref", "name": "releaseNotesDue" } }, { "type": "not", "condition": { "type": "fieldPresent", "path": "$.releaseNotes" } } ] }, "score": 100, "finding": "○ Release Notes: Not required yet (status: {$.status})" }, { "when": { "type": "not", "condition": { "type": "fieldPresent", "path": "$.releaseNotes" } }, "score": 0, "finding": "✗ Release Notes: Empty", "recommendation": "Fill in the Release Notes field with one or two customer-facing sentences of at least 8 words saying what changed for the customer" }, { "when": { "type": "any", "conditions": [ { "type": "placeholder", "source": { "paths": [ "$.releaseNotes" ] } }, { "type": "fieldMatches", "path": "$.releaseNotes", "patterns": [ "(?i)^\\s*(see (the )?(ticket|description)|none|-+)\\s*\\.?\\s*$" ] } ] }, "score": 0, "finding": "✗ Release Notes: Placeholder", "recommendation": "Replace the placeholder in the Release Notes field with one or two sentences saying what changed for the customer" }, { "when": { "type": "fieldMatches", "path": "$.releaseNotes", "patterns": [ "(?i)^\\s*(fixed (a )?bugs?|bug ?fix(es)?|various (fixes|improvements)|minor (fixes|improvements))\\s*\\.?\\s*$" ] }, "score": 0, "finding": "✗ Release Notes: Generic", "recommendation": "Say what changed and for whom: a generic note such as 'Fixed bug' tells the customer nothing" }, @@ -608,6 +610,7 @@ "definition": { "type": "tiered", "tiers": [ + { "when": { "type": "all", "conditions": [ { "type": "not", "condition": { "type": "ref", "name": "releaseNotesDue" } }, { "type": "not", "condition": { "type": "fieldPresent", "path": "$.releaseNotes" } } ] }, "score": 100, "finding": "○ Release Notes: Not required yet (status: {$.status})" }, { "when": { "type": "not", "condition": { "type": "fieldPresent", "path": "$.releaseNotes" } }, "score": 0, "finding": "✗ Release Notes: Empty", "recommendation": "Fill in the Release Notes field with one or two customer-facing sentences of at least 8 words saying what changed for the customer" }, { "when": { "type": "any", "conditions": [ { "type": "placeholder", "source": { "paths": [ "$.releaseNotes" ] } }, { "type": "fieldMatches", "path": "$.releaseNotes", "patterns": [ "(?i)^\\s*(see (the )?(ticket|description)|none|-+)\\s*\\.?\\s*$" ] } ] }, "score": 0, "finding": "✗ Release Notes: Placeholder", "recommendation": "Replace the placeholder in the Release Notes field with one or two sentences saying what changed for the customer" }, { "when": { "type": "fieldMatches", "path": "$.releaseNotes", "patterns": [ "(?i)^\\s*(fixed (a )?bugs?|bug ?fix(es)?|various (fixes|improvements)|minor (fixes|improvements))\\s*\\.?\\s*$" ] }, "score": 0, "finding": "✗ Release Notes: Generic", "recommendation": "Say what changed and for whom: a generic note such as 'Fixed bug' tells the customer nothing" }, @@ -663,8 +666,11 @@ "group": "mandatory", "cap": 0.79, "when": { - "type": "any", - "conditions": [ { "type": "assessmentScore", "assessment": "ReleaseNotes", "lessThan": 100 }, { "type": "assessmentScore", "assessment": "EpicReleaseNotes", "lessThan": 100 } ] + "type": "all", + "conditions": [ + { "type": "ref", "name": "releaseNotesDue" }, + { "type": "any", "conditions": [ { "type": "assessmentScore", "assessment": "ReleaseNotes", "lessThan": 100 }, { "type": "assessmentScore", "assessment": "EpicReleaseNotes", "lessThan": 100 } ] } + ] } }, {