Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ All notable changes to this project are documented here. The format follows
[Semantic Versioning](https://semver.org/). Versions are `major.minor.height`, where the height comes from
Nerdbank.GitVersioning.

## [1.2] - Unreleased

Adds two Magic Suite ticket checks to the built-in rubric and a justified "not needed" option for documentation and
certifications. Everything is additive; 1.1 rubrics load and score as before, though a ticket's score can move where
the new checks apply.

### Added

- `NormalizedIssue.ReleaseNotes` (Jira `customfield_10600`, plain text or ADF) and `NormalizedIssue.Resolution`
(`fields.resolution.name`), read from the `releaseNotes` and `resolution` JSON keys.
- Built-in ticket-quality rubric: MS-only `ReleaseNotes` assessment (and `EpicReleaseNotes` in Epic Quality), weight 6,
mandatory cap 0.79. It checks that the notes are present, not a placeholder or generic, at least 8 words, different
from the summary, customer-facing (no ticket keys other than standards ids, file paths or code identifiers), free of
em dashes and in British spelling. It does not apply to other projects or to undelivered resolutions.
- Built-in ticket-quality rubric: MS-only `TicketContract` assessment (not for Epics), weight 6, mandatory cap 0.79.
Acceptance criteria must each be classified MUST, SHOULD or MAY, with at least one MUST. A criterion is classified
when it starts with a keyword, contains exactly one distinct keyword (the RFC 2119 sentence form), or sits under a
label line such as `*MUST*` or `MUST:`. Only capitals count. From Ready for Test onwards
the ticket needs an `h2. Verification Evidence` section (optionally marked "(Retrospective)") with a criterion table,
a Reachability block, a Not covered block and a Suggested QA route of A to D.
- New condition type `fieldsDiffer` and new measure `criteriaClassification`, for rubrics that need them.

### Changed

- Documentation and Certifications accept a justified `No documentation change: <reason>` or
`No certification question: <reason>` line, for all projects.

## [1.1] - Unreleased

Adds rubrics: whole configurations written as JSON, with a built-in Panoramic Data ticket-quality rubric. Everything
Expand Down
37 changes: 37 additions & 0 deletions PanoramicData.IssueAssessment.Tests/Mapping/MapperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,43 @@ public void Jira_V3Description_ListItemParagraphsAndAttributelessNodes_Flatten()
issue.Description.Should().Be("* first\nsecond\nend");
}

[Fact]
public void FromRestJson_ReadsReleaseNotesFromCustomField10600()
{
JiraIssueMapper.FromRestJson(Json("""{ "fields": { "customfield_10600": "Fixed the export." } }"""))
.ReleaseNotes.Should().Be("Fixed the export.");

JiraIssueMapper.FromRestJson(Json("""
{ "fields": { "customfield_10600": { "type": "doc", "version": 1, "content": [
{ "type": "paragraph", "content": [ { "type": "text", "text": "Fixed the export." } ] } ] } } }
""")).ReleaseNotes.Should().Be("Fixed the export.");

JiraIssueMapper.FromRestJson(Json("""{ "fields": {} }""")).ReleaseNotes.Should().BeNull();
}

[Theory]
[InlineData("Won't Do")]
[InlineData("Fixed")]
public void FromRestJson_ReadsResolutionName(string resolution)
=> JiraIssueMapper.FromRestJson(Json(JsonSerializer.Serialize(new { fields = new { resolution = new { name = resolution } } })))
.Resolution.Should().Be(resolution);

[Fact]
public void FromRestJson_NullResolution_IsNull()
=> JiraIssueMapper.FromRestJson(Json("""{ "fields": { "resolution": null } }""")).Resolution.Should().BeNull();

[Fact]
public void ToJsonObject_OmitsNullReleaseNotesAndResolution()
{
var empty = new NormalizedIssue().ToJsonObject();
empty.ContainsKey("releaseNotes").Should().BeFalse();
empty.ContainsKey("resolution").Should().BeFalse();

var set = new NormalizedIssue { ReleaseNotes = "n", Resolution = "Fixed" }.ToJsonObject();
set["releaseNotes"]!.GetValue<string>().Should().Be("n");
set["resolution"]!.GetValue<string>().Should().Be("Fixed");
}

[Fact]
public void Jira_NoDescription_IsNull()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Rubrics\yoda-fixtures.jsonl" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Rubrics\ticket-quality-1.1.6.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace PanoramicData.IssueAssessment.Tests.Rubrics;
/// </remarks>
public class BuiltInRubricParityTests : TestBase
{
// Yoda has no Presentation factor, so parity is proved over the rubric without it; PresentationTests covers that one.
// Yoda has no Presentation, Release Notes or Ticket Contract factor, so parity is proved over the rubric without them; their own test classes cover those.
private static readonly IssueAssessor Assessor = new(Serialization.AssessmentRubric.Load(WithoutPresentation(BuiltInRubrics.PanoramicDataTicketQualityJson)));

public static TheoryData<int> Cases => [.. Enumerable.Range(0, File.ReadAllLines(FixturePath).Length)];
Expand Down Expand Up @@ -69,12 +69,18 @@ private static string WithoutPresentation(string rubricJson)
foreach (JsonNode? category in rubric["categories"]!.AsArray())
{
JsonArray assessments = category!["assessments"]!.AsArray();
foreach (JsonNode? assessment in assessments.Where(item => item!["name"]!.GetValue<string>().EndsWith("Presentation", StringComparison.Ordinal)).ToList())
foreach (JsonNode? assessment in assessments.Where(item => item!["name"]!.GetValue<string>().EndsWith("Presentation", StringComparison.Ordinal) || item["name"]!.GetValue<string>().EndsWith("ReleaseNotes", StringComparison.Ordinal) || item["name"]!.GetValue<string>() == "TicketContract").ToList())
{
assessments.Remove(assessment);
}
}

JsonArray caps = rubric["scoreCaps"]!.AsArray();
foreach (JsonNode? cap in caps.Where(item => item!["name"]!.GetValue<string>() is "Release notes" or "Ticket contract").ToList())
{
caps.Remove(cap);
}

return rubric.ToJsonString();
}

Expand Down
70 changes: 70 additions & 0 deletions PanoramicData.IssueAssessment.Tests/Rubrics/NotNeededTierTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using PanoramicData.IssueAssessment.Mapping;
using PanoramicData.IssueAssessment.Rubrics;

namespace PanoramicData.IssueAssessment.Tests.Rubrics;

public class NotNeededTierTests : TestBase
{
private static readonly IssueAssessor Assessor = new(BuiltInRubrics.PanoramicDataTicketQuality);

[Fact]
public async Task NoDocumentationChangeWithReason_Scores100()
{
AssessmentReportItem item = await Item("Documentation", "No documentation change: internal refactor with no user-visible behaviour", "Improvement", "MS");
item.RawScore.Should().Be(100);
item.Finding.Should().Be("✓ Documentation: Not needed (reason given)");
}

[Fact]
public async Task NoDocumentationChangeBare_Scores0()
=> (await Item("Documentation", "No documentation change.", "Improvement", "MS")).Finding.Should().NotContain("Not needed");

[Fact]
public async Task NoDocumentationChangeNA_Scores0()
=> (await Item("Documentation", "No documentation change: N/A", "Improvement", "MS")).Finding.Should().NotContain("Not needed");

[Fact]
public async Task NoCertificationQuestionWithReason_Scores100()
{
AssessmentReportItem item = await Item("Certifications", "No certification question: internal refactor with no user-visible behaviour", "Improvement", "MS");
item.RawScore.Should().Be(100);
item.Finding.Should().Be("✓ Certifications: Not needed (reason given)");
}

[Fact]
public async Task NoCertificationQuestionBare_Scores0()
=> (await Item("Certifications", "No certification question.", "Improvement", "MS")).Finding.Should().NotContain("Not needed");

[Fact]
public async Task NoCertificationQuestionNA_Scores0()
=> (await Item("Certifications", "No certification question: N/A", "Improvement", "MS")).Finding.Should().NotContain("Not needed");

[Fact]
public async Task AppliesToOpsToo()
{
(await Item("Documentation", "No documentation change: internal refactor with no user-visible behaviour", "Improvement", "OPS")).RawScore.Should().Be(100);
(await Item("Certifications", "No certification question: internal refactor with no user-visible behaviour", "Improvement", "OPS")).RawScore.Should().Be(100);
}

[Fact]
public async Task BugCertifications_StillNotApplicable()
=> (await Item("Certifications", "No certification question: internal refactor with no user-visible behaviour", "Bug", "MS")).Finding.Should().Be("○ Certifications: Not applicable for bugs");

private static async Task<AssessmentReportItem> Item(string name, string description, string issueType, string project)
{
NormalizedIssue issue = new()
{
Key = project + "-1",
Project = project,
Title = "Tidy the schedule code",
Description = description,
IssueType = issueType,
Status = "In Progress",
Platform = "Jira",
Labels = [],
};
using JsonDocument document = issue.ToJsonDocument();
AssessmentReport report = AssessmentReport.From(await Assessor.AssessAsync(document, CancellationToken));
return report.Categories.SelectMany(category => category.Assessments).Single(item => item.Name == name);
}
}
Loading
Loading