diff --git a/.reviewmark.yaml b/.reviewmark.yaml
index c02eba2..a17a717 100644
--- a/.reviewmark.yaml
+++ b/.reviewmark.yaml
@@ -11,6 +11,7 @@ needs-review:
- "**/*.cs" # All C# source and test files
- "docs/reqstream/**/*.yaml" # Requirements files
- "docs/design/**/*.md" # Design documentation files
+ - "docs/verification/**/*.md" # Verification documentation files
- "docs/user_guide/**/*.md" # User guide documentation
- "!**/obj/**" # Exclude build output
- "!**/bin/**" # Exclude build output
@@ -45,6 +46,7 @@ reviews:
- "docs/reqstream/build-mark/build-mark.yaml"
- "docs/design/introduction.md"
- "docs/design/build-mark/build-mark.md"
+ - "docs/verification/build-mark/build-mark.md"
- "test/**/IntegrationTests.cs"
- "test/**/Runner.cs"
- "test/**/AssemblyInfo.cs"
@@ -56,6 +58,7 @@ reviews:
- "docs/reqstream/build-mark/platform-requirements.yaml"
- "docs/design/introduction.md"
- "docs/design/build-mark/**/*.md"
+ - "docs/verification/introduction.md"
- id: BuildMark-AllRequirements
title: Review of All BuildMark requirements quality and traceability
@@ -70,6 +73,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/program.yaml"
- "docs/design/build-mark/program.md"
+ - "docs/verification/build-mark/program.md"
- "src/**/Program.cs"
- "test/**/ProgramTests.cs"
@@ -79,6 +83,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/cli/cli.yaml"
- "docs/design/build-mark/cli/cli.md"
+ - "docs/verification/build-mark/cli/cli.md"
- "test/**/Cli/CliTests.cs"
- id: BuildMark-Cli-Context
@@ -86,6 +91,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/cli/context.yaml"
- "docs/design/build-mark/cli/context.md"
+ - "docs/verification/build-mark/cli/context.md"
- "src/**/Cli/Context.cs"
- "test/**/Cli/ContextTests.cs"
@@ -95,6 +101,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/self-test/self-test.yaml"
- "docs/design/build-mark/self-test/self-test.md"
+ - "docs/verification/build-mark/self-test/self-test.md"
- "test/**/SelfTest/SelfTestTests.cs"
- id: BuildMark-SelfTest-Validation
@@ -102,6 +109,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/self-test/validation.yaml"
- "docs/design/build-mark/self-test/validation.md"
+ - "docs/verification/build-mark/self-test/validation.md"
- "src/**/SelfTest/Validation.cs"
- "test/**/SelfTest/ValidationTests.cs"
@@ -111,6 +119,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/utilities/utilities.yaml"
- "docs/design/build-mark/utilities/utilities.md"
+ - "docs/verification/build-mark/utilities/utilities.md"
- "test/**/Utilities/UtilitiesTests.cs"
- id: BuildMark-Utilities-PathHelpers
@@ -118,6 +127,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/utilities/path-helpers.yaml"
- "docs/design/build-mark/utilities/path-helpers.md"
+ - "docs/verification/build-mark/utilities/path-helpers.md"
- "src/**/Utilities/PathHelpers.cs"
- "test/**/Utilities/PathHelpersTests.cs"
@@ -126,6 +136,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/utilities/process-runner.yaml"
- "docs/design/build-mark/utilities/process-runner.md"
+ - "docs/verification/build-mark/utilities/process-runner.md"
- "src/**/Utilities/ProcessRunner.cs"
- "test/**/Utilities/ProcessRunnerTests.cs"
@@ -135,6 +146,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/version/version.yaml"
- "docs/design/build-mark/version/version.md"
+ - "docs/verification/build-mark/version/version.md"
- "test/**/Version/VersionTests.cs"
- id: BuildMark-Version-VersionComparable
@@ -142,6 +154,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/version/version-comparable.yaml"
- "docs/design/build-mark/version/version-comparable.md"
+ - "docs/verification/build-mark/version/version-comparable.md"
- "src/**/Version/VersionComparable.cs"
- "test/**/Version/VersionComparableTests.cs"
@@ -150,6 +163,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/version/version-semantic.yaml"
- "docs/design/build-mark/version/version-semantic.md"
+ - "docs/verification/build-mark/version/version-semantic.md"
- "src/**/Version/VersionSemantic.cs"
- "test/**/Version/VersionSemanticTests.cs"
@@ -158,6 +172,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/version/version-tag.yaml"
- "docs/design/build-mark/version/version-tag.md"
+ - "docs/verification/build-mark/version/version-tag.md"
- "src/**/Version/VersionTag.cs"
- "test/**/Version/VersionTagTests.cs"
@@ -166,6 +181,8 @@ reviews:
paths:
- "docs/reqstream/build-mark/version/version-interval.yaml"
- "docs/design/build-mark/version/version-interval.md"
+ - "docs/verification/build-mark/version/version-interval.md"
+ - "docs/verification/build-mark/version/version-interval-set.md"
- "src/**/Version/VersionInterval.cs"
- "src/**/Version/VersionIntervalSet.cs"
- "test/**/Version/VersionIntervalTests.cs"
@@ -176,6 +193,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/version/version-commit-tag.yaml"
- "docs/design/build-mark/version/version-commit-tag.md"
+ - "docs/verification/build-mark/version/version-commit-tag.md"
- "src/**/Version/VersionCommitTag.cs"
- "test/**/BuildNotes/BuildInformationTests.cs"
@@ -185,6 +203,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/build-notes/build-notes.yaml"
- "docs/design/build-mark/build-notes/build-notes.md"
+ - "docs/verification/build-mark/build-notes/build-notes.md"
- "test/**/BuildNotes/BuildNotesTests.cs"
- id: BuildMark-BuildNotes-BuildInformation
@@ -192,6 +211,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/build-notes/build-information.yaml"
- "docs/design/build-mark/build-notes/build-information.md"
+ - "docs/verification/build-mark/build-notes/build-information.md"
- "src/**/BuildNotes/BuildInformation.cs"
- "test/**/BuildNotes/BuildInformationTests.cs"
@@ -200,6 +220,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/build-notes/item-info.yaml"
- "docs/design/build-mark/build-notes/item-info.md"
+ - "docs/verification/build-mark/build-notes/item-info.md"
- "src/**/BuildNotes/ItemInfo.cs"
- id: BuildMark-BuildNotes-WebLink
@@ -207,6 +228,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/build-notes/web-link.yaml"
- "docs/design/build-mark/build-notes/web-link.md"
+ - "docs/verification/build-mark/build-notes/web-link.md"
- "src/**/BuildNotes/WebLink.cs"
- "test/**/BuildNotes/BuildInformationTests.cs"
@@ -216,6 +238,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/configuration/configuration.yaml"
- "docs/design/build-mark/configuration/configuration.md"
+ - "docs/verification/build-mark/configuration/configuration.md"
- "test/**/Configuration/ConfigurationSubsystemTests.cs"
- id: BuildMark-Configuration-BuildMarkConfigReader
@@ -223,6 +246,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/configuration/configuration.yaml"
- "docs/design/build-mark/configuration/configuration.md"
+ - "docs/verification/build-mark/configuration/build-mark-config-reader.md"
- "src/**/Configuration/BuildMarkConfigReader.cs"
- "test/**/Configuration/ConfigurationTests.cs"
@@ -231,6 +255,8 @@ reviews:
paths:
- "docs/reqstream/build-mark/configuration/configuration.yaml"
- "docs/design/build-mark/configuration/configuration.md"
+ - "docs/verification/build-mark/configuration/configuration-load-result.md"
+ - "docs/verification/build-mark/configuration/configuration-issue.md"
- "src/**/Configuration/ConfigurationLoadResult.cs"
- "src/**/Configuration/ConfigurationIssue.cs"
- "test/**/Configuration/ConfigurationTests.cs"
@@ -240,6 +266,14 @@ reviews:
paths:
- "docs/reqstream/build-mark/configuration/configuration.yaml"
- "docs/design/build-mark/configuration/configuration.md"
+ - "docs/verification/build-mark/configuration/build-mark-config.md"
+ - "docs/verification/build-mark/configuration/connector-config.md"
+ - "docs/verification/build-mark/configuration/git-hub-connector-config.md"
+ - "docs/verification/build-mark/configuration/azure-dev-ops-connector-config.md"
+ - "docs/verification/build-mark/configuration/report-config.md"
+ - "docs/verification/build-mark/configuration/section-config.md"
+ - "docs/verification/build-mark/configuration/rule-config.md"
+ - "docs/verification/build-mark/configuration/rule-match-config.md"
- "src/**/Configuration/BuildMarkConfig.cs"
- "src/**/Configuration/ConnectorConfig.cs"
- "src/**/Configuration/GitHubConnectorConfig.cs"
@@ -255,6 +289,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/repo-connectors/repo-connectors.yaml"
- "docs/design/build-mark/repo-connectors/repo-connectors.md"
+ - "docs/verification/build-mark/repo-connectors/repo-connectors.md"
- "test/**/RepoConnectors/RepoConnectorsTests.cs"
- id: BuildMark-RepoConnectors-RepoConnectorBase
@@ -262,6 +297,8 @@ reviews:
paths:
- "docs/reqstream/build-mark/repo-connectors/repo-connector-base.yaml"
- "docs/design/build-mark/repo-connectors/repo-connector-base.md"
+ - "docs/verification/build-mark/repo-connectors/repo-connector-base.md"
+ - "docs/verification/build-mark/repo-connectors/i-repo-connector.md"
- "src/**/RepoConnectors/IRepoConnector.cs"
- "src/**/RepoConnectors/RepoConnectorBase.cs"
- "test/**/RepoConnectors/RepoConnectorBaseTests.cs"
@@ -271,6 +308,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/repo-connectors/repo-connector-factory.yaml"
- "docs/design/build-mark/repo-connectors/repo-connector-factory.md"
+ - "docs/verification/build-mark/repo-connectors/repo-connector-factory.md"
- "src/**/RepoConnectors/RepoConnectorFactory.cs"
- "test/**/RepoConnectors/RepoConnectorFactoryTests.cs"
@@ -280,6 +318,8 @@ reviews:
- "docs/reqstream/build-mark/repo-connectors/item-controls-parser.yaml"
- "docs/design/build-mark/repo-connectors/item-controls-info.md"
- "docs/design/build-mark/repo-connectors/item-controls-parser.md"
+ - "docs/verification/build-mark/repo-connectors/item-controls-parser.md"
+ - "docs/verification/build-mark/repo-connectors/item-controls-info.md"
- "src/**/RepoConnectors/ItemControlsInfo.cs"
- "src/**/RepoConnectors/ItemControlsParser.cs"
- "test/**/ItemControls/ItemControlsParserTests.cs"
@@ -290,6 +330,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/repo-connectors/item-router.yaml"
- "docs/design/build-mark/repo-connectors/item-router.md"
+ - "docs/verification/build-mark/repo-connectors/item-router.md"
- "src/**/RepoConnectors/ItemRouter.cs"
- "test/**/RepoConnectors/ItemRouterTests.cs"
@@ -298,6 +339,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/repo-connectors/mock/mock.yaml"
- "docs/design/build-mark/repo-connectors/mock/mock.md"
+ - "docs/verification/build-mark/repo-connectors/mock/mock.md"
- "test/**/RepoConnectors/Mock/MockTests.cs"
- id: BuildMark-RepoConnectors-Mock-MockRepoConnector
@@ -305,6 +347,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/repo-connectors/mock/mock-repo-connector.yaml"
- "docs/design/build-mark/repo-connectors/mock/mock-repo-connector.md"
+ - "docs/verification/build-mark/repo-connectors/mock/mock-repo-connector.md"
- "src/**/RepoConnectors/Mock/MockRepoConnector.cs"
- "test/**/RepoConnectors/Mock/MockRepoConnectorTests.cs"
@@ -313,6 +356,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/repo-connectors/github/github.yaml"
- "docs/design/build-mark/repo-connectors/github/github.md"
+ - "docs/verification/build-mark/repo-connectors/github/github.md"
- "test/**/RepoConnectors/GitHub/GitHubTests.cs"
- id: BuildMark-RepoConnectors-GitHub-GitHubGraphQLClient
@@ -321,6 +365,8 @@ reviews:
- "docs/reqstream/build-mark/repo-connectors/github/github-graphql-client.yaml"
- "docs/design/build-mark/repo-connectors/github/github-graphql-client.md"
- "docs/design/build-mark/repo-connectors/github/github-graphql-types.md"
+ - "docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-client.md"
+ - "docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-types.md"
- "src/**/RepoConnectors/GitHub/GitHubGraphQLClient.cs"
- "src/**/RepoConnectors/GitHub/GitHubGraphQLTypes.cs"
- "test/**/RepoConnectors/GitHub/GitHubGraphQLClientFindIssueIdsTests.cs"
@@ -337,6 +383,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/repo-connectors/github/github-repo-connector.yaml"
- "docs/design/build-mark/repo-connectors/github/github-repo-connector.md"
+ - "docs/verification/build-mark/repo-connectors/github/git-hub-repo-connector.md"
- "src/**/RepoConnectors/GitHub/GitHubRepoConnector.cs"
- "test/**/RepoConnectors/GitHub/GitHubRepoConnectorTests.cs"
- "test/**/RepoConnectors/GitHub/MockableGitHubRepoConnector.cs"
@@ -346,6 +393,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops.yaml"
- "docs/design/build-mark/repo-connectors/azure-devops/azure-devops.md"
+ - "docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops.md"
- "test/**/RepoConnectors/AzureDevOps/AzureDevOpsTests.cs"
- id: BuildMark-RepoConnectors-AzureDevOps-AzureDevOpsRestClient
@@ -354,6 +402,8 @@ reviews:
- "docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.yaml"
- "docs/design/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.md"
- "docs/design/build-mark/repo-connectors/azure-devops/azure-devops-api-types.md"
+ - "docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-rest-client.md"
+ - "docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-api-types.md"
- "src/**/RepoConnectors/AzureDevOps/AzureDevOpsRestClient.cs"
- "src/**/RepoConnectors/AzureDevOps/AzureDevOpsApiTypes.cs"
- "test/**/RepoConnectors/AzureDevOps/MockAzureDevOpsHttpMessageHandler.cs"
@@ -364,6 +414,7 @@ reviews:
paths:
- "docs/reqstream/build-mark/repo-connectors/azure-devops/work-item-mapper.yaml"
- "docs/design/build-mark/repo-connectors/azure-devops/work-item-mapper.md"
+ - "docs/verification/build-mark/repo-connectors/azure-dev-ops/work-item-mapper.md"
- "src/**/RepoConnectors/AzureDevOps/WorkItemMapper.cs"
- "test/**/RepoConnectors/AzureDevOps/WorkItemMapperTests.cs"
@@ -372,6 +423,68 @@ reviews:
paths:
- "docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.yaml"
- "docs/design/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md"
+ - "docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-repo-connector.md"
- "src/**/RepoConnectors/AzureDevOps/AzureDevOpsRepoConnector.cs"
- "test/**/RepoConnectors/AzureDevOps/AzureDevOpsRepoConnectorTests.cs"
- "test/**/RepoConnectors/AzureDevOps/MockableAzureDevOpsRepoConnector.cs"
+
+ # OTS Items
+ - id: OTS-BuildMark
+ title: Review of BuildMark OTS verification evidence
+ paths:
+ - "docs/reqstream/ots/buildmark.yaml"
+ - "docs/verification/ots/buildmark.md"
+
+ - id: OTS-FileAssert
+ title: Review of FileAssert OTS verification evidence
+ paths:
+ - "docs/reqstream/ots/fileassert.yaml"
+ - "docs/verification/ots/fileassert.md"
+
+ - id: OTS-Pandoc
+ title: Review of Pandoc OTS verification evidence
+ paths:
+ - "docs/reqstream/ots/pandoc.yaml"
+ - "docs/verification/ots/pandoc.md"
+
+ - id: OTS-ReqStream
+ title: Review of ReqStream OTS verification evidence
+ paths:
+ - "docs/reqstream/ots/reqstream.yaml"
+ - "docs/verification/ots/reqstream.md"
+
+ - id: OTS-ReviewMark
+ title: Review of ReviewMark OTS verification evidence
+ paths:
+ - "docs/reqstream/ots/reviewmark.yaml"
+ - "docs/verification/ots/reviewmark.md"
+
+ - id: OTS-SarifMark
+ title: Review of SarifMark OTS verification evidence
+ paths:
+ - "docs/reqstream/ots/sarifmark.yaml"
+ - "docs/verification/ots/sarifmark.md"
+
+ - id: OTS-SonarMark
+ title: Review of SonarMark OTS verification evidence
+ paths:
+ - "docs/reqstream/ots/sonarmark.yaml"
+ - "docs/verification/ots/sonarmark.md"
+
+ - id: OTS-VersionMark
+ title: Review of VersionMark OTS verification evidence
+ paths:
+ - "docs/reqstream/ots/versionmark.yaml"
+ - "docs/verification/ots/versionmark.md"
+
+ - id: OTS-WeasyPrint
+ title: Review of WeasyPrint OTS verification evidence
+ paths:
+ - "docs/reqstream/ots/weasyprint.yaml"
+ - "docs/verification/ots/weasyprint.md"
+
+ - id: OTS-xUnit
+ title: Review of xUnit OTS verification evidence
+ paths:
+ - "docs/reqstream/ots/xunit.yaml"
+ - "docs/verification/ots/xunit.md"
diff --git a/docs/design/build-mark/program.md b/docs/design/build-mark/program.md
index 5451cc2..c3c0ead 100644
--- a/docs/design/build-mark/program.md
+++ b/docs/design/build-mark/program.md
@@ -43,7 +43,7 @@ method applies the following priority order:
2. Print the application banner (version and copyright).
3. If `context.Help` is set, print the usage message and return.
4. If `context.Validate` is set, delegate to `Validation.Run(context)` and return.
-5. If `context.Lint` is set, call `BuildMarkConfigReader.ReadAsync`, call
+5. If `context.Lint` is set, call `LoadConfiguration()`, call
`result.ReportTo(context)`, and return.
6. Otherwise, call `ProcessBuildNotes(context)` to generate the build report.
diff --git a/docs/design/build-mark/version/version.md b/docs/design/build-mark/version/version.md
index fa6afb8..b204c4b 100644
--- a/docs/design/build-mark/version/version.md
+++ b/docs/design/build-mark/version/version.md
@@ -22,12 +22,12 @@ The Version subsystem is composed of six units:
```text
VersionTag (raw repository tag)
↓ (extraction)
-VersionSemantic (parsed semantic version)
+VersionSemantic (parsed semantic version)
↓ (normalization)
VersionComparable (optimized comparison)
↓ (range operations)
VersionInterval / VersionIntervalSet (version ranges)
- ↓ (build association)
+ ↓ (build association)
VersionCommitTag (version + commit hash)
```
@@ -35,7 +35,7 @@ VersionCommitTag (version + commit hash)
### Semantic Versioning Compliance
-All version processing strictly adheres to [Semantic Versioning 2.0.0](https://semver.org/) specification
+All version processing strictly adheres to Semantic Versioning 2.0.0 () specification
to ensure predictable and industry-standard behavior.
### Performance Optimization
@@ -48,7 +48,7 @@ through construction-time parsing and cached segment arrays.
Each version type serves a specific purpose with clear boundaries:
- **VersionTag**: Raw string from repository
-- **VersionSemantic**: Validated SemVer structure
+- **VersionSemantic**: Validated SemVer structure
- **VersionComparable**: Optimized for comparison operations
- **VersionInterval**: Range queries and filtering
- **VersionCommitTag**: Build metadata association
@@ -69,7 +69,7 @@ Each version type serves a specific purpose with clear boundaries:
Version subsystem processes raw repository tags from GitHub and other sources,
extracting semantic versions for build boundary determination.
-### Build Notes
+### Build Notes
Provides VersionCommitTag associations that link semantic versions to specific commit hashes for build information generation.
diff --git a/docs/design/introduction.md b/docs/design/introduction.md
index f8a6000..b8009e9 100644
--- a/docs/design/introduction.md
+++ b/docs/design/introduction.md
@@ -45,7 +45,7 @@ BuildMark (System)
│ └── ProcessRunner (Unit)
├── Version (Subsystem)
│ ├── VersionComparable (Unit)
-│ ├── VersionSemantic (Unit)
+│ ├── VersionSemantic (Unit)
│ ├── VersionTag (Unit)
│ ├── VersionInterval (Unit)
│ ├── VersionIntervalSet (Unit)
@@ -156,6 +156,4 @@ Throughout this document:
## References
- See the BuildMark User Guide for user-facing documentation.
-- [BuildMark Repository][repo]
-
-[repo]: https://github.com/demaconsulting/BuildMark
+- See the BuildMark repository at .
diff --git a/docs/reqstream/build-mark/build-mark.yaml b/docs/reqstream/build-mark/build-mark.yaml
index 28fd32a..6b24105 100644
--- a/docs/reqstream/build-mark/build-mark.yaml
+++ b/docs/reqstream/build-mark/build-mark.yaml
@@ -365,6 +365,48 @@ sections:
- BuildMark-BuildNotes-ReportModel
- BuildMark-Program-Report
+ - id: BuildMark-Report-BaselinePreRelease
+ title: >-
+ The tool shall select the previous tag with a different commit hash as the
+ baseline when generating build notes for a pre-release version.
+ justification: |
+ When two pre-release tags point to the same commit, using the immediately
+ preceding tag as the baseline would produce an empty changelog. Selecting
+ the most recent predecessor with a different commit hash ensures a meaningful
+ changelog is always generated for pre-release builds.
+ tests:
+ - BuildMark_Report_BaselinePreRelease_SkipsSameCommitPredecessor
+ children:
+ - BuildMark-RepoConnectorBase-BaselinePreRelease
+
+ - id: BuildMark-Report-BaselineRelease
+ title: >-
+ The tool shall select the previous release tag as the baseline when
+ generating build notes for a release version, skipping all pre-release tags.
+ justification: |
+ Release notes should cover all changes since the last stable release, not
+ just changes since the most recent pre-release. Skipping pre-release
+ predecessors when selecting the baseline ensures completeness and correctness
+ of the generated release notes.
+ tests:
+ - BuildMark_Report_ShowsVersionRangeFromPreviousRelease
+ children:
+ - BuildMark-RepoConnectorBase-BaselineRelease
+
+ - id: BuildMark-Report-BaselineFirstRelease
+ title: >-
+ The tool shall generate build notes from the beginning of repository history
+ when no previous version tag qualifies as a baseline.
+ justification: |
+ When a project publishes its first release there is no predecessor tag.
+ Treating the absence of a qualifying baseline as "generate from the start
+ of history" ensures that the first release still receives meaningful, complete
+ build notes rather than an error or an empty report.
+ tests:
+ - BuildMark_Report_BaselineFirstRelease_GeneratesFromBeginningOfHistory
+ children:
+ - BuildMark-RepoConnectorBase-BaselineNoHistory
+
- id: BuildMark-Report-Hyperlinks
title: The tool shall include hyperlinks to issues and pull requests.
justification: |
diff --git a/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.yaml b/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.yaml
index 1bf013c..2ba73b5 100644
--- a/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.yaml
+++ b/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.yaml
@@ -10,9 +10,8 @@ sections:
requirements:
- id: BuildMark-AzureDevOps-RestClient
title: >-
- The AzureDevOpsRestClient shall retrieve repository details, commits, tags,
- pull requests, and work items from the Azure DevOps API, including support
- for paginated responses.
+ The AzureDevOpsRestClient shall retrieve repository data using the Azure DevOps
+ REST API, including support for paginated responses.
justification: |
The Azure DevOps REST API returns large result sets in pages. The client must
transparently follow pagination to retrieve complete repository data so that
diff --git a/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops.yaml b/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops.yaml
index 6ab7593..ca3d2ab 100644
--- a/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops.yaml
+++ b/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops.yaml
@@ -13,10 +13,8 @@ sections:
requirements:
- id: BuildMark-AzureDevOps-SubSystem
title: >-
- The AzureDevOps sub-subsystem shall provide a repository connector that implements
- the IRepoConnector interface, retrieves build information from Azure DevOps, supports
- URL parsing, item controls, configurable routing rules, REST API access, and work
- item mapping.
+ The AzureDevOps sub-subsystem shall implement the IRepoConnector interface
+ for Azure DevOps repositories.
justification: |
Generating build notes from Azure DevOps repositories requires a dedicated sub-subsystem
that can authenticate to the Azure DevOps REST API, retrieve commits, tags, pull
diff --git a/docs/reqstream/build-mark/repo-connectors/github/github-graphql-client.yaml b/docs/reqstream/build-mark/repo-connectors/github/github-graphql-client.yaml
index 5666515..c9620bc 100644
--- a/docs/reqstream/build-mark/repo-connectors/github/github-graphql-client.yaml
+++ b/docs/reqstream/build-mark/repo-connectors/github/github-graphql-client.yaml
@@ -11,8 +11,8 @@ sections:
requirements:
- id: BuildMark-GitHub-GraphQLClient
title: >-
- The GitHubGraphQLClient shall retrieve commits, releases, tags, pull requests,
- and issues from the GitHub repository, including support for paginated responses.
+ The GitHubGraphQLClient shall retrieve repository data using the GitHub GraphQL API,
+ including support for paginated responses.
justification: |
The GitHub API returns large result sets in pages. The client must transparently
follow pagination cursors to retrieve all commits, releases, tags, pull requests,
diff --git a/docs/reqstream/build-mark/repo-connectors/github/github.yaml b/docs/reqstream/build-mark/repo-connectors/github/github.yaml
index 9ca3ca1..a4efb5c 100644
--- a/docs/reqstream/build-mark/repo-connectors/github/github.yaml
+++ b/docs/reqstream/build-mark/repo-connectors/github/github.yaml
@@ -13,9 +13,8 @@ sections:
requirements:
- id: BuildMark-GitHub-SubSystem
title: >-
- The GitHub sub-subsystem shall provide a repository connector that implements
- the IRepoConnector interface, retrieves build information from GitHub, supports
- item controls, description body retrieval, and configurable routing rules.
+ The GitHub sub-subsystem shall implement the IRepoConnector interface
+ for GitHub repositories.
justification: |
Generating build notes from GitHub repositories requires a dedicated sub-subsystem
that can authenticate to the GitHub API, retrieve commits, releases, tags, pull
diff --git a/docs/reqstream/build-mark/repo-connectors/repo-connector-base.yaml b/docs/reqstream/build-mark/repo-connectors/repo-connector-base.yaml
index 8bc8f71..6036504 100644
--- a/docs/reqstream/build-mark/repo-connectors/repo-connector-base.yaml
+++ b/docs/reqstream/build-mark/repo-connectors/repo-connector-base.yaml
@@ -63,3 +63,45 @@ sections:
tests:
- RepoConnectorBase_FindVersionIndex_DifferentPrefixSameVersion_ReturnsCorrectIndex
- RepoConnectorBase_FindVersionIndex_VersionNotInList_ReturnsMinusOne
+
+ - id: BuildMark-RepoConnectorBase-BaselinePreRelease
+ title: >-
+ For a pre-release target version, RepoConnectorBase shall select the most
+ recent preceding tag with a different commit hash as the baseline, skipping
+ tags that share the same commit hash to avoid generating empty changelogs.
+ justification: |
+ When multiple pre-release tags are created from the same commit (e.g., a tag
+ is renamed or recreated), using such a tag as the baseline would produce an
+ empty changelog because the commit range would be empty. By skipping tags with
+ the same commit hash, BuildMark always produces a meaningful changelog for
+ pre-release builds.
+ tests:
+ - RepoConnectorBase_FindBaselineForPreRelease_SameCommitSkipped_ReturnsPreviousDistinctCommit
+ - RepoConnectorBase_FindBaselineForPreRelease_AllSameCommit_ReturnsNull
+
+ - id: BuildMark-RepoConnectorBase-BaselineRelease
+ title: >-
+ For a release target version, RepoConnectorBase shall select the most recent
+ preceding release tag as the baseline, skipping all pre-release tags.
+ justification: |
+ Release notes should describe changes since the previous stable release, not
+ since the most recent pre-release candidate. Skipping pre-release tags when
+ selecting the baseline ensures that the generated changelog covers the complete
+ set of changes that went into the release.
+ tests:
+ - RepoConnectorBase_FindBaselineForRelease_SkipsPreReleaseTags_ReturnsPreviousReleaseTag
+ - RepoConnectorBase_FindBaselineForRelease_NoPreviousRelease_ReturnsNull
+
+ - id: BuildMark-RepoConnectorBase-BaselineNoHistory
+ title: >-
+ When no qualifying baseline tag exists in the version history,
+ RepoConnectorBase shall return null to indicate that build notes should be
+ generated from the beginning of repository history.
+ justification: |
+ The first release in a repository has no predecessor tag, so there is no
+ meaningful baseline from which to compute a changelog. Returning null allows
+ the connector to generate build notes covering the complete repository history
+ up to the first release, rather than producing an error or an empty report.
+ tests:
+ - RepoConnectorBase_FindBaselineForRelease_NoPreviousVersion_ReturnsNull
+ - RepoConnectorBase_FindBaselineForPreRelease_NoPreviousVersion_ReturnsNull
diff --git a/docs/user_guide/cli-reference.md b/docs/user_guide/cli-reference.md
index 32ef036..ca43d44 100644
--- a/docs/user_guide/cli-reference.md
+++ b/docs/user_guide/cli-reference.md
@@ -14,7 +14,7 @@
| `--build-version ` | Specify the build version for report generation |
| `--report ` | Export build notes to a markdown file |
| `--depth ` | Set markdown heading depth (default: 1) |
-| `--include-known-issues` | Include open bugs as known issues |
+| `--include-known-issues` | Include known issues (open bugs, plus closed bugs with matching affected-versions). |
## Display Options
@@ -89,7 +89,8 @@ buildmark --build-version v1.2.3 --report build-notes.md --depth 2
### `--include-known-issues`
-Include known issues in the generated report. Known issues are open bugs in the GitHub repository.
+Include known issues in the generated report. Known issues are open bugs in the repository,
+plus closed bugs whose `affected-versions` field intersects the current build version.
```bash
buildmark --build-version v1.2.3 --report build-notes.md --include-known-issues
diff --git a/docs/verification/build-mark/build-notes/build-information.md b/docs/verification/build-mark/build-notes/build-information.md
index eef2cc9..bea5183 100644
--- a/docs/verification/build-mark/build-notes/build-information.md
+++ b/docs/verification/build-mark/build-notes/build-information.md
@@ -2,57 +2,267 @@
## Verification Approach
-`BuildInformation` is a data model with no dedicated test class. It is verified
-indirectly through integration tests in `ProgramTests.cs` and `RepoConnectorsTests.cs`
-that exercise the full pipeline and assert on the structure and content of
-`BuildInformation` instances returned by connectors.
+`BuildInformation` is verified with dedicated unit tests in `BuildInformationTests.cs`. Tests
+construct `BuildInformation` instances either directly or via `MockRepoConnector`, invoke
+`ToMarkdown`, and assert on the rendered output. `NSubstitute` is used in a small number of tests
+to simulate connector error conditions; `MockRepoConnector` is used for the remaining tests. No
+further mocking is needed.
## Dependencies
-| Mock / Stub | Reason |
-| ------------------- | -------------------------------------------------- |
-| `MockRepoConnector` | Returns deterministic `BuildInformation` instances |
+| Mock / Stub | Reason |
+| --- | --- |
+| `MockRepoConnector` | Supplies deterministic `BuildInformation` instances for rendering tests. |
+| `NSubstitute` (`IRepoConnector`) | Simulates error conditions that `MockRepoConnector` cannot reproduce. |
-## Test Scenarios (Integration)
+## Test Scenarios
-### Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues
+### BuildInformation_GetBuildInformationAsync_ThrowsWhenNoVersionAndNoTags
-**Scenario**: `Program.Run` generates a report using a mock connector.
+**Scenario**: A substitute connector is configured to throw `InvalidOperationException` with the
+message "No tags found"; `GetBuildInformationAsync()` is called without a version argument.
-**Expected**: `BuildInformation.ToMarkdown` produces valid markdown output including
-known issues section.
+**Expected**: `InvalidOperationException` is thrown; message contains "No tags found".
-**Requirement coverage**: `BuildMark-BuildNotes-BuildInformation`
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
-### RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation
+### BuildInformation_GetBuildInformationAsync_ThrowsWhenNoVersionAndCommitDoesNotMatchTag
-**Scenario**: Mock connector's `GetBuildInformationAsync` returns a complete result.
+**Scenario**: A substitute connector is configured to throw `InvalidOperationException` with the
+message "does not match any tag"; `GetBuildInformationAsync()` is called.
-**Expected**: `BuildInformation` instance contains version, baseline, changes,
-known issues, and routed sections.
+**Expected**: `InvalidOperationException` is thrown; message contains "does not match any tag".
-**Requirement coverage**: `BuildMark-BuildNotes-BuildInformation`
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
-### RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation
+### BuildInformation_GetBuildInformationAsync_WorksWithExplicitVersion
-**Scenario**: GitHub connector returns `BuildInformation` from mocked API responses.
+**Scenario**: `MockRepoConnector.GetBuildInformationAsync(VersionTag.Create("v2.1.0"))` is called.
-**Expected**: `BuildInformation` fields are correctly populated from the mock data.
+**Expected**: `CurrentVersionTag.VersionTag.Tag` equals `"v2.1.0"`; commit hash is
+`"current123hash456"`; `BaselineVersionTag.VersionTag.Tag` equals `"v2.0.0"`.
-**Requirement coverage**: `BuildMark-BuildNotes-BuildInformation`
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
-### RepoConnectors_AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation
+### BuildInformation_GetBuildInformationAsync_WorksWhenCurrentCommitMatchesLatestTag
-**Scenario**: Azure DevOps connector returns `BuildInformation` from mocked API responses.
+**Scenario**: A substitute connector returns a `BuildInformation` where `CurrentVersionTag` is
+`v2.0.0`; `GetBuildInformationAsync()` is called.
-**Expected**: `BuildInformation` fields are correctly populated from the mock data.
+**Expected**: Returned `BuildInformation.CurrentVersionTag.VersionTag.Tag` equals `"v2.0.0"`;
+`BaselineVersionTag.VersionTag.Tag` equals `"ver-1.1.0"`.
-**Requirement coverage**: `BuildMark-BuildNotes-BuildInformation`
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_GetBuildInformationAsync_PreReleaseUsesPreviousTag
+
+**Scenario**: `MockRepoConnector.GetBuildInformationAsync(VersionTag.Create("v2.0.0-beta.1"))` is
+called.
+
+**Expected**: `CurrentVersionTag.VersionTag.Tag` equals `"v2.0.0-beta.1"`;
+`BaselineVersionTag.VersionTag.Tag` equals `"v2.0.0"`.
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_GetBuildInformationAsync_ReleaseSkipsPreReleases
+
+**Scenario**: `MockRepoConnector.GetBuildInformationAsync(VersionTag.Create("v2.0.0"))` is called.
+
+**Expected**: `CurrentVersionTag.VersionTag.Tag` equals `"v2.0.0"`;
+`BaselineVersionTag.VersionTag.Tag` equals `"ver-1.1.0"` (pre-releases are skipped).
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_GetBuildInformationAsync_CollectsIssuesCorrectly
+
+**Scenario**: `MockRepoConnector.GetBuildInformationAsync(VersionTag.Create("ver-1.1.0"))` is
+called.
+
+**Expected**: `Changes` contains 2 items (ids `"1"` and `"#13"`); `Bugs` is empty; `KnownIssues`
+contains 2 items (ids `"4"` and `"6"`).
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_GetBuildInformationAsync_OrdersChangesByIndex
+
+**Scenario**: `MockRepoConnector.GetBuildInformationAsync(VersionTag.Create("ver-1.1.0"))` is
+called.
+
+**Expected**: `Changes` are ordered by `Index`; the item with `Index` 10 precedes the item with
+`Index` 13.
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_GetBuildInformationAsync_SeparatesBugAndChangeIssues
+
+**Scenario**: `MockRepoConnector.GetBuildInformationAsync(VersionTag.Create("v2.0.0"))` is called.
+
+**Expected**: `Changes` contains 1 item; `Bugs` contains 1 item (id `"2"`, title `"Fix bug in Y"`).
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_GetBuildInformationAsync_HandlesFirstReleaseCorrectly
+
+**Scenario**: `MockRepoConnector.GetBuildInformationAsync(VersionTag.Create("v1.0.0"))` is called.
+
+**Expected**: `BaselineVersionTag` is null; `CurrentVersionTag.VersionTag.Tag` equals `"v1.0.0"`.
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_ToMarkdown_GeneratesCorrectMarkdownWithDefaults
+
+**Scenario**: `ToMarkdown()` is called on `BuildInformation` for `v2.0.0` without any optional
+arguments.
+
+**Expected**: Markdown contains `# Build Report`, `## Version Information`, `## Changes`,
+`## Bugs Fixed`, `v2.0.0`, and `ver-1.1.0`; `## Known Issues` is absent.
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_ToMarkdown_IncludesKnownIssuesWhenRequested
+
+**Scenario**: `ToMarkdown(includeKnownIssues: true)` is called on `BuildInformation` for `v2.0.0`.
+
+**Expected**: Markdown contains `## Known Issues`, `Known bug A`, and `Known bug C`; `Known bug B`
+is absent.
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_ToMarkdown_RespectsCustomHeadingDepth
+
+**Scenario**: `ToMarkdown(headingDepth: 3)` is called on `BuildInformation` for `v2.0.0`.
+
+**Expected**: Top-level heading is `### Build Report`; sub-headings are `#### Version Information`,
+`#### Changes`, and `#### Bugs Fixed`.
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_ToMarkdown_DisplaysNAForEmptyChanges
+
+**Scenario**: A `BuildInformation` with an empty `Changes` list is rendered via `ToMarkdown()`.
+
+**Expected**: The Changes section contains `- N/A`.
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_ToMarkdown_DisplaysNAForEmptyBugs
+
+**Scenario**: A `BuildInformation` with an empty `Bugs` list is rendered via `ToMarkdown()`.
+
+**Expected**: The Bugs Fixed section contains `- N/A`.
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_ToMarkdown_IncludesIssueLinks
+
+**Scenario**: `ToMarkdown()` is called on `BuildInformation` for `v2.0.0` which has items with
+GitHub URLs.
+
+**Expected**: Markdown contains formatted links such as
+`- [3](https://github.com/example/repo/issues/3)` and
+`- [2](https://github.com/example/repo/issues/2)`.
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_ToMarkdown_HandlesFirstReleaseWithNA
+
+**Scenario**: `ToMarkdown()` is called on `BuildInformation` for `v1.0.0` (no baseline).
+
+**Expected**: Version Information section contains `| **Previous Version** | N/A |` and
+`| **Previous Commit Hash** | N/A |`.
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_ToMarkdown_IncludesFullChangelogWhenLinkPresent
+
+**Scenario**: `ToMarkdown()` is called on `BuildInformation` for `v2.0.0` which has a changelog
+link.
+
+**Expected**: Markdown contains `## Full Changelog`, the text `See the full changelog at`, and the
+URL containing `ver-1.1.0...v2.0.0`.
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_ToMarkdown_ExcludesFullChangelogWhenNoBaseline
+
+**Scenario**: `ToMarkdown()` is called on `BuildInformation` for `v1.0.0` (no baseline version).
+
+**Expected**: Markdown does not contain `## Full Changelog`.
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### BuildInformation_ToMarkdown_UsesBulletLists
+
+**Scenario**: `ToMarkdown(includeKnownIssues: true)` is called on `BuildInformation` for `v2.0.0`.
+
+**Expected**: Changes, Bugs Fixed, and Known Issues sections each use `- [` bullet format; no
+markdown table notation (`| :-: | :---------- |`) is present in those sections.
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### VersionCommitTag_Constructor_StoresVersionAndHash
+
+**Scenario**: A `VersionCommitTag` is constructed with a `VersionTag` and a commit hash string.
+
+**Expected**: `VersionTag` property equals the supplied tag; `CommitHash` equals the supplied hash
+string.
+
+**Requirement coverage**: `BuildMark-BuildInformation-Markdown`.
+
+### WebLink_Constructor_StoresTextAndUrl
+
+**Scenario**: A `WebLink` is constructed with display text and a target URL.
+
+**Expected**: `LinkText` property equals the supplied text; `TargetUrl` property equals the
+supplied URL.
+
+**Requirement coverage**: `BuildMark-WebLink-Record`.
+
+### BuildInformation_ToMarkdown_WithRoutedSections_RendersCustomSections
+
+**Scenario**: A `BuildInformation` is constructed with a populated `RoutedSections` list containing
+two custom sections (Features, Bugs); `ToMarkdown()` is called.
+
+**Expected**: Markdown contains `## Features` and `## Bugs` headings with their respective items;
+`## Changes` and `## Bugs Fixed` are absent.
+
+**Requirement coverage**: `BuildMark-BuildInformation-RoutedSections`.
+
+### BuildInformation_ToMarkdown_WithoutRoutedSections_RendersDefaultSections
+
+**Scenario**: A `BuildInformation` is constructed with `RoutedSections` left as null;
+`ToMarkdown()` is called.
+
+**Expected**: Markdown contains the default `## Changes` and `## Bugs Fixed` headings.
+
+**Requirement coverage**: `BuildMark-BuildInformation-RoutedSections`.
## Requirements Coverage
-- **BuildMark-BuildNotes-BuildInformation**:
- Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues,
- RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation,
- RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation,
- RepoConnectors_AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation
+- **`BuildMark-BuildInformation-Markdown`**:
+ - BuildInformation_GetBuildInformationAsync_ThrowsWhenNoVersionAndNoTags
+ - BuildInformation_GetBuildInformationAsync_ThrowsWhenNoVersionAndCommitDoesNotMatchTag
+ - BuildInformation_GetBuildInformationAsync_WorksWithExplicitVersion
+ - BuildInformation_GetBuildInformationAsync_WorksWhenCurrentCommitMatchesLatestTag
+ - BuildInformation_GetBuildInformationAsync_PreReleaseUsesPreviousTag
+ - BuildInformation_GetBuildInformationAsync_ReleaseSkipsPreReleases
+ - BuildInformation_GetBuildInformationAsync_CollectsIssuesCorrectly
+ - BuildInformation_GetBuildInformationAsync_OrdersChangesByIndex
+ - BuildInformation_GetBuildInformationAsync_SeparatesBugAndChangeIssues
+ - BuildInformation_GetBuildInformationAsync_HandlesFirstReleaseCorrectly
+ - BuildInformation_ToMarkdown_GeneratesCorrectMarkdownWithDefaults
+ - BuildInformation_ToMarkdown_IncludesKnownIssuesWhenRequested
+ - BuildInformation_ToMarkdown_RespectsCustomHeadingDepth
+ - BuildInformation_ToMarkdown_DisplaysNAForEmptyChanges
+ - BuildInformation_ToMarkdown_DisplaysNAForEmptyBugs
+ - BuildInformation_ToMarkdown_IncludesIssueLinks
+ - BuildInformation_ToMarkdown_HandlesFirstReleaseWithNA
+ - BuildInformation_ToMarkdown_IncludesFullChangelogWhenLinkPresent
+ - BuildInformation_ToMarkdown_ExcludesFullChangelogWhenNoBaseline
+ - BuildInformation_ToMarkdown_UsesBulletLists
+ - VersionCommitTag_Constructor_StoresVersionAndHash
+- **`BuildMark-WebLink-Record`**:
+ - WebLink_Constructor_StoresTextAndUrl
+- **`BuildMark-BuildInformation-RoutedSections`**:
+ - BuildInformation_ToMarkdown_WithRoutedSections_RendersCustomSections
+ - BuildInformation_ToMarkdown_WithoutRoutedSections_RendersDefaultSections
diff --git a/docs/verification/build-mark/build-notes/build-notes.md b/docs/verification/build-mark/build-notes/build-notes.md
index 7f91309..6ae4050 100644
--- a/docs/verification/build-mark/build-notes/build-notes.md
+++ b/docs/verification/build-mark/build-notes/build-notes.md
@@ -2,51 +2,52 @@
## Verification Approach
-The BuildNotes subsystem is verified at the integration level through `ProgramTests.cs`
-and `RepoConnectorsTests.cs`. There is no dedicated `BuildNotesTests.cs` file; the
-subsystem is exercised indirectly whenever `Program.Run` generates a report or when
-a connector's `GetBuildInformationAsync` is exercised with mock data.
-
-`BuildInformation.ToMarkdown` is exercised by report generation tests.
-`ItemInfo` and `WebLink` data models are exercised through the connector tests that
-populate `BuildInformation` instances with change items and web links.
+`BuildNotes` is the subsystem encompassing `BuildInformation`, `ItemInfo`, and `WebLink`. It is
+verified with dedicated subsystem tests in `BuildNotesTests.cs`. The subsystem uses
+`MockRepoConnector` to supply deterministic `BuildInformation` instances; no other mocking or test
+doubles are required.
## Dependencies
-| Mock / Stub | Reason |
-| ------------------- | --------------------------------------------------------- |
-| `MockRepoConnector` | Returns deterministic `BuildInformation` with known items |
-| `Context` | Provides output capture for markdown assertion |
+| Mock / Stub | Reason |
+| ------------------- | -------------------------------------------------------------------- |
+| `MockRepoConnector` | Provides deterministic `BuildInformation` for subsystem-level tests. |
+
+## Test Scenarios
+
+### BuildNotes_ReportModel_GeneratesCorrectMarkdown
-## Test Scenarios (Integration)
+**Scenario**: `MockRepoConnector.GetBuildInformationAsync` is called with version `v2.0.0`; the
+resulting `BuildInformation` is rendered via `ToMarkdown()`.
-The following integration tests in `ProgramTests.cs` exercise the BuildNotes subsystem:
+**Expected**: Markdown contains `# Build Report`, `## Version Information`, `## Changes`,
+`## Bugs Fixed`, `v2.0.0`, and `ver-1.1.0`.
-### Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues
+**Requirement coverage**: `BuildMark-BuildNotes-ReportModel`.
-**Scenario**: A full pipeline run generates a report with known issues included.
+### BuildNotes_ReportModel_IncludesKnownIssues
-**Expected**: `BuildInformation` is populated; markdown report is written to file.
+**Scenario**: `ToMarkdown(includeKnownIssues: true)` is called on a `BuildInformation` for `v2.0.0`
+that has known issues.
-**Requirement coverage**: `BuildMark-BuildNotes-BuildInformation`,
-`BuildMark-BuildNotes-ItemInfo`
+**Expected**: Markdown contains `## Known Issues`, `Known bug A`, and `Known bug C`; `Known bug B`
+(which has `affected-versions [5.0.0,)`) does not appear.
-The following tests in `RepoConnectorsTests.cs` exercise the subsystem through
-connector output:
+**Requirement coverage**: `BuildMark-BuildNotes-ReportModel`.
-### RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation
+### BuildNotes_ReportModel_IncludesFullChangelog
-**Scenario**: Mock connector returns a fully populated `BuildInformation` instance.
+**Scenario**: `ToMarkdown()` is called on a `BuildInformation` for `v2.0.0` that has a changelog
+link.
-**Expected**: All fields including changes, known issues, and web links are present.
+**Expected**: Markdown contains `## Full Changelog` and the comparison link text
+`ver-1.1.0...v2.0.0`.
-**Requirement coverage**: `BuildMark-BuildNotes-BuildInformation`,
-`BuildMark-BuildNotes-ItemInfo`, `BuildMark-BuildNotes-WebLink`
+**Requirement coverage**: `BuildMark-BuildNotes-ReportModel`.
## Requirements Coverage
-- **BuildMark-BuildNotes-BuildInformation**: Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues,
- RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation
-- **BuildMark-BuildNotes-ItemInfo**: Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues,
- RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation
-- **BuildMark-BuildNotes-WebLink**: RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation
+- **`BuildMark-BuildNotes-ReportModel`**:
+ - BuildNotes_ReportModel_GeneratesCorrectMarkdown
+ - BuildNotes_ReportModel_IncludesKnownIssues
+ - BuildNotes_ReportModel_IncludesFullChangelog
diff --git a/docs/verification/build-mark/build-notes/item-info.md b/docs/verification/build-mark/build-notes/item-info.md
index d45ea3d..f19ad36 100644
--- a/docs/verification/build-mark/build-notes/item-info.md
+++ b/docs/verification/build-mark/build-notes/item-info.md
@@ -2,47 +2,41 @@
## Verification Approach
-`ItemInfo` is a data model with no dedicated test class. It is verified indirectly
-through connector tests in `RepoConnectorsTests.cs`, `GitHubRepoConnectorTests.cs`,
-`AzureDevOpsRepoConnectorTests.cs`, and `MockRepoConnectorTests.cs` that assert on
-the items contained in `BuildInformation` instances returned by connectors.
+`ItemInfo` is a record type that carries no logic of its own. It is verified through
+`BuildInformationTests.cs`, which asserts on the ordering, identity, and link properties of
+`ItemInfo` entries returned by `MockRepoConnector`. No mocking beyond `MockRepoConnector` is
+needed.
## Dependencies
-| Mock / Stub | Reason |
-| ------------------- | -------------------------------------------------------- |
-| `MockRepoConnector` | Returns `BuildInformation` with known `ItemInfo` entries |
+| Mock / Stub | Reason |
+| ------------------- | -------------------------------------------------------------------- |
+| `MockRepoConnector` | Provides `BuildInformation` instances with known `ItemInfo` entries. |
-## Test Scenarios (Integration)
+## Test Scenarios
-### RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation
+### BuildInformation_GetBuildInformationAsync_OrdersChangesByIndex
-**Scenario**: Mock connector returns complete build information with change items.
+**Scenario**: `MockRepoConnector.GetBuildInformationAsync(VersionTag.Create("ver-1.1.0"))` is
+called; the `Changes` collection is inspected.
-**Expected**: `BuildInformation.Changes` contains `ItemInfo` entries with correct
-title, type, visibility, and web link.
+**Expected**: `ItemInfo` entries in `Changes` are ordered by `Index` in ascending order; the first
+entry has `Index` 10 and `Id` `"1"`; the second has `Index` 13.
-**Requirement coverage**: `BuildMark-BuildNotes-ItemInfo`
+**Requirement coverage**: `BuildMark-ItemInfo-Record`.
-### RepoConnectors_GitHubConnector_GetBuildInformation_WithPullRequests_GathersChanges
+### BuildInformation_ToMarkdown_UsesBulletLists
-**Scenario**: GitHub connector processes pull requests into change items.
+**Scenario**: `ToMarkdown(includeKnownIssues: true)` is called on `BuildInformation` for `v2.0.0`;
+the rendered bullet list items are inspected.
-**Expected**: Each pull request is represented as an `ItemInfo` in `BuildInformation.Changes`.
+**Expected**: Each `ItemInfo` entry is rendered as a `- [id](url)` bullet; no table-row format is
+present.
-**Requirement coverage**: `BuildMark-BuildNotes-ItemInfo`
-
-### RepoConnectors_AzureDevOps_GetBuildInformation_WithPullRequests_GathersChanges
-
-**Scenario**: Azure DevOps connector processes pull requests into change items.
-
-**Expected**: Each pull request is represented as an `ItemInfo` in `BuildInformation.Changes`.
-
-**Requirement coverage**: `BuildMark-BuildNotes-ItemInfo`
+**Requirement coverage**: `BuildMark-ItemInfo-Record`.
## Requirements Coverage
-- **BuildMark-BuildNotes-ItemInfo**:
- RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation,
- RepoConnectors_GitHubConnector_GetBuildInformation_WithPullRequests_GathersChanges,
- RepoConnectors_AzureDevOps_GetBuildInformation_WithPullRequests_GathersChanges
+- **`BuildMark-ItemInfo-Record`**:
+ - BuildInformation_GetBuildInformationAsync_OrdersChangesByIndex
+ - BuildInformation_ToMarkdown_UsesBulletLists
diff --git a/docs/verification/build-mark/build-notes/web-link.md b/docs/verification/build-mark/build-notes/web-link.md
index 0856603..399cb2d 100644
--- a/docs/verification/build-mark/build-notes/web-link.md
+++ b/docs/verification/build-mark/build-notes/web-link.md
@@ -2,46 +2,28 @@
## Verification Approach
-`WebLink` is a utility data model with no dedicated test class. It is verified
-indirectly through connector tests that assert on web link properties within
-`ItemInfo` and `BuildInformation` instances. The `WebLink` type is used to attach
-URLs to items, releases, and changelog links.
+`WebLink` is a record type with no logic. It is verified through a dedicated test in
+`BuildInformationTests.cs` that constructs a `WebLink` directly and asserts on its properties.
+No mocking is required.
## Dependencies
-| Mock / Stub | Reason |
-| ------------------- | ---------------------------------------------------------- |
-| `MockRepoConnector` | Returns `BuildInformation` with `WebLink` entries on items |
+| Mock / Stub | Reason |
+| ----------- | --------------- |
+| None | No mocks needed |
-## Test Scenarios (Integration)
+## Test Scenarios
-### RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation
+### WebLink_Constructor_StoresTextAndUrl
-**Scenario**: Mock connector returns complete build information including items with web links.
+**Scenario**: A `WebLink` is constructed with display text `"v1.0.0...v2.0.0"` and a GitHub
+compare URL.
-**Expected**: `ItemInfo` entries contain `WebLink` instances with non-null URL and label.
+**Expected**: `LinkText` equals `"v1.0.0...v2.0.0"`; `TargetUrl` equals the supplied URL.
-**Requirement coverage**: `BuildMark-BuildNotes-WebLink`
-
-### RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation
-
-**Scenario**: GitHub connector populates change items with links to GitHub pull request URLs.
-
-**Expected**: `ItemInfo.Link` is a `WebLink` with a valid GitHub URL.
-
-**Requirement coverage**: `BuildMark-BuildNotes-WebLink`
-
-### RepoConnectors_AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation
-
-**Scenario**: Azure DevOps connector populates change items with links to Azure DevOps URLs.
-
-**Expected**: `ItemInfo.Link` is a `WebLink` with a valid Azure DevOps URL.
-
-**Requirement coverage**: `BuildMark-BuildNotes-WebLink`
+**Requirement coverage**: `BuildMark-WebLink-Record`.
## Requirements Coverage
-- **BuildMark-BuildNotes-WebLink**:
- RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation,
- RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation,
- RepoConnectors_AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation
+- **`BuildMark-WebLink-Record`**:
+ - WebLink_Constructor_StoresTextAndUrl
diff --git a/docs/verification/build-mark/cli/cli.md b/docs/verification/build-mark/cli/cli.md
index d69f9de..d84a3a9 100644
--- a/docs/verification/build-mark/cli/cli.md
+++ b/docs/verification/build-mark/cli/cli.md
@@ -67,9 +67,10 @@ and output behavior.
### Cli_ReportFlags_SetProperties
-**Scenario**: Context is created with `--report path.md` and `--report-depth 2` arguments.
+**Scenario**: Context is created with `["--report", "output.md", "--depth", "3",
+"--include-known-issues"]`.
-**Expected**: `ReportFile` and `Depth` properties are set to the provided values.
+**Expected**: `ReportFile` equals `"output.md"`; `Depth` equals 3; `IncludeKnownIssues` is true.
**Requirement coverage**: `BuildMark-Program-Report`, `BuildMark-Program-Depth`
@@ -91,17 +92,17 @@ and output behavior.
### Cli_ResultsFlag_SetsProperty
-**Scenario**: Context is created with `--results path.trx` argument.
+**Scenario**: Context is created with `["--results", "results.trx"]`.
-**Expected**: `Results` property equals the provided path.
+**Expected**: `ResultsFile` property equals `"results.trx"`.
**Requirement coverage**: `BuildMark-Program-Results`
### Cli_ResultFlag_SetsProperty
-**Scenario**: Context is created with `--result path.trx` argument (alias).
+**Scenario**: Context is created with `["--result", "results.trx"]` (alias).
-**Expected**: `Results` property equals the provided path.
+**Expected**: `ResultsFile` property equals `"results.trx"`.
**Requirement coverage**: `BuildMark-Program-Results`
@@ -109,23 +110,25 @@ and output behavior.
**Scenario**: `context.WriteError` is called with an error message.
-**Expected**: Message is written to the error stream; exit code is set.
+**Expected**: The message `"Subsystem error test"` appears in the standard error stream.
**Requirement coverage**: `BuildMark-Program-ErrorHandling`
### Cli_InvalidArgument_ThrowsException
-**Scenario**: Context is created with an unrecognized flag.
+**Scenario**: Context is created with `["--unsupported"]`.
-**Expected**: `ArgumentException` is thrown.
+**Expected**: `ArgumentException` is thrown with a message containing
+`"Unsupported argument '--unsupported'"`.
**Requirement coverage**: `BuildMark-Cli-Context`
### Cli_MissingArgumentValue_ThrowsException
-**Scenario**: Context is created with a flag that requires a value but no value is provided.
+**Scenario**: Context is created with `["--build-version"]` (value missing).
-**Expected**: `ArgumentException` is thrown.
+**Expected**: `ArgumentException` is thrown with a message containing
+`"--build-version requires a version argument"`.
**Requirement coverage**: `BuildMark-Cli-Context`
diff --git a/docs/verification/build-mark/cli/context.md b/docs/verification/build-mark/cli/context.md
index 8c0305b..edeed44 100644
--- a/docs/verification/build-mark/cli/context.md
+++ b/docs/verification/build-mark/cli/context.md
@@ -21,7 +21,7 @@ no mocking is needed at this level.
**Scenario**: `Context.Create` is called with an empty argument array.
-**Expected**: All boolean flags are false; `ResultsFile` is null; `HeadingDepth` is 1;
+**Expected**: All boolean flags are false; `ResultsFile` is null; `Depth` is null;
exit code is 0.
**Requirement coverage**: `BuildMark-Context-DefaultConstruction`.
@@ -110,15 +110,16 @@ exit code is 0.
**Scenario**: `Context.Create` is called with `["--depth", "3"]`.
-**Expected**: `HeadingDepth` property equals 3.
+**Expected**: `Depth` property equals 3.
**Requirement coverage**: `BuildMark-Context-ArgumentParsing`.
### Context_Create_LegacyReportDepthArgument_SetsDepthProperty
-**Scenario**: `Context.Create` is called with the legacy depth argument form.
+**Scenario**: `Context.Create` is called with `["--report-depth", "3"]` (legacy alias for
+`--depth`).
-**Expected**: `HeadingDepth` property is set to the specified value.
+**Expected**: `Depth` property equals 3.
**Requirement coverage**: `BuildMark-Context-ArgumentParsing`.
@@ -157,15 +158,19 @@ with a test message.
### Context_Create_MultipleArguments_SetsAllPropertiesCorrectly
-**Scenario**: `Context.Create` is called with multiple flags together.
+**Scenario**: `Context.Create` is called with `["--silent", "--validate", "--lint",
+"--build-version", "1.2.3", "--report", "report.md", "--depth", "2",
+"--include-known-issues", "--results", "results.trx"]`.
-**Expected**: All corresponding properties are set correctly; no exception is thrown.
+**Expected**: `Silent`, `Validate`, `Lint`, and `IncludeKnownIssues` are true; `BuildVersion`
+equals `"1.2.3"`; `ReportFile` equals `"report.md"`; `Depth` equals 2; `ResultsFile` equals
+`"results.trx"`; no exception is thrown.
**Requirement coverage**: `BuildMark-Context-FlagParsing`, `BuildMark-Context-ArgumentParsing`.
### Context_Create_UnsupportedArgument_ThrowsArgumentException
-**Scenario**: `Context.Create` is called with an unrecognized argument (e.g., `["--unknown"]`).
+**Scenario**: `Context.Create` is called with `["--unsupported"]`.
**Expected**: An `ArgumentException` is thrown containing the text "Unsupported argument".
diff --git a/docs/verification/build-mark/configuration/azure-dev-ops-connector-config.md b/docs/verification/build-mark/configuration/azure-dev-ops-connector-config.md
index 00ddb8f..941c889 100644
--- a/docs/verification/build-mark/configuration/azure-dev-ops-connector-config.md
+++ b/docs/verification/build-mark/configuration/azure-dev-ops-connector-config.md
@@ -2,30 +2,42 @@
## Verification Approach
-`AzureDevOpsConnectorConfig` is a data model class verified indirectly through
-`RepoConnectorFactoryTests.cs`. Tests that supply Azure DevOps-specific configuration
-within a `ConnectorConfig` confirm that the settings are forwarded to the created
-`AzureDevOpsRepoConnector`.
+`AzureDevOpsConnectorConfig` is verified through `ConfigurationTests.cs`. Tests write
+`.buildmark.yaml` files with Azure DevOps connector blocks and assert that `OrganizationUrl`,
+`Organization`, `Project`, and `Repository` properties are correctly parsed, including alias key
+support. No mocking is required.
## Dependencies
-| Mock / Stub | Reason |
-| ----------- | ---------- |
-| None | Data class |
+| Mock / Stub | Reason |
+| ----------- | -------------------------------------------------------------------- |
+| File system | Tests create temporary `.buildmark.yaml` files in `Path.GetTempPath` |
-## Test Scenarios (Integration)
+## Test Scenarios
-### RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration
+### BuildMarkConfigReader_ReadAsync_ValidAzureDevOpsConnector_ReturnsParsedConfiguration
-**Scenario**: A `ConnectorConfig` with an `AzureDevOpsConnectorConfig` is passed to
-the factory.
+**Scenario**: A `.buildmark.yaml` with an Azure DevOps connector block using canonical keys
+(`organization`, `repository`) is written; `BuildMarkConfigReader.ReadAsync` is called;
+`Config.Connector.AzureDevOps` is inspected.
-**Expected**: The resulting connector incorporates the Azure DevOps-specific
-configuration.
+**Expected**: `OrganizationUrl` equals `"https://dev.azure.com/myorg"`; `Organization` equals
+`"myorg"`; `Project` equals `"myproject"`; `Repository` equals `"myrepo"`.
-**Requirement coverage**: `BuildMark-Configuration-AzureDevOpsConnectorConfig`
+**Requirement coverage**: `BuildMark-AzureDevOpsConnectorConfig-Properties`.
+
+### BuildMarkConfigReader_ReadAsync_AzureDevOpsConnectorAliases_ReturnsParsedConfiguration
+
+**Scenario**: A `.buildmark.yaml` with Azure DevOps connector using alias keys (`org`, `repo`) is
+written; `BuildMarkConfigReader.ReadAsync` is called; `Config.Connector.AzureDevOps` is inspected.
+
+**Expected**: `OrganizationUrl`, `Organization`, `Project`, and `Repository` are all populated
+correctly from the alias keys.
+
+**Requirement coverage**: `BuildMark-AzureDevOpsConnectorConfig-Properties`.
## Requirements Coverage
-- **BuildMark-Configuration-AzureDevOpsConnectorConfig**:
- RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration
+- **`BuildMark-AzureDevOpsConnectorConfig-Properties`**:
+ - BuildMarkConfigReader_ReadAsync_ValidAzureDevOpsConnector_ReturnsParsedConfiguration
+ - BuildMarkConfigReader_ReadAsync_AzureDevOpsConnectorAliases_ReturnsParsedConfiguration
diff --git a/docs/verification/build-mark/configuration/build-mark-config-reader.md b/docs/verification/build-mark/configuration/build-mark-config-reader.md
index 8ba1b2d..3af72ef 100644
--- a/docs/verification/build-mark/configuration/build-mark-config-reader.md
+++ b/docs/verification/build-mark/configuration/build-mark-config-reader.md
@@ -2,41 +2,105 @@
## Verification Approach
-`BuildMarkConfigReader` is verified indirectly through `ProgramTests.cs`. The lint
-and report tests exercise `BuildMarkConfigReader.ReadAsync` as part of the program
-execution pipeline. When `Lint = true`, `ReadAsync` is called and the result is
-reported to the context. When a report is generated, `ReadAsync` is called to load
-optional configuration before connector creation.
+`BuildMarkConfigReader` is verified with dedicated unit tests in `ConfigurationTests.cs`. Tests
+write `.buildmark.yaml` files to temporary directories and call `BuildMarkConfigReader.ReadAsync`,
+asserting on the returned `ConfigurationLoadResult`. No mocking is required; the real file system
+is used.
## Dependencies
-| Mock / Stub | Reason |
-| ----------- | ------------------------------------------------------- |
-| File system | `ReadAsync` attempts to read `.buildmark.yaml` from the |
-| | current directory; tests control file presence |
+| Mock / Stub | Reason |
+| ----------- | -------------------------------------------------------------------- |
+| File system | Tests create temporary `.buildmark.yaml` files in `Path.GetTempPath` |
-## Test Scenarios (Integration)
+## Test Scenarios
-### Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero
+### BuildMarkConfigReader_ReadAsync_MissingFile_ReturnsEmptyResult
-**Scenario**: `Program.Run` calls `BuildMarkConfigReader.ReadAsync` when no
-`.buildmark.yaml` is present.
+**Scenario**: `BuildMarkConfigReader.ReadAsync` is called on a temp directory containing no
+`.buildmark.yaml`.
-**Expected**: Reader returns a result with no errors; exit code is 0.
+**Expected**: `Config` is null; `HasErrors` is false; `Issues` is empty.
-**Requirement coverage**: `BuildMark-Configuration-BuildMarkConfigReader`
+**Requirement coverage**: `BuildMark-ConfigReader-MissingFile`.
-### Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues
+### BuildMarkConfigReader_ReadAsync_ValidFile_ReturnsParsedConfiguration
-**Scenario**: `Program.Run` calls `BuildMarkConfigReader.ReadAsync` before report
-generation; configuration may or may not be present.
+**Scenario**: A `.buildmark.yaml` with a GitHub connector, sections, and rules is written;
+`BuildMarkConfigReader.ReadAsync` is called.
-**Expected**: Reader returns without error; report generation proceeds.
+**Expected**: `Config` is non-null; `HasErrors` is false; connector type, owner, repo, base-url,
+sections, and rules are parsed correctly.
-**Requirement coverage**: `BuildMark-Configuration-BuildMarkConfigReader`
+**Requirement coverage**: `BuildMark-ConfigReader-ReadAsync`.
+
+### BuildMarkConfigReader_ReadAsync_InvalidRepositoryValue_ReturnsErrorIssue
+
+**Scenario**: A `.buildmark.yaml` with `repository: invalid` (not in `owner/repo` format) is
+written; `BuildMarkConfigReader.ReadAsync` is called.
+
+**Expected**: `Config` is null; `HasErrors` is true; issue description contains `"owner/repo"`.
+
+**Requirement coverage**: `BuildMark-ConfigReader-MalformedFile`.
+
+### BuildMarkConfigReader_ReadAsync_MalformedFile_ReturnsErrorIssue
+
+**Scenario**: A `.buildmark.yaml` containing a tab character (invalid YAML) is written;
+`BuildMarkConfigReader.ReadAsync` is called.
+
+**Expected**: `Config` is null; `HasErrors` is true; issue description contains `"tab"`
+(case-insensitive).
+
+**Requirement coverage**: `BuildMark-ConfigReader-MalformedFile`.
+
+### BuildMarkConfigReader_ReadAsync_ValidAzureDevOpsConnector_ReturnsParsedConfiguration
+
+**Scenario**: A `.buildmark.yaml` with an Azure DevOps connector block (url, organization,
+project, repository) is written; `BuildMarkConfigReader.ReadAsync` is called.
+
+**Expected**: `Config.Connector.Type` is `"azure-devops"`; all Azure DevOps fields are parsed
+correctly.
+
+**Requirement coverage**: `BuildMark-ConfigReader-ReadAsync`.
+
+### BuildMarkConfigReader_ReadAsync_AzureDevOpsConnectorAliases_ReturnsParsedConfiguration
+
+**Scenario**: A `.buildmark.yaml` with Azure DevOps connector using alias keys (`org`, `repo`) is
+written; `BuildMarkConfigReader.ReadAsync` is called.
+
+**Expected**: All Azure DevOps fields are parsed correctly using the alias keys.
+
+**Requirement coverage**: `BuildMark-ConfigReader-ReadAsync`.
+
+### BuildMarkConfigReader_ReadAsync_AzureDevOpsUnsupportedKey_ReturnsErrorIssue
+
+**Scenario**: A `.buildmark.yaml` with an unknown key inside the Azure DevOps connector block is
+written; `BuildMarkConfigReader.ReadAsync` is called.
+
+**Expected**: `Config` is null; `HasErrors` is true; issue description contains
+`"Unsupported Azure DevOps connector key"`.
+
+**Requirement coverage**: `BuildMark-ConfigReader-MalformedFile`.
+
+### BuildMarkConfigReader_ReadAsync_AzureDevOpsNonMapping_ReturnsErrorIssue
+
+**Scenario**: A `.buildmark.yaml` where `azure-devops:` is a scalar value instead of a mapping is
+written; `BuildMarkConfigReader.ReadAsync` is called.
+
+**Expected**: `Config` is null; `HasErrors` is true; issue description contains `"YAML mapping"`.
+
+**Requirement coverage**: `BuildMark-ConfigReader-MalformedFile`.
## Requirements Coverage
-- **BuildMark-Configuration-BuildMarkConfigReader**:
- Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero,
- Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues
+- **`BuildMark-ConfigReader-ReadAsync`**:
+ - BuildMarkConfigReader_ReadAsync_ValidFile_ReturnsParsedConfiguration
+ - BuildMarkConfigReader_ReadAsync_ValidAzureDevOpsConnector_ReturnsParsedConfiguration
+ - BuildMarkConfigReader_ReadAsync_AzureDevOpsConnectorAliases_ReturnsParsedConfiguration
+- **`BuildMark-ConfigReader-MissingFile`**:
+ - BuildMarkConfigReader_ReadAsync_MissingFile_ReturnsEmptyResult
+- **`BuildMark-ConfigReader-MalformedFile`**:
+ - BuildMarkConfigReader_ReadAsync_InvalidRepositoryValue_ReturnsErrorIssue
+ - BuildMarkConfigReader_ReadAsync_MalformedFile_ReturnsErrorIssue
+ - BuildMarkConfigReader_ReadAsync_AzureDevOpsUnsupportedKey_ReturnsErrorIssue
+ - BuildMarkConfigReader_ReadAsync_AzureDevOpsNonMapping_ReturnsErrorIssue
diff --git a/docs/verification/build-mark/configuration/build-mark-config.md b/docs/verification/build-mark/configuration/build-mark-config.md
index 359f550..f0f30c7 100644
--- a/docs/verification/build-mark/configuration/build-mark-config.md
+++ b/docs/verification/build-mark/configuration/build-mark-config.md
@@ -2,40 +2,33 @@
## Verification Approach
-`BuildMarkConfig` is a data model class with no dedicated test class. It is verified
-indirectly through program-level and connector factory tests that exercise the
-configuration pipeline. When `BuildMarkConfigReader.ReadAsync` returns a
-`ConfigurationLoadResult`, the embedded `BuildMarkConfig` instance drives connector
-creation and report configuration.
+`BuildMarkConfig` is verified with dedicated unit tests in `ConfigurationTests.cs`. The test
+`BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection` calls
+`BuildMarkConfig.CreateDefault()` and asserts on the returned sections and routing rules. No
+mocking is required.
## Dependencies
-| Mock / Stub | Reason |
-| ----------- | ---------------------------------------------------------- |
-| File system | Integration tests may create temporary configuration files |
+| Mock / Stub | Reason |
+| ----------- | --------------- |
+| None | No mocks needed |
-## Test Scenarios (Integration)
+## Test Scenarios
-### Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero
+### BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection
-**Scenario**: `Program.Run` processes a lint request; `BuildMarkConfig` is loaded
-as an empty/default instance when no file is present.
+**Scenario**: `BuildMarkConfig.CreateDefault()` is called.
-**Expected**: Default `BuildMarkConfig` is handled gracefully; exit code is 0.
+**Expected**: The result contains 3 sections (`changes`, `bugs-fixed`, `dependency-updates`) with
+correct titles; 6 rules are present with correct routes and match conditions; the final catch-all
+rule has a null match.
-**Requirement coverage**: `BuildMark-Configuration-BuildMarkConfig`
-
-### RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration
-
-**Scenario**: `BuildMarkConfig.Connector` field is used by the factory to create
-a connector with GitHub settings.
-
-**Expected**: Connector reflects the `BuildMarkConfig` connector settings.
-
-**Requirement coverage**: `BuildMark-Configuration-BuildMarkConfig`
+**Requirement coverage**: `BuildMark-SectionConfig-Properties`,
+`BuildMark-RuleConfig-Properties`.
## Requirements Coverage
-- **BuildMark-Configuration-BuildMarkConfig**:
- Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero,
- RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration
+- **`BuildMark-SectionConfig-Properties`**:
+ - BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection
+- **`BuildMark-RuleConfig-Properties`**:
+ - BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection
diff --git a/docs/verification/build-mark/configuration/configuration-issue.md b/docs/verification/build-mark/configuration/configuration-issue.md
index 045b399..30b234f 100644
--- a/docs/verification/build-mark/configuration/configuration-issue.md
+++ b/docs/verification/build-mark/configuration/configuration-issue.md
@@ -2,30 +2,50 @@
## Verification Approach
-`ConfigurationIssue` is a data class with no dedicated test class. It is verified
-indirectly through `ProgramTests.cs` via the `ConfigurationLoadResult` pipeline.
-When configuration parsing produces issues, they are stored as `ConfigurationIssue`
-instances and reported to the context. No dedicated test exercises this path in
-isolation; it is covered by integration paths where configuration errors surface.
+`ConfigurationIssue` is a record type with no logic. It is verified through
+`ConfigurationTests.cs`, which constructs `ConfigurationIssue` instances directly and asserts on
+their `FilePath`, `Line`, `Severity`, and `Description` properties. No mocking is required.
## Dependencies
-| Mock / Stub | Reason |
-| ----------- | ---------- |
-| None | Data class |
+| Mock / Stub | Reason |
+| ----------- | --------------- |
+| None | No mocks needed |
-## Test Scenarios (Integration)
+## Test Scenarios
-### Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero
+### ConfigurationLoadResult_ReportTo_ErrorIssue_SetsExitCode
-**Scenario**: `ConfigurationLoadResult` contains no `ConfigurationIssue` entries
-when no config file is present; `ReportTo` is a no-op.
+**Scenario**: A `ConfigurationIssue` with `Error` severity is constructed and placed in a
+`ConfigurationLoadResult`; `ReportTo` is called.
-**Expected**: No issues reported; exit code is 0.
+**Expected**: `context.ExitCode` equals 1, confirming the issue record carries severity correctly.
-**Requirement coverage**: `BuildMark-Configuration-ConfigurationIssue`
+**Requirement coverage**: `BuildMark-ConfigurationIssue-Record`.
+
+### ConfigurationLoadResult_ReportTo_WarningIssue_DoesNotSetExitCode
+
+**Scenario**: A `ConfigurationIssue` with `Warning` severity is constructed and placed in a
+`ConfigurationLoadResult`; `ReportTo` is called.
+
+**Expected**: `context.ExitCode` remains 0, confirming severity is preserved and evaluated
+correctly.
+
+**Requirement coverage**: `BuildMark-ConfigurationIssue-Record`.
+
+### ConfigurationLoadResult_ReportTo_IssueMessage_IncludesLineNumber
+
+**Scenario**: A `ConfigurationIssue` is constructed with `FilePath` `"/repo/.buildmark.yaml"`,
+`Line` 7, `Error` severity, and description `"Unexpected value"`; properties are inspected.
+
+**Expected**: `FilePath` equals `"/repo/.buildmark.yaml"`; `Line` equals 7; `Severity` equals
+`Error`; `Description` equals `"Unexpected value"`.
+
+**Requirement coverage**: `BuildMark-ConfigurationIssue-Record`.
## Requirements Coverage
-- **BuildMark-Configuration-ConfigurationIssue**:
- Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero
+- **`BuildMark-ConfigurationIssue-Record`**:
+ - ConfigurationLoadResult_ReportTo_ErrorIssue_SetsExitCode
+ - ConfigurationLoadResult_ReportTo_WarningIssue_DoesNotSetExitCode
+ - ConfigurationLoadResult_ReportTo_IssueMessage_IncludesLineNumber
diff --git a/docs/verification/build-mark/configuration/configuration-load-result.md b/docs/verification/build-mark/configuration/configuration-load-result.md
index 057ae83..688b1a9 100644
--- a/docs/verification/build-mark/configuration/configuration-load-result.md
+++ b/docs/verification/build-mark/configuration/configuration-load-result.md
@@ -2,30 +2,51 @@
## Verification Approach
-`ConfigurationLoadResult` is a data class with no dedicated test class. It is verified
-indirectly through `ProgramTests.cs`, where the result of `BuildMarkConfigReader.ReadAsync`
-is used to report issues and extract the active configuration. Tests that exercise
-the lint path confirm that `ConfigurationLoadResult` is handled correctly when no
-issues are present.
+`ConfigurationLoadResult` is verified with dedicated unit tests in `ConfigurationTests.cs`. Tests
+construct `ConfigurationLoadResult` instances directly with controlled `ConfigurationIssue` entries
+and assert on the behavior of `ReportTo(context)`. No mocking is required.
## Dependencies
-| Mock / Stub | Reason |
-| ----------- | ---------- |
-| None | Data class |
+| Mock / Stub | Reason |
+| ----------- | --------------- |
+| None | No mocks needed |
-## Test Scenarios (Integration)
+## Test Scenarios
-### Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero
+### ConfigurationLoadResult_ReportTo_ErrorIssue_SetsExitCode
-**Scenario**: `ConfigurationLoadResult` is returned from `BuildMarkConfigReader` with
-no issues when no configuration file is present.
+**Scenario**: A `ConfigurationLoadResult` containing one `Error`-severity issue is created;
+`ReportTo` is called on a silent `Context`.
-**Expected**: `result.ReportTo(context)` completes without errors; exit code is 0.
+**Expected**: `context.ExitCode` equals 1.
-**Requirement coverage**: `BuildMark-Configuration-ConfigurationLoadResult`
+**Requirement coverage**: `BuildMark-ConfigLoadResult-ReportTo`.
+
+### ConfigurationLoadResult_ReportTo_WarningIssue_DoesNotSetExitCode
+
+**Scenario**: A `ConfigurationLoadResult` containing one `Warning`-severity issue is created;
+`ReportTo` is called on a silent `Context`.
+
+**Expected**: `context.ExitCode` remains 0.
+
+**Requirement coverage**: `BuildMark-ConfigLoadResult-ReportTo`.
+
+### ConfigurationLoadResult_ReportTo_IssueMessage_IncludesLineNumber
+
+**Scenario**: A `ConfigurationLoadResult` containing an `Error`-severity issue at `FilePath`
+`"/repo/.buildmark.yaml"`, `Line` 7, with description `"Unexpected value"` is created; `ReportTo`
+is called.
+
+**Expected**: `result.HasErrors` is true; `context.ExitCode` is 1;
+`result.Issues[0].FilePath` equals `"/repo/.buildmark.yaml"`; `result.Issues[0].Line` equals 7;
+`result.Issues[0].Description` equals `"Unexpected value"`.
+
+**Requirement coverage**: `BuildMark-ConfigLoadResult-ReportTo`.
## Requirements Coverage
-- **BuildMark-Configuration-ConfigurationLoadResult**:
- Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero
+- **`BuildMark-ConfigLoadResult-ReportTo`**:
+ - ConfigurationLoadResult_ReportTo_ErrorIssue_SetsExitCode
+ - ConfigurationLoadResult_ReportTo_WarningIssue_DoesNotSetExitCode
+ - ConfigurationLoadResult_ReportTo_IssueMessage_IncludesLineNumber
diff --git a/docs/verification/build-mark/configuration/configuration.md b/docs/verification/build-mark/configuration/configuration.md
index e6a2197..d8673c2 100644
--- a/docs/verification/build-mark/configuration/configuration.md
+++ b/docs/verification/build-mark/configuration/configuration.md
@@ -2,57 +2,106 @@
## Verification Approach
-The Configuration subsystem is verified at the integration level through
-`ProgramTests.cs` and `RepoConnectorFactoryTests.cs`. There is no dedicated
-`ConfigurationTests.cs` file. The lint-related tests in `ProgramTests.cs` exercise
-`BuildMarkConfigReader` indirectly when `Program.Run` with `Lint = true` calls
-`BuildMarkConfigReader.ReadAsync`. The factory tests in `RepoConnectorFactoryTests.cs`
-exercise connector configuration forwarding.
+The Configuration subsystem is verified with dedicated subsystem tests in
+`ConfigurationSubsystemTests.cs`. Tests create temporary `.buildmark.yaml` files, call
+`BuildMarkConfigReader.ReadAsync`, and assert on the returned `ConfigurationLoadResult`. No mocking
+is required; the real file system is used with temporary directories.
## Dependencies
-| Mock / Stub | Reason |
-| ----------- | --------------------------------------------------------- |
-| File system | Some tests create temporary `.buildmark.yaml` files |
-| `Context` | Provides output capture for configuration issue reporting |
+| Mock / Stub | Reason |
+| ----------- | -------------------------------------------------------------------- |
+| File system | Tests create temporary `.buildmark.yaml` files in `Path.GetTempPath` |
-## Test Scenarios (Integration)
+## Test Scenarios
-### Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero
+### Configuration_ReadAsync_ValidFile_ReturnsConfiguration
-**Scenario**: `Program.Run` is called with `Lint = true` and no `.buildmark.yaml` present.
+**Scenario**: A valid `.buildmark.yaml` containing a GitHub connector, one section, and one rule
+is written to a temp directory; `BuildMarkConfigReader.ReadAsync` is called.
-**Expected**: `BuildMarkConfigReader` returns a default/empty result; exit code is 0.
+**Expected**: Result has no errors; `Config.Connector.Type` is `"github"`;
+`Config.Connector.GitHub.Owner` is `"test-owner"`; one section and one rule are parsed.
-**Requirement coverage**: `BuildMark-Configuration-BuildMarkConfigReader`
+**Requirement coverage**: `BuildMark-Configuration-Read`.
-### RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration
+### Configuration_ReadAsync_MissingFile_ReturnsEmptyResult
-**Scenario**: `RepoConnectorFactory.Create` is called with a `ConnectorConfig` that
-specifies GitHub settings.
+**Scenario**: `BuildMarkConfigReader.ReadAsync` is called on a temp directory containing no
+`.buildmark.yaml`.
-**Expected**: The returned connector has the GitHub configuration applied.
+**Expected**: `Config` is null; `HasErrors` is false; `Issues` is empty.
-**Requirement coverage**: `BuildMark-Configuration-ConnectorConfig`,
-`BuildMark-Configuration-GitHubConnectorConfig`
+**Requirement coverage**: `BuildMark-Configuration-Read`.
-### RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration
+### Configuration_ReadAsync_MalformedFile_ReportsError
-**Scenario**: `RepoConnectorFactory.Create` is called with Azure DevOps connector config.
+**Scenario**: A `.buildmark.yaml` with a tab character (malformed YAML) is written;
+`BuildMarkConfigReader.ReadAsync` is called.
-**Expected**: The returned connector has the Azure DevOps configuration applied.
+**Expected**: `Config` is null; `HasErrors` is true; `Issues` contains at least one entry with
+`Error` severity.
-**Requirement coverage**: `BuildMark-Configuration-ConnectorConfig`,
-`BuildMark-Configuration-AzureDevOpsConnectorConfig`
+**Requirement coverage**: `BuildMark-Configuration-Read`.
+
+### Configuration_Issues_ErrorIssue_SetsExitCode
+
+**Scenario**: A `ConfigurationLoadResult` containing one `Error`-severity `ConfigurationIssue` is
+created; `ReportTo(context)` is called on a silent context.
+
+**Expected**: `context.ExitCode` equals 1.
+
+**Requirement coverage**: `BuildMark-Configuration-Issues`.
+
+### Configuration_Issues_WarningIssue_DoesNotSetExitCode
+
+**Scenario**: A `ConfigurationLoadResult` containing one `Warning`-severity `ConfigurationIssue`
+is created; `ReportTo(context)` is called on a silent context.
+
+**Expected**: `context.ExitCode` remains 0.
+
+**Requirement coverage**: `BuildMark-Configuration-Issues`.
+
+### Configuration_Issues_ValidationError_ReportsAccurateLine
+
+**Scenario**: A `.buildmark.yaml` with an unsupported key on line 3 is written;
+`BuildMarkConfigReader.ReadAsync` is called.
+
+**Expected**: The resulting issue reports `Line` equal to 3.
+
+**Requirement coverage**: `BuildMark-Configuration-Issues`.
+
+### Configuration_ConnectorConfig_ValidFile_ParsesConnectorSettings
+
+**Scenario**: A `.buildmark.yaml` with a GitHub connector block (owner, repo, base-url) is
+written; `BuildMarkConfigReader.ReadAsync` is called.
+
+**Expected**: `Config.Connector.Type` is `"github"`; `GitHub.Owner`, `GitHub.Repo`, and
+`GitHub.BaseUrl` match the file values.
+
+**Requirement coverage**: `BuildMark-Configuration-ConnectorConfig`.
+
+### Configuration_ConnectorConfig_ValidFile_ParsesAzureDevOpsSettings
+
+**Scenario**: A `.buildmark.yaml` with an Azure DevOps connector block (url, organization,
+project, repository) is written; `BuildMarkConfigReader.ReadAsync` is called.
+
+**Expected**: `Config.Connector.Type` is `"azure-devops"`; `AzureDevOps.OrganizationUrl`,
+`AzureDevOps.Organization`, `AzureDevOps.Project`, and `AzureDevOps.Repository` match the file
+values.
+
+**Requirement coverage**: `BuildMark-Configuration-ConnectorConfig`.
## Requirements Coverage
-- **BuildMark-Configuration-BuildMarkConfigReader**:
- Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero
-- **BuildMark-Configuration-ConnectorConfig**:
- RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration,
- RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration
-- **BuildMark-Configuration-GitHubConnectorConfig**:
- RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration
-- **BuildMark-Configuration-AzureDevOpsConnectorConfig**:
- RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration
+- **`BuildMark-Configuration-Read`**:
+ - Configuration_ReadAsync_ValidFile_ReturnsConfiguration
+ - Configuration_ReadAsync_MissingFile_ReturnsEmptyResult
+ - Configuration_ReadAsync_MalformedFile_ReportsError
+- **`BuildMark-Configuration-Issues`**:
+ - Configuration_Issues_ErrorIssue_SetsExitCode
+ - Configuration_Issues_WarningIssue_DoesNotSetExitCode
+ - Configuration_Issues_ValidationError_ReportsAccurateLine
+- **`BuildMark-Configuration-ConnectorConfig`**:
+ - Configuration_ConnectorConfig_ValidFile_ParsesConnectorSettings
+ - Configuration_ConnectorConfig_ValidFile_ParsesAzureDevOpsSettings
diff --git a/docs/verification/build-mark/configuration/connector-config.md b/docs/verification/build-mark/configuration/connector-config.md
index 24f6fc2..aa0953d 100644
--- a/docs/verification/build-mark/configuration/connector-config.md
+++ b/docs/verification/build-mark/configuration/connector-config.md
@@ -2,46 +2,41 @@
## Verification Approach
-`ConnectorConfig` is a data model class verified indirectly through
-`RepoConnectorFactoryTests.cs`. Tests that pass a `ConnectorConfig` to
-`RepoConnectorFactory.Create` exercise the configuration forwarding path and confirm
-that the connector type and sub-configuration are interpreted correctly.
+`ConnectorConfig` is verified through `ConfigurationTests.cs`. Tests write `.buildmark.yaml` files
+with connector blocks and assert that `ConnectorConfig.Type`, `ConnectorConfig.GitHub`, and
+`ConnectorConfig.AzureDevOps` are correctly populated by `BuildMarkConfigReader.ReadAsync`. No
+mocking is required.
## Dependencies
-| Mock / Stub | Reason |
-| ----------- | ---------- |
-| None | Data class |
+| Mock / Stub | Reason |
+| ----------- | -------------------------------------------------------------------- |
+| File system | Tests create temporary `.buildmark.yaml` files in `Path.GetTempPath` |
-## Test Scenarios (Integration)
+## Test Scenarios
-### RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration
+### BuildMarkConfigReader_ReadAsync_ValidFile_ReturnsParsedConfiguration
-**Scenario**: `ConnectorConfig` with GitHub settings is passed to the factory.
+**Scenario**: A `.buildmark.yaml` with a GitHub connector block is written;
+`BuildMarkConfigReader.ReadAsync` is called; `Config.Connector` is inspected.
-**Expected**: Factory creates a GitHub connector with the supplied settings applied.
+**Expected**: `Config.Connector.Type` equals `"github"`; `Config.Connector.GitHub` is non-null;
+`Config.Connector.AzureDevOps` is null.
-**Requirement coverage**: `BuildMark-Configuration-ConnectorConfig`
+**Requirement coverage**: `BuildMark-ConnectorConfig-Properties`.
-### RepoConnectorFactory_Create_WithAzureDevOpsType_CreatesAzureDevOpsConnector
+### BuildMarkConfigReader_ReadAsync_ValidAzureDevOpsConnector_ReturnsParsedConfiguration
-**Scenario**: `ConnectorConfig` with Azure DevOps type is passed to the factory.
+**Scenario**: A `.buildmark.yaml` with an Azure DevOps connector block is written;
+`BuildMarkConfigReader.ReadAsync` is called; `Config.Connector` is inspected.
-**Expected**: Factory creates an Azure DevOps connector.
+**Expected**: `Config.Connector.Type` equals `"azure-devops"`;
+`Config.Connector.AzureDevOps` is non-null with all fields set; `Config.Connector.GitHub` is null.
-**Requirement coverage**: `BuildMark-Configuration-ConnectorConfig`
-
-### RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration
-
-**Scenario**: `ConnectorConfig` with Azure DevOps settings is passed to the factory.
-
-**Expected**: Factory creates Azure DevOps connector with the supplied settings applied.
-
-**Requirement coverage**: `BuildMark-Configuration-ConnectorConfig`
+**Requirement coverage**: `BuildMark-ConnectorConfig-Properties`.
## Requirements Coverage
-- **BuildMark-Configuration-ConnectorConfig**:
- RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration,
- RepoConnectorFactory_Create_WithAzureDevOpsType_CreatesAzureDevOpsConnector,
- RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration
+- **`BuildMark-ConnectorConfig-Properties`**:
+ - BuildMarkConfigReader_ReadAsync_ValidFile_ReturnsParsedConfiguration
+ - BuildMarkConfigReader_ReadAsync_ValidAzureDevOpsConnector_ReturnsParsedConfiguration
diff --git a/docs/verification/build-mark/configuration/git-hub-connector-config.md b/docs/verification/build-mark/configuration/git-hub-connector-config.md
index 64541b7..36af597 100644
--- a/docs/verification/build-mark/configuration/git-hub-connector-config.md
+++ b/docs/verification/build-mark/configuration/git-hub-connector-config.md
@@ -2,28 +2,29 @@
## Verification Approach
-`GitHubConnectorConfig` is a data model class verified indirectly through
-`RepoConnectorFactoryTests.cs`. Tests that supply GitHub-specific configuration
-within a `ConnectorConfig` confirm that the settings are forwarded to the created
-`GitHubRepoConnector`.
+`GitHubConnectorConfig` is verified through `ConfigurationTests.cs`. Tests write `.buildmark.yaml`
+files with a GitHub connector block and assert that `Owner`, `Repo`, and `BaseUrl` properties are
+correctly parsed. No mocking is required.
## Dependencies
-| Mock / Stub | Reason |
-| ----------- | ---------- |
-| None | Data class |
+| Mock / Stub | Reason |
+| ----------- | -------------------------------------------------------------------- |
+| File system | Tests create temporary `.buildmark.yaml` files in `Path.GetTempPath` |
-## Test Scenarios (Integration)
+## Test Scenarios
-### RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration
+### BuildMarkConfigReader_ReadAsync_ValidFile_ReturnsParsedConfiguration
-**Scenario**: A `ConnectorConfig` with a `GitHubConnectorConfig` is passed to the factory.
+**Scenario**: A `.buildmark.yaml` with `github.owner`, `github.repo`, and `github.base-url` fields
+is written; `BuildMarkConfigReader.ReadAsync` is called; `Config.Connector.GitHub` is inspected.
-**Expected**: The resulting connector incorporates the GitHub-specific configuration.
+**Expected**: `Owner` equals `"example-owner"`; `Repo` equals `"hello-world"`; `BaseUrl` equals
+`"https://api.github.com"`.
-**Requirement coverage**: `BuildMark-Configuration-GitHubConnectorConfig`
+**Requirement coverage**: `BuildMark-GitHubConnectorConfig-Properties`.
## Requirements Coverage
-- **BuildMark-Configuration-GitHubConnectorConfig**:
- RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration
+- **`BuildMark-GitHubConnectorConfig-Properties`**:
+ - BuildMarkConfigReader_ReadAsync_ValidFile_ReturnsParsedConfiguration
diff --git a/docs/verification/build-mark/configuration/report-config.md b/docs/verification/build-mark/configuration/report-config.md
index e11a51e..0f84b13 100644
--- a/docs/verification/build-mark/configuration/report-config.md
+++ b/docs/verification/build-mark/configuration/report-config.md
@@ -2,30 +2,41 @@
## Verification Approach
-`ReportConfig` is a data model class verified indirectly through `ProgramTests.cs`.
-Report configuration fields (`File`, `Depth`, `IncludeKnownIssues`) influence the
-behavior of `Program.ProcessBuildNotes`. The test
-`Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues`
-exercises the `IncludeKnownIssues` field path.
+`ReportConfig` is verified through `ConfigurationTests.cs`. Tests write `.buildmark.yaml` files
+with a `report:` block and assert that `File`, `Depth`, and `IncludeKnownIssues` properties are
+correctly parsed or that invalid values produce error issues. No mocking is required.
## Dependencies
-| Mock / Stub | Reason |
-| ----------- | ---------- |
-| None | Data class |
+| Mock / Stub | Reason |
+| ----------- | -------------------------------------------------------------------- |
+| File system | Tests create temporary `.buildmark.yaml` files in `Path.GetTempPath` |
-## Test Scenarios (Integration)
+## Test Scenarios
-### Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues
+### BuildMarkConfigReader_ReadAsync_ValidReportSection_ReturnsParsedReportConfig
-**Scenario**: `ReportConfig.IncludeKnownIssues` is set; report generation includes
-known issues.
+**Scenario**: A `.buildmark.yaml` with `report.file`, `report.depth: 2`, and
+`report.include-known-issues: true` is written; `BuildMarkConfigReader.ReadAsync` is called;
+`Config.Report` is inspected.
-**Expected**: Generated report contains a known issues section.
+**Expected**: `Config.Report.File` equals `"build-notes.md"`; `Config.Report.Depth` equals 2;
+`Config.Report.IncludeKnownIssues` is true.
-**Requirement coverage**: `BuildMark-Configuration-ReportConfig`
+**Requirement coverage**: `BuildMark-ReportConfig-Properties`.
+
+### BuildMarkConfigReader_ReadAsync_InvalidReportDepth_ReturnsErrorIssue
+
+**Scenario**: A `.buildmark.yaml` with `report.depth: -1` is written;
+`BuildMarkConfigReader.ReadAsync` is called.
+
+**Expected**: `Config` is null; `HasErrors` is true; issue description contains
+`"positive integer"`.
+
+**Requirement coverage**: `BuildMark-ReportConfig-Properties`.
## Requirements Coverage
-- **BuildMark-Configuration-ReportConfig**:
- Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues
+- **`BuildMark-ReportConfig-Properties`**:
+ - BuildMarkConfigReader_ReadAsync_ValidReportSection_ReturnsParsedReportConfig
+ - BuildMarkConfigReader_ReadAsync_InvalidReportDepth_ReturnsErrorIssue
diff --git a/docs/verification/build-mark/configuration/rule-config.md b/docs/verification/build-mark/configuration/rule-config.md
index e6b3b69..89e97a1 100644
--- a/docs/verification/build-mark/configuration/rule-config.md
+++ b/docs/verification/build-mark/configuration/rule-config.md
@@ -2,47 +2,32 @@
## Verification Approach
-`RuleConfig` is a data model class verified indirectly through `ItemRouterTests.cs`
-and connector configuration tests. Tests that provide `RuleConfig` instances to
-connectors or to `ItemRouter` exercise the routing logic and confirm that items
-are routed to the correct sections.
+`RuleConfig` is verified through `ConfigurationTests.cs`. The test
+`BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection` calls
+`BuildMarkConfig.CreateDefault()` and asserts on the `Route` and `Match` properties of the
+returned rules. No mocking is required.
## Dependencies
-| Mock / Stub | Reason |
-| ----------- | ---------- |
-| None | Data class |
+| Mock / Stub | Reason |
+| ----------- | --------------- |
+| None | No mocks needed |
-## Test Scenarios (Integration)
+## Test Scenarios
-### ItemRouter_Route_MatchingRuleRoutesItemToConfiguredSection
+### BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection
-**Scenario**: A `RuleConfig` with a matching condition routes an item to the
-configured section.
+**Scenario**: `BuildMarkConfig.CreateDefault()` is called; the returned `Rules` collection is
+inspected.
-**Expected**: Item appears in the correct section of the output.
+**Expected**: Six rules are present; the first three rules have `dependency-updates`,
+`bugs-fixed`, and `bugs-fixed` as routes with appropriate label and work-item-type match
+conditions; the fourth and fifth rules have `suppressed` routes; the sixth rule has `changes` as
+route with a null match.
-**Requirement coverage**: `BuildMark-Configuration-RuleConfig`
-
-### ItemRouter_Route_SuppressedRouteOmitsMatchingItem
-
-**Scenario**: A `RuleConfig` with suppress set routes a matching item to be omitted.
-
-**Expected**: Item does not appear in any section of the output.
-
-**Requirement coverage**: `BuildMark-Configuration-RuleConfig`
-
-### MockRepoConnector_Configure_StoresRulesAndSections
-
-**Scenario**: `MockRepoConnector.Configure` is called with `RuleConfig` entries.
-
-**Expected**: Rules are stored; `HasRules` returns `true`.
-
-**Requirement coverage**: `BuildMark-Configuration-RuleConfig`
+**Requirement coverage**: `BuildMark-RuleConfig-Properties`.
## Requirements Coverage
-- **BuildMark-Configuration-RuleConfig**:
- ItemRouter_Route_MatchingRuleRoutesItemToConfiguredSection,
- ItemRouter_Route_SuppressedRouteOmitsMatchingItem,
- MockRepoConnector_Configure_StoresRulesAndSections
+- **`BuildMark-RuleConfig-Properties`**:
+ - BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection
diff --git a/docs/verification/build-mark/configuration/section-config.md b/docs/verification/build-mark/configuration/section-config.md
index c1b49a2..36afae8 100644
--- a/docs/verification/build-mark/configuration/section-config.md
+++ b/docs/verification/build-mark/configuration/section-config.md
@@ -2,40 +2,31 @@
## Verification Approach
-`SectionConfig` is a data model class verified indirectly through
-`RepoConnectorsTests.cs` and connector-specific tests. When `Configure` is called
-on a connector with `SectionConfig` entries, the connector creates named sections in
-the `BuildInformation.RoutedSections` output. Tests that assert on routed sections
-exercise `SectionConfig` indirectly.
+`SectionConfig` is verified through `ConfigurationTests.cs`. The test
+`BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection` calls
+`BuildMarkConfig.CreateDefault()` and asserts on the `Id` and `Title` properties of the returned
+sections. No mocking is required.
## Dependencies
-| Mock / Stub | Reason |
-| ----------- | ---------- |
-| None | Data class |
+| Mock / Stub | Reason |
+| ----------- | --------------- |
+| None | No mocks needed |
-## Test Scenarios (Integration)
+## Test Scenarios
-### RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation
+### BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection
-**Scenario**: A connector configured with `SectionConfig` entries processes items and
-populates `RoutedSections`.
+**Scenario**: `BuildMarkConfig.CreateDefault()` is called; the returned `Sections` collection is
+inspected.
-**Expected**: Routed sections are present in the result as configured.
+**Expected**: Three sections are present with ids `"changes"`, `"bugs-fixed"`, and
+`"dependency-updates"` and corresponding titles `"Changes"`, `"Bugs Fixed"`, and
+`"Dependency Updates"`.
-**Requirement coverage**: `BuildMark-Configuration-SectionConfig`
-
-### MockRepoConnector_GetBuildInformationAsync_WithRules_ReturnsRoutedSections
-
-**Scenario**: `MockRepoConnector` is configured with sections; `GetBuildInformationAsync`
-populates routed sections.
-
-**Expected**: `RoutedSections` contains the configured section names.
-
-**Requirement coverage**: `BuildMark-Configuration-SectionConfig`
+**Requirement coverage**: `BuildMark-SectionConfig-Properties`.
## Requirements Coverage
-- **BuildMark-Configuration-SectionConfig**:
- RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation,
- MockRepoConnector_GetBuildInformationAsync_WithRules_ReturnsRoutedSections
+- **`BuildMark-SectionConfig-Properties`**:
+ - BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection
diff --git a/docs/verification/build-mark/program.md b/docs/verification/build-mark/program.md
index 8322304..c3d7f78 100644
--- a/docs/verification/build-mark/program.md
+++ b/docs/verification/build-mark/program.md
@@ -86,15 +86,53 @@ to avoid live API calls where needed.
**Expected**: Error is written to stderr; exit code is 1.
-**Requirement coverage**: `BuildMark-Program-ErrorHandling`
-
-### Program_Run_ConnectorThrowsInvalidOperationException_WritesErrorAndSetsExitCode
+**Requirement coverage**: `BuildMark-Program-ErrorHandling-InvalidBuildVersion`
**Scenario**: `Program.Run` is called but connector factory throws `InvalidOperationException`.
**Expected**: Error is written to stderr; exit code is 1.
-**Requirement coverage**: `BuildMark-Program-ErrorHandling`
+**Requirement coverage**: `BuildMark-Program-ErrorHandling-ConnectorFailure`
+
+### Program_Run_WithSilentFlag_SuppressesOutput
+
+**Scenario**: `Program.Run` is called with `--silent` and `--help` flags in context.
+
+**Expected**: Banner text is suppressed from console output; exit code is 0.
+
+**Requirement coverage**: `BuildMark-Program-Silent`
+
+### Program_Run_WithLogFlag_WritesToLogFile
+
+**Scenario**: `Program.Run` is called with `--log ` flag pointing to a temporary file.
+
+**Expected**: Log file is created and contains non-empty output; exit code is 0.
+
+**Requirement coverage**: `BuildMark-Program-Log`
+
+### Program_Run_WithResultsFlag_WritesResultsFile
+
+**Scenario**: `Program.Run` is called with `--validate` and `--results ` flags.
+
+**Expected**: Results file is created; exit code is 0.
+
+**Requirement coverage**: `BuildMark-Program-Results`
+
+### Program_Run_WithBuildVersionFlag_AcceptsBuildVersion
+
+**Scenario**: `Program.Run` is called with `--build-version 3.2.1` and a mock connector.
+
+**Expected**: Report is generated containing the specified version string; exit code is 0.
+
+**Requirement coverage**: `BuildMark-Program-BuildVersion`
+
+### Program_Run_WithDepthFlag_SetsHeadingDepth
+
+**Scenario**: `Program.Run` is called with `--depth 3` and a mock connector.
+
+**Expected**: Report uses level-three headings (`###`) for the title; exit code is 0.
+
+**Requirement coverage**: `BuildMark-Program-Depth`
## Requirements Coverage
@@ -105,5 +143,10 @@ to avoid live API calls where needed.
- **BuildMark-Program-Validate**: Program_Run_ValidateFlag_OutputsValidationMessage
- **BuildMark-Program-Report**: Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues
- **BuildMark-Program-Lint**: Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero
-- **BuildMark-Program-ErrorHandling**: Program_Run_InvalidBuildVersion_WritesErrorAndSetsExitCode,
- Program_Run_ConnectorThrowsInvalidOperationException_WritesErrorAndSetsExitCode
+- **BuildMark-Program-Silent**: Program_Run_WithSilentFlag_SuppressesOutput
+- **BuildMark-Program-Log**: Program_Run_WithLogFlag_WritesToLogFile
+- **BuildMark-Program-Results**: Program_Run_WithResultsFlag_WritesResultsFile
+- **BuildMark-Program-BuildVersion**: Program_Run_WithBuildVersionFlag_AcceptsBuildVersion
+- **BuildMark-Program-Depth**: Program_Run_WithDepthFlag_SetsHeadingDepth
+- **BuildMark-Program-ErrorHandling-InvalidBuildVersion**: Program_Run_InvalidBuildVersion_WritesErrorAndSetsExitCode
+- **BuildMark-Program-ErrorHandling-ConnectorFailure**: Program_Run_ConnectorThrowsInvalidOperationException_WritesErrorAndSetsExitCode
diff --git a/docs/verification/build-mark/repo-connectors/github/git-hub-repo-connector.md b/docs/verification/build-mark/repo-connectors/github/git-hub-repo-connector.md
index 0ac83ea..71e819e 100644
--- a/docs/verification/build-mark/repo-connectors/github/git-hub-repo-connector.md
+++ b/docs/verification/build-mark/repo-connectors/github/git-hub-repo-connector.md
@@ -28,7 +28,9 @@ and edge cases such as duplicate commit SHAs and substring label matching.
**Scenario**: `GitHubRepoConnector` is constructed with a `GitHubConnectorConfig`.
-**Expected**: Configuration overrides (e.g., owner, repo) are stored.
+**Expected**: `ConfigurationOverrides.Owner` equals `"example-owner"`;
+`ConfigurationOverrides.Repo` equals `"example-repo"`; `ConfigurationOverrides.BaseUrl`
+equals `"https://api.github.com"`.
**Requirement coverage**: `BuildMark-RepoConnectors-GitHub`
diff --git a/docs/verification/build-mark/self-test/self-test.md b/docs/verification/build-mark/self-test/self-test.md
index b4165e3..a097310 100644
--- a/docs/verification/build-mark/self-test/self-test.md
+++ b/docs/verification/build-mark/self-test/self-test.md
@@ -2,29 +2,71 @@
## Verification Approach
-The SelfTest subsystem is verified through the `Program_Run_ValidateFlag_OutputsValidationMessage`
-test in `ProgramTests.cs` and through the CI pipeline integration test step
-`Run self-validation`. Both confirm that the `Validation.Run` method can be invoked
-without errors and produces output, providing evidence that all internal self-checks
-pass in the test environment.
+The SelfTest subsystem is verified with dedicated subsystem tests in `SelfTestTests.cs`. Tests
+invoke `Validation.Run` through a `Context` constructed with controlled argument arrays, then
+inspect created results files and log output. `MockRepoConnector` is used so that no real
+repository access is needed.
## Dependencies
-| Mock / Stub | Reason |
-| ----------- | -------------------------------------------------------- |
-| `Context` | Provides output capture for validation message assertion |
+| Mock / Stub | Reason |
+| --- | --- |
+| `MockRepoConnector` | Provides a real connector factory that does not require external network access. |
-## Test Scenarios (Integration)
+## Test Scenarios
-### Program_Run_ValidateFlag_OutputsValidationMessage
+### SelfTest_Validation_WithTrxFile_WritesResults
-**Scenario**: `Program.Run` is called with `Validate = true`.
+**Scenario**: `Validation.Run` is called with `--validate --results .trx --silent`; the
+results file path ends in `.trx`.
-**Expected**: Validation framework runs; output is written; exit code is 0.
+**Expected**: A `.trx` file is created; its content contains `TestRun` and
+`BuildMark Self-Validation`.
-**Requirement coverage**: `BuildMark-SelfTest-Validation`, `BuildMark-Program-Validate`
+**Requirement coverage**: `BuildMark-SelfTest-Qualification`.
+
+### SelfTest_Validation_WithXmlFile_WritesResults
+
+**Scenario**: `Validation.Run` is called with `--validate --results .xml --silent`; the
+results file path ends in `.xml`.
+
+**Expected**: An `.xml` file is created; its content contains `testsuites` and
+`BuildMark Self-Validation`.
+
+**Requirement coverage**: `BuildMark-SelfTest-Qualification`.
+
+### SelfTest_ResultsOutput_WithTrxFile_CreatesFile
+
+**Scenario**: `Validation.Run` is called with `--validate --results output.trx --silent`.
+
+**Expected**: The `.trx` results file is created and has a non-zero file size.
+
+**Requirement coverage**: `BuildMark-SelfTest-ResultsOutput`.
+
+### SelfTest_ResultsOutput_WithXmlFile_CreatesFile
+
+**Scenario**: `Validation.Run` is called with `--validate --results output.xml --silent`.
+
+**Expected**: The `.xml` results file is created and has a non-zero file size.
+
+**Requirement coverage**: `BuildMark-SelfTest-ResultsOutput`.
+
+### SelfTest_Qualification_WithoutResultsFile_Succeeds
+
+**Scenario**: `Validation.Run` is called with `--validate --log /validation.log --silent`; no
+`--results` argument is supplied.
+
+**Expected**: Validation completes without error; the log file is created; its content contains
+`BuildMark Self-validation` and `Total Tests:`.
+
+**Requirement coverage**: `BuildMark-SelfTest-Qualification`.
## Requirements Coverage
-- **BuildMark-SelfTest-Validation**: Program_Run_ValidateFlag_OutputsValidationMessage
-- **BuildMark-Program-Validate**: Program_Run_ValidateFlag_OutputsValidationMessage
+- **`BuildMark-SelfTest-Qualification`**:
+ - SelfTest_Validation_WithTrxFile_WritesResults
+ - SelfTest_Validation_WithXmlFile_WritesResults
+ - SelfTest_Qualification_WithoutResultsFile_Succeeds
+- **`BuildMark-SelfTest-ResultsOutput`**:
+ - SelfTest_ResultsOutput_WithTrxFile_CreatesFile
+ - SelfTest_ResultsOutput_WithXmlFile_CreatesFile
diff --git a/docs/verification/build-mark/self-test/validation.md b/docs/verification/build-mark/self-test/validation.md
index c6f53bd..677cc98 100644
--- a/docs/verification/build-mark/self-test/validation.md
+++ b/docs/verification/build-mark/self-test/validation.md
@@ -2,33 +2,76 @@
## Verification Approach
-`Validation` is tested indirectly through `ProgramTests.cs`. The test
-`Program_Run_ValidateFlag_OutputsValidationMessage` invokes `Program.Run` with
-`Validate = true`, which delegates to `Validation.Run`. Successful completion without
-exception or non-zero exit code constitutes evidence that the validation framework
-is operational.
-
-The CI pipeline integration test `Run self-validation` also runs
-`buildmark --validate --results artifacts/validation-*.trx` on each operating
-system and .NET runtime combination, providing platform-level evidence.
+`Validation` is verified with dedicated unit tests in `ValidationTests.cs`. Tests construct a
+`Context` with controlled arguments, call `Validation.Run`, and assert on the created results
+files, console error output, and exit code. `MockRepoConnector` is provided as the connector
+factory; no further mocking is required.
## Dependencies
-| Mock / Stub | Reason |
-| ----------- | ----------------------------------------------------- |
-| `Context` | Provides output stream for validation result messages |
+| Mock / Stub | Reason |
+| ------------------- | --------------------------------------------------------------------- |
+| `MockRepoConnector` | Supplies connector factory so self-check runs without network access. |
## Test Scenarios
-### Program_Run_ValidateFlag_OutputsValidationMessage
+### Validation_Run_WithTrxResultsFile_WritesTrxFile
+
+**Scenario**: `Validation.Run` is called with `--validate --results /results.trx`; console
+output is captured.
+
+**Expected**: `results.trx` is created; its content contains `TestRun` and
+`BuildMark Self-Validation`.
+
+**Requirement coverage**: `BuildMark-Validation-Run`, `BuildMark-Validation-TrxOutput`.
+
+### Validation_Run_WithXmlResultsFile_WritesJUnitFile
+
+**Scenario**: `Validation.Run` is called with `--validate --results /results.xml`; console
+output is captured.
+
+**Expected**: `results.xml` is created; its content contains `testsuites` and
+`BuildMark Self-Validation`.
+
+**Requirement coverage**: `BuildMark-Validation-Run`, `BuildMark-Validation-JUnitOutput`.
+
+### Validation_Run_WithUnsupportedResultsFileExtension_ShowsError
+
+**Scenario**: `Validation.Run` is called with `--validate --results /results.json`
+(unsupported extension); console error is captured.
+
+**Expected**: Console error contains `"Unsupported results file format"`; exit code is 1.
+
+**Requirement coverage**: `BuildMark-Validation-TrxOutput`, `BuildMark-Validation-JUnitOutput`.
+
+### Validation_Run_WithInvalidResultsFilePath_ShowsError
+
+**Scenario**: `Validation.Run` is called with
+`--validate --results /invalid_path_that_does_not_exist/results.trx`; console error is captured.
+
+**Expected**: Console error contains `"Failed to write results file"`; exit code is 1.
+
+**Requirement coverage**: `BuildMark-Validation-TrxOutput`.
+
+### Validation_Run_WithoutResultsFile_CompletesSuccessfully
-**Scenario**: `Program.Run` is called with `Validate = true`; control reaches
-`Validation.Run`.
+**Scenario**: `Validation.Run` is called with `--validate --silent`; no `--results` argument is
+supplied.
-**Expected**: Validation output is written to the context output; exit code is 0.
+**Expected**: Validation completes without error; exit code is 0.
-**Requirement coverage**: `BuildMark-SelfTest-Validation`
+**Requirement coverage**: `BuildMark-Validation-Run`.
## Requirements Coverage
-- **BuildMark-SelfTest-Validation**: Program_Run_ValidateFlag_OutputsValidationMessage
+- **`BuildMark-Validation-Run`**:
+ - Validation_Run_WithTrxResultsFile_WritesTrxFile
+ - Validation_Run_WithXmlResultsFile_WritesJUnitFile
+ - Validation_Run_WithoutResultsFile_CompletesSuccessfully
+- **`BuildMark-Validation-TrxOutput`**:
+ - Validation_Run_WithTrxResultsFile_WritesTrxFile
+ - Validation_Run_WithUnsupportedResultsFileExtension_ShowsError
+ - Validation_Run_WithInvalidResultsFilePath_ShowsError
+- **`BuildMark-Validation-JUnitOutput`**:
+ - Validation_Run_WithXmlResultsFile_WritesJUnitFile
+ - Validation_Run_WithUnsupportedResultsFileExtension_ShowsError
diff --git a/docs/verification/build-mark/version/version-semantic.md b/docs/verification/build-mark/version/version-semantic.md
index 62bd147..5cb37ef 100644
--- a/docs/verification/build-mark/version/version-semantic.md
+++ b/docs/verification/build-mark/version/version-semantic.md
@@ -16,36 +16,36 @@ to the underlying `VersionComparable`, string formatting, and comparison.
### VersionSemantic_Create_WithBuildMetadata_ReturnsInstance
-**Scenario**: `VersionSemantic.Create` is called with a version string that includes
-build metadata (the `+` suffix).
+**Scenario**: `VersionSemantic.Create` is called with `"1.2.3+build.123"`.
-**Expected**: Returns a non-null instance with metadata set.
+**Expected**: `Metadata` equals `"build.123"`; `FullVersion` equals `"1.2.3+build.123"`.
**Requirement coverage**: `BuildMark-Version-VersionSemantic`
### VersionSemantic_Create_WithoutBuildMetadata_ReturnsInstance
-**Scenario**: `VersionSemantic.Create` is called with a version string that has no
-build metadata.
+**Scenario**: `VersionSemantic.Create` is called with `"1.2.3"`.
-**Expected**: Returns a non-null instance with metadata as empty string.
+**Expected**: `Metadata` is `null`; `FullVersion` equals `"1.2.3"`.
**Requirement coverage**: `BuildMark-Version-VersionSemantic`
### VersionSemantic_Properties_DelegateToComparable_Correctly
-**Scenario**: Major, minor, patch, and pre-release properties are accessed on a
-`VersionSemantic` instance.
+**Scenario**: `VersionSemantic.Create` is called with `"1.2.3-alpha"` and the `Major`,
+`Minor`, `Patch`, `PreRelease`, and `CompareVersion` properties are read.
-**Expected**: Values match those of the underlying `VersionComparable`.
+**Expected**: `Major` equals 1; `Minor` equals 2; `Patch` equals 3; `PreRelease` equals
+`"alpha"`; `CompareVersion` equals `"1.2.3-alpha"`.
**Requirement coverage**: `BuildMark-Version-VersionSemantic`
### VersionSemantic_ToString_FormatsCompletely_WithAllComponents
-**Scenario**: `ToString` is called on a `VersionSemantic` with pre-release and metadata.
+**Scenario**: `VersionSemantic.Create` is called with `"1.2.3-alpha+build.123"` and
+`FullVersion` is read.
-**Expected**: Returns a string in `major.minor.patch-pre+meta` format.
+**Expected**: `FullVersion` equals `"1.2.3-alpha+build.123"`.
**Requirement coverage**: `BuildMark-Version-VersionSemantic`
@@ -67,31 +67,37 @@ build metadata.
### VersionSemantic_Create_SimpleVersion_ParsesVersion
-**Scenario**: `VersionSemantic.Create` is called with a simple `major.minor.patch`.
+**Scenario**: `VersionSemantic.Create` is called with `"1.2.3"`.
-**Expected**: Instance created with zero pre-release and metadata.
+**Expected**: `Major` equals 1; `Minor` equals 2; `Patch` equals 3; `Numbers` equals
+`"1.2.3"`; `PreRelease` equals `""`; `Metadata` is `null`; `CompareVersion` equals
+`"1.2.3"`; `FullVersion` equals `"1.2.3"`; `IsPreRelease` is false.
**Requirement coverage**: `BuildMark-Version-VersionSemantic`
### VersionSemantic_Create_VersionWithMetadata_ParsesVersion
-**Scenario**: `VersionSemantic.Create` is called with version plus build metadata.
+**Scenario**: `VersionSemantic.Create` is called with `"1.2.3+build.5"`.
-**Expected**: Metadata property returns the metadata string.
+**Expected**: `Numbers` equals `"1.2.3"`; `PreRelease` equals `""`; `Metadata` equals
+`"build.5"`; `CompareVersion` equals `"1.2.3"`; `FullVersion` equals `"1.2.3+build.5"`;
+`IsPreRelease` is false.
**Requirement coverage**: `BuildMark-Version-VersionSemantic`
### VersionSemantic_Create_PreReleaseWithMetadata_ParsesVersion
-**Scenario**: `VersionSemantic.Create` is called with pre-release and metadata.
+**Scenario**: `VersionSemantic.Create` is called with `"2.0.0-alpha.1+linux.x64"`.
-**Expected**: Both pre-release and metadata properties are set correctly.
+**Expected**: `Numbers` equals `"2.0.0"`; `PreRelease` equals `"alpha.1"`; `Metadata`
+equals `"linux.x64"`; `CompareVersion` equals `"2.0.0-alpha.1"`; `FullVersion` equals
+`"2.0.0-alpha.1+linux.x64"`; `IsPreRelease` is true.
**Requirement coverage**: `BuildMark-Version-VersionSemantic`
### VersionSemantic_TryCreate_InvalidVersion_ReturnsNull
-**Scenario**: `VersionSemantic.TryCreate` is called with an invalid string.
+**Scenario**: `VersionSemantic.TryCreate` is called with `"not-a-version"`.
**Expected**: Returns `null`.
@@ -99,17 +105,19 @@ build metadata.
### VersionSemantic_Create_InvalidVersion_ThrowsArgumentException
-**Scenario**: `VersionSemantic.Create` is called with an invalid string.
+**Scenario**: `VersionSemantic.Create` is called with `"not-a-version"`.
-**Expected**: `ArgumentException` is thrown.
+**Expected**: `ArgumentException` is thrown with a message containing
+`"does not match semantic version format"`.
**Requirement coverage**: `BuildMark-Version-VersionSemantic`
### VersionSemantic_Comparable_AllowsComparison
-**Scenario**: Two `VersionSemantic` instances are compared using comparison operators.
+**Scenario**: `Comparable` is compared for `"1.2.3+build1"` and `"1.2.4+build2"`.
-**Expected**: Comparison delegates to the underlying `VersionComparable` correctly.
+**Expected**: `version1.Comparable < version2.Comparable` is true (i.e., `"1.2.3"` sorts
+before `"1.2.4"`).
**Requirement coverage**: `BuildMark-Version-VersionSemantic`
diff --git a/docs/verification/build-mark/version/version-tag.md b/docs/verification/build-mark/version/version-tag.md
index 5491980..8264678 100644
--- a/docs/verification/build-mark/version/version-tag.md
+++ b/docs/verification/build-mark/version/version-tag.md
@@ -25,49 +25,52 @@ and error handling for invalid tags.
### VersionTag_Create_StandardTag_ParsesCorrectly
-**Scenario**: `VersionTag.Create` is called with a standard `v1.2.3` tag.
+**Scenario**: `VersionTag.Create` is called with `"1.2.3"` (no prefix).
-**Expected**: Version components are parsed correctly.
+**Expected**: `Tag` equals `"1.2.3"`; `FullVersion` equals `"1.2.3"`; `Numbers` equals `"1.2.3"`.
**Requirement coverage**: `BuildMark-Version-VersionTag`
### VersionTag_Create_PrefixedTag_ParsesCorrectly
-**Scenario**: `VersionTag.Create` is called with a prefixed tag like `v1.2.3`.
+**Scenario**: `VersionTag.Create` is called with `"v1.2.3"`.
-**Expected**: `v` prefix is stripped; version parses correctly.
+**Expected**: `Tag` equals `"v1.2.3"`; `FullVersion` equals `"1.2.3"`; `Numbers` equals `"1.2.3"`.
**Requirement coverage**: `BuildMark-Version-VersionTag`
### VersionTag_Create_DotSeparatedPreRelease_NormalizesToHyphen
-**Scenario**: `VersionTag.Create` is called with a tag using dots in the pre-release
-segment.
+**Scenario**: `VersionTag.Create` is called with `"v1.2.3.alpha.1"`.
-**Expected**: Dots in the pre-release are normalized to hyphens.
+**Expected**: `Tag` equals `"v1.2.3.alpha.1"`; `FullVersion` equals `"1.2.3-alpha.1"`;
+`PreRelease` equals `"alpha.1"`.
**Requirement coverage**: `BuildMark-Version-VersionTag`
### VersionTag_Create_ComplexTag_ExtractsVersionCorrectly
-**Scenario**: `VersionTag.Create` is called with a complex tag containing prefix,
-version, pre-release, and build metadata.
+**Scenario**: `VersionTag.Create` is called with `"Release_1.2.3.beta.5+build.123"`.
-**Expected**: All components are extracted correctly.
+**Expected**: `Tag` equals `"Release_1.2.3.beta.5+build.123"`; `FullVersion` equals
+`"1.2.3-beta.5+build.123"`; `Numbers` equals `"1.2.3"`; `PreRelease` equals `"beta.5"`;
+`Metadata` equals `"build.123"`.
**Requirement coverage**: `BuildMark-Version-VersionTag`
### VersionTag_Properties_ExposeOriginalAndParsed_Correctly
-**Scenario**: `Tag` and `Semantic` properties are accessed after creating a tag.
+**Scenario**: `VersionTag.Create` is called with `"v1.2.3-alpha"` and the `Tag`,
+`FullVersion`, `Numbers`, and `PreRelease` properties are read.
-**Expected**: `Tag` returns the original string; `Semantic` returns the parsed version.
+**Expected**: `Tag` equals `"v1.2.3-alpha"`; `FullVersion` equals `"1.2.3-alpha"`;
+`Numbers` equals `"1.2.3"`; `PreRelease` equals `"alpha"`.
**Requirement coverage**: `BuildMark-Version-VersionTag`
### VersionTag_ToString_ReturnsOriginalTag
-**Scenario**: `ToString` is called on a `VersionTag` instance.
+**Scenario**: `ToString` is called on a `VersionTag` created from `"v1.2.3-alpha+build.123"`.
**Expected**: Returns the original tag string unchanged.
@@ -75,23 +78,28 @@ version, pre-release, and build metadata.
### VersionTag_Create_SimpleVPrefix_ParsesVersion
-**Scenario**: `VersionTag.Create` is called with `v1.0.0`.
+**Scenario**: `VersionTag.Create` is called with `"v1.2.3"`.
-**Expected**: Instance created with version `1.0.0`.
+**Expected**: `Tag` equals `"v1.2.3"`; `FullVersion` equals `"1.2.3"`; `Numbers` equals
+`"1.2.3"`; `PreRelease` equals `""`; `CompareVersion` equals `"1.2.3"`; `Metadata` equals
+`""`; `IsPreRelease` is false.
**Requirement coverage**: `BuildMark-Version-VersionTag`
### VersionTag_Create_ComplexVersionWithMetadata_ParsesVersion
-**Scenario**: `VersionTag.Create` is called with a tag including build metadata.
+**Scenario**: `VersionTag.Create` is called with `"Rel_1.2.3.rc.4+build.5"`.
-**Expected**: Metadata is preserved in the `Semantic` property.
+**Expected**: `Tag` equals `"Rel_1.2.3.rc.4+build.5"`; `FullVersion` equals
+`"1.2.3-rc.4+build.5"`; `Numbers` equals `"1.2.3"`; `PreRelease` equals `"rc.4"`;
+`CompareVersion` equals `"1.2.3-rc.4"`; `Metadata` equals `"build.5"`; `IsPreRelease`
+is true.
**Requirement coverage**: `BuildMark-Version-VersionTag`
### VersionTag_TryCreate_InvalidTag_ReturnsNull
-**Scenario**: `VersionTag.TryCreate` is called with an unparseable tag string.
+**Scenario**: `VersionTag.TryCreate` is called with `"not-a-version"`.
**Expected**: Returns `null`.
@@ -99,41 +107,45 @@ version, pre-release, and build metadata.
### VersionTag_Create_InvalidTag_ThrowsArgumentException
-**Scenario**: `VersionTag.Create` is called with an unparseable tag string.
+**Scenario**: `VersionTag.Create` is called with `"not-a-version"`.
-**Expected**: `ArgumentException` is thrown.
+**Expected**: `ArgumentException` is thrown with a message containing
+`"does not match version format"`.
**Requirement coverage**: `BuildMark-Version-VersionTag`
### VersionTag_Create_NoPrefix_ParsesVersion
-**Scenario**: `VersionTag.Create` is called with a tag that has no alphabetic prefix.
+**Scenario**: `VersionTag.Create` is called with `"1.0.0"`.
-**Expected**: Version is parsed directly from the string.
+**Expected**: `Tag` equals `"1.0.0"`; `FullVersion` equals `"1.0.0"`; `IsPreRelease` is false.
**Requirement coverage**: `BuildMark-Version-VersionTag`
### VersionTag_Create_HyphenPreReleaseWithMetadata_ParsesVersion
-**Scenario**: `VersionTag.Create` is called with a tag using hyphen pre-release and
-build metadata.
+**Scenario**: `VersionTag.Create` is called with `"Rel_1.2.3-rc.4+build.5"`.
-**Expected**: Both pre-release and metadata are parsed correctly.
+**Expected**: `Tag` equals `"Rel_1.2.3-rc.4+build.5"`; `FullVersion` equals
+`"1.2.3-rc.4+build.5"`; `Numbers` equals `"1.2.3"`; `PreRelease` equals `"rc.4"`;
+`CompareVersion` equals `"1.2.3-rc.4"`; `Metadata` equals `"build.5"`; `IsPreRelease`
+is true.
**Requirement coverage**: `BuildMark-Version-VersionTag`
### VersionTag_Semantic_AllowsComparison
-**Scenario**: `Semantic` property is used to compare two tags.
+**Scenario**: `Semantic.Comparable` is compared for tags `"v1.2.3"` and `"v1.11.2"`.
-**Expected**: Comparison behaves correctly via the underlying `VersionSemantic`.
+**Expected**: `tag1.Semantic.Comparable < tag2.Semantic.Comparable` is true (i.e.,
+`"v1.2.3"` sorts before `"v1.11.2"`).
**Requirement coverage**: `BuildMark-Version-VersionTag`
### VersionComparable_Equals_DifferentPrefixesSameVersion_ReturnsTrue
-**Scenario**: Two tags with different prefixes but the same version are compared
-via `VersionComparable`.
+**Scenario**: `Semantic.Comparable` is extracted from `"v1.2.3"`, `"VER1.2.3"`,
+`"Release_1.2.3"`, and `"release/1.2.3"` and the four values are compared for equality.
**Expected**: Comparable values are equal.
@@ -141,34 +153,40 @@ via `VersionComparable`.
### VersionTag_GetVersionComparable_SemanticTags_ReturnsCorrectComparison
-**Scenario**: `VersionComparable` extracted from two semantic tags is compared.
+**Scenario**: `Semantic.Comparable` is compared for `"v1.0.0-alpha"`, `"v1.0.0-beta"`,
+and `"v1.0.0"`.
-**Expected**: Returns the correct ordering.
+**Expected**: alpha < beta < release: `"v1.0.0-alpha"` sorts before `"v1.0.0-beta"`,
+which sorts before `"v1.0.0"`.
**Requirement coverage**: `BuildMark-Version-VersionTag`
### VersionTag_Create_PathSeparatorPrefix_ParsesCorrectly
-**Scenario**: `VersionTag.Create` is called with a tag in `prefix/1.2.3` format.
+**Scenario**: `VersionTag.Create` is called with `"release/1.2.3"`.
-**Expected**: Path prefix is stripped; version is parsed correctly.
+**Expected**: `Tag` equals `"release/1.2.3"`; `FullVersion` equals `"1.2.3"`; `Numbers`
+equals `"1.2.3"`; `PreRelease` equals `""`; `IsPreRelease` is false.
**Requirement coverage**: `BuildMark-Version-VersionTag`
### VersionTag_Create_PathSeparatorPrefixWithPreRelease_ParsesCorrectly
-**Scenario**: `VersionTag.Create` is called with `prefix/1.2.3-alpha`.
+**Scenario**: `VersionTag.Create` is called with `"release/1.2.3-rc.4"`.
-**Expected**: Path prefix is stripped; pre-release is parsed correctly.
+**Expected**: `Tag` equals `"release/1.2.3-rc.4"`; `FullVersion` equals `"1.2.3-rc.4"`;
+`Numbers` equals `"1.2.3"`; `PreRelease` equals `"rc.4"`; `CompareVersion` equals
+`"1.2.3-rc.4"`; `IsPreRelease` is true.
**Requirement coverage**: `BuildMark-Version-VersionTag`
### VersionTag_Create_MultiLevelPathPrefix_ParsesCorrectly
-**Scenario**: `VersionTag.Create` is called with a multi-level path prefix like
-`a/b/1.2.3`.
+**Scenario**: `VersionTag.Create` is called with `"builds/release/1.2.3-beta.1+build.99"`.
-**Expected**: All prefix segments are stripped; version parses correctly.
+**Expected**: `Tag` equals `"builds/release/1.2.3-beta.1+build.99"`; `FullVersion` equals
+`"1.2.3-beta.1+build.99"`; `Numbers` equals `"1.2.3"`; `PreRelease` equals `"beta.1"`;
+`Metadata` equals `"build.99"`; `IsPreRelease` is true.
**Requirement coverage**: `BuildMark-Version-VersionTag`
diff --git a/docs/verification/introduction.md b/docs/verification/introduction.md
index ddc07be..ee69353 100644
--- a/docs/verification/introduction.md
+++ b/docs/verification/introduction.md
@@ -139,4 +139,4 @@ match identifiers defined in the ReqStream YAML files under `docs/reqstream/`.
- See the *BuildMark Software Design* document for implementation details of each unit.
- See the *BuildMark Requirements* document for the full requirements specification.
-- [BuildMark Repository](https://github.com/demaconsulting/BuildMark)
+- See the BuildMark repository at .
diff --git a/src/DemaConsulting.BuildMark/Program.cs b/src/DemaConsulting.BuildMark/Program.cs
index 71d493d..0244ba3 100644
--- a/src/DemaConsulting.BuildMark/Program.cs
+++ b/src/DemaConsulting.BuildMark/Program.cs
@@ -36,6 +36,17 @@ internal static class Program
///
/// Gets the application version string.
///
+ ///
+ /// The version is resolved using the following fallback chain:
+ ///
+ /// - — preferred because it includes
+ /// pre-release labels and build metadata (e.g., 1.2.3-beta.1+abc123).
+ /// - assembly.GetName().Version — numeric fallback when the informational
+ /// attribute is absent.
+ /// - "0.0.0" — final safety net for test assemblies or stripped builds that
+ /// carry neither attribute.
+ ///
+ ///
public static string Version
{
get
@@ -99,6 +110,19 @@ private static int Main(string[] args)
/// Runs the program logic based on the provided context.
///
/// The context containing command line arguments and program state.
+ ///
+ ///
+ /// This method is to serve as the test injection point.
+ /// Unit and integration tests construct a pre-configured and
+ /// call Run directly, bypassing the argument-parsing step in Main and
+ /// allowing individual code paths to be exercised in isolation.
+ ///
+ ///
+ /// As a side effect, the method mutates — callers
+ /// must read context.ExitCode after Run returns to determine success
+ /// or failure; the method itself has no return value.
+ ///
+ ///
public static void Run(Context context)
{
// Priority 1: Version query
@@ -141,6 +165,12 @@ public static void Run(Context context)
/// Prints the application banner.
///
/// The context for output.
+ ///
+ /// Called unconditionally after the version-flag check in the dispatch
+ /// sequence, so the banner is visible for every execution path except --version.
+ /// Factored out from to keep the dispatch sequence readable and to
+ /// allow the banner format to change without modifying the dispatch logic.
+ ///
private static void PrintBanner(Context context)
{
context.WriteLine($"BuildMark version {Version}");
@@ -152,6 +182,12 @@ private static void PrintBanner(Context context)
/// Prints usage information.
///
/// The context for output.
+ ///
+ /// Called for all three help-flag variants (-?, -h, --help) from
+ /// the dispatch sequence. Factored out from so that
+ /// the help text can evolve independently of the dispatch logic and so that a single
+ /// method is the authoritative source of the usage message.
+ ///
private static void PrintHelp(Context context)
{
context.WriteLine("Usage: buildmark [options]");
diff --git a/src/DemaConsulting.BuildMark/RepoConnectors/AzureDevOps/AzureDevOpsRepoConnector.cs b/src/DemaConsulting.BuildMark/RepoConnectors/AzureDevOps/AzureDevOpsRepoConnector.cs
index d6f51f5..c4b53b5 100644
--- a/src/DemaConsulting.BuildMark/RepoConnectors/AzureDevOps/AzureDevOpsRepoConnector.cs
+++ b/src/DemaConsulting.BuildMark/RepoConnectors/AzureDevOps/AzureDevOpsRepoConnector.cs
@@ -297,92 +297,27 @@ private static (VersionTag? fromVersion, string? fromHash) DetermineBaselineVers
return (null, null);
}
+ // Find the position of target version in the newest-first tag list
var toIndex = FindVersionIndex(lookupData.TagVersions, toVersion);
- var fromVersion = toVersion.IsPreRelease
- ? DetermineBaselineForPreRelease(toIndex, toHash, lookupData)
- : DetermineBaselineForRelease(toIndex, lookupData.TagVersions);
-
- if (fromVersion != null &&
- lookupData.TagToCommitHash.TryGetValue(fromVersion.Tag, out var fromHash))
- {
- return (fromVersion, fromHash);
- }
-
- return (fromVersion, null);
- }
-
- ///
- /// Determines the baseline version for a pre-release.
- ///
- /// Index of target version in version list.
- /// Commit hash of target version.
- /// Lookup data structures.
- /// Baseline version or null.
- private static VersionTag? DetermineBaselineForPreRelease(int toIndex, string toHash, LookupData lookupData)
- {
- var versions = lookupData.TagVersions;
- int startIndex;
-
- if (toIndex >= 0 && toIndex < versions.Count - 1)
- {
- startIndex = toIndex + 1;
- }
- else if (toIndex == -1 && versions.Count > 0)
- {
- startIndex = 0;
- }
- else
- {
- return null;
- }
-
- // Search for previous version with different commit hash
- for (var i = startIndex; i < versions.Count; i++)
+ // Build preceding versions list (oldest-first) for the base class selection methods.
+ // TagVersions is newest-first; preceding entries start at toIndex+1 (or 0 if target
+ // not found) and span to the end of the list. Iterating from Count-1 down gives oldest-first.
+ var startIndex = toIndex >= 0 ? toIndex + 1 : 0;
+ var preceding = new List();
+ for (var i = lookupData.TagVersions.Count - 1; i >= startIndex; i--)
{
- if (lookupData.TagToCommitHash.TryGetValue(versions[i].Tag, out var candidateHash) &&
- candidateHash != toHash)
- {
- return versions[i];
- }
- }
-
- return null;
- }
-
- ///
- /// Determines the baseline version for a release (non-pre-release).
- ///
- /// Index of target version in version list.
- /// List of version tags.
- /// Baseline version or null.
- private static VersionTag? DetermineBaselineForRelease(int toIndex, List versions)
- {
- int startIndex;
-
- if (toIndex >= 0 && toIndex < versions.Count - 1)
- {
- startIndex = toIndex + 1;
- }
- else if (toIndex == -1 && versions.Count > 0)
- {
- startIndex = 0;
- }
- else
- {
- return null;
+ var tag = lookupData.TagVersions[i];
+ var hash = lookupData.TagToCommitHash.TryGetValue(tag.Tag, out var h) ? h : string.Empty;
+ preceding.Add(new VersionCommitTag(tag, hash));
}
- // Search for previous non-pre-release version
- for (var i = startIndex; i < versions.Count; i++)
- {
- if (!versions[i].IsPreRelease)
- {
- return versions[i];
- }
- }
+ // Delegate selection to the base class algorithm
+ var baseline = toVersion.IsPreRelease
+ ? FindBaselineForPreRelease(preceding, toHash)
+ : FindBaselineForRelease(preceding);
- return null;
+ return (baseline?.VersionTag, baseline?.CommitHash);
}
///
diff --git a/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubRepoConnector.cs b/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubRepoConnector.cs
index 2e93434..be39f83 100644
--- a/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubRepoConnector.cs
+++ b/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubRepoConnector.cs
@@ -464,131 +464,30 @@ private static (VersionTag? fromVersion, string? fromHash) DetermineBaselineVers
return (null, null);
}
- // Find the position of target version in release history
+ // Find the position of target version in the newest-first release list
var toIndex = FindVersionIndex(lookupData.ReleaseVersions, toVersion);
- // Determine baseline version based on whether target is pre-release
- var fromVersion = toVersion.IsPreRelease
- ? DetermineBaselineForPreRelease(toIndex, toHash, lookupData)
- : DetermineBaselineForRelease(toIndex, lookupData.ReleaseVersions);
-
- // Get commit hash for baseline version if one was found
- if (fromVersion != null &&
- lookupData.TagToRelease.TryGetValue(fromVersion.Tag, out var fromRelease) &&
- lookupData.TagsByName.TryGetValue(fromRelease.TagName!, out var fromTagCommit))
+ // Build preceding versions list (oldest-first) for the base class selection methods.
+ // ReleaseVersions is newest-first; preceding entries start at toIndex+1 (or 0 if target
+ // not found) and span to the end of the list. Iterating from Count-1 down gives oldest-first.
+ var startIndex = toIndex >= 0 ? toIndex + 1 : 0;
+ var preceding = new List();
+ for (var i = lookupData.ReleaseVersions.Count - 1; i >= startIndex; i--)
{
- return (fromVersion, fromTagCommit.Commit.Sha);
+ var tag = lookupData.ReleaseVersions[i];
+ var hash = lookupData.TagToRelease.TryGetValue(tag.Tag, out var rel) &&
+ lookupData.TagsByName.TryGetValue(rel.TagName!, out var tagCommit)
+ ? tagCommit.Commit.Sha
+ : string.Empty;
+ preceding.Add(new VersionCommitTag(tag, hash));
}
- // Return baseline version with null hash if commit not found
- return (fromVersion, null);
- }
-
- ///
- /// Determines the baseline version for a pre-release.
- /// Pre-release versions pick the previous tag (release or pre-release) that isn't the same commit-hash.
- ///
- /// Index of target version in release history.
- /// Commit hash of target version.
- /// Lookup data structures.
- /// Baseline version or null.
- private static VersionTag? DetermineBaselineForPreRelease(int toIndex, string toHash, LookupData lookupData)
- {
- var releaseVersions = lookupData.ReleaseVersions;
-
- // Determine starting index for search
- int startIndex;
- if (toIndex >= 0 && toIndex < releaseVersions.Count - 1)
- {
- // Target exists, start from next older release
- startIndex = toIndex + 1;
- }
- else if (toIndex == -1 && releaseVersions.Count > 0)
- {
- // Target not in history, start from most recent
- startIndex = 0;
- }
- else
- {
- // No valid starting point
- startIndex = -1;
- }
-
- // If no valid starting point, return null
- if (startIndex < 0)
- {
- return null;
- }
-
- // Search forward through older releases (incrementing index) for previous version with different commit hash
- for (var i = startIndex; i < releaseVersions.Count; i++)
- {
- var candidateVersion = releaseVersions[i];
-
- // Get commit hash for candidate version
- if (lookupData.TagToRelease.TryGetValue(candidateVersion.Tag, out var candidateRelease) &&
- lookupData.TagsByName.TryGetValue(candidateRelease.TagName!, out var candidateTag) &&
- candidateTag.Commit.Sha != toHash)
- {
- // Found a version with a different commit hash - use it
- return candidateVersion;
- }
- }
-
- // No version with different commit hash found
- return null;
- }
-
- ///
- /// Determines the baseline version for a release (non-pre-release).
- ///
- /// Index of target version in release history.
- /// List of release versions.
- /// Baseline version or null.
- private static VersionTag? DetermineBaselineForRelease(int toIndex, List releaseVersions)
- {
- // Release versions skip pre-releases and use previous non-pre-release as baseline
- var startIndex = DetermineSearchStartIndex(toIndex, releaseVersions.Count);
-
- // Search forward through older releases (incrementing index) for previous non-pre-release version
- if (startIndex >= 0)
- {
- for (var i = startIndex; i < releaseVersions.Count; i++)
- {
- if (!releaseVersions[i].IsPreRelease)
- {
- return releaseVersions[i];
- }
- }
- }
-
- return null;
- }
-
- ///
- /// Determines the starting index for searching for previous releases.
- ///
- /// Index of target version in release history.
- /// Total number of releases.
- /// Starting index for search, or -1 if no search needed.
- private static int DetermineSearchStartIndex(int toIndex, int releaseCount)
- {
- // Target version exists in history, start search from next older release
- if (toIndex >= 0 && toIndex < releaseCount - 1)
- {
- return toIndex + 1;
- }
-
- // Target version not in history, start from most recent release
- if (toIndex == -1 && releaseCount > 0)
- {
- // Target version not in history, start from most recent release
- // The target is newer than all existing releases, so use the most recent as baseline
- return 0;
- }
+ // Delegate selection to the base class algorithm
+ var baseline = toVersion.IsPreRelease
+ ? FindBaselineForPreRelease(preceding, toHash)
+ : FindBaselineForRelease(preceding);
- // Target is the oldest release or no releases exist, no previous release exists
- return -1;
+ return (baseline?.VersionTag, baseline?.CommitHash);
}
///
diff --git a/src/DemaConsulting.BuildMark/RepoConnectors/Mock/MockRepoConnector.cs b/src/DemaConsulting.BuildMark/RepoConnectors/Mock/MockRepoConnector.cs
index 2b6a83a..73d0542 100644
--- a/src/DemaConsulting.BuildMark/RepoConnectors/Mock/MockRepoConnector.cs
+++ b/src/DemaConsulting.BuildMark/RepoConnectors/Mock/MockRepoConnector.cs
@@ -231,7 +231,7 @@ public override async Task GetBuildInformationAsync(VersionTag
/// Determines the baseline version for comparing changes.
///
/// Target version.
- /// List of tag history.
+ /// List of tag history, ordered oldest first.
/// Tuple of (fromTagInfo, fromHash).
private async Task<(VersionTag? fromTagInfo, string? fromHash)> DetermineBaselineVersionAsync(
VersionTag toTagInfo,
@@ -243,95 +243,28 @@ public override async Task GetBuildInformationAsync(VersionTag
return (null, null);
}
- // Find the position of target version in tag history
+ // Find the position of target version in the tag history
var toIndex = FindVersionIndex(tags, toTagInfo);
- // Determine baseline version based on whether target is pre-release
- var fromTagInfo = toTagInfo.IsPreRelease
- ? DetermineBaselineForPreRelease(toIndex, tags)
- : DetermineBaselineForRelease(toIndex, tags);
+ // Get the commit hash of the target version for same-commit detection
+ var toHash = await GetHashForTagAsync(toTagInfo.Tag) ?? CurrentHash;
- // Get commit hash for baseline version if one was found
- if (fromTagInfo != null)
- {
- var fromHash = await GetHashForTagAsync(fromTagInfo.Tag);
- return (fromTagInfo, fromHash);
- }
-
- // Return baseline version with null hash
- return (fromTagInfo, null);
- }
-
- ///
- /// Determines the baseline version for a pre-release.
- ///
- /// Index of target version in tag history.
- /// List of tags.
- /// Baseline version or null.
- private static VersionTag? DetermineBaselineForPreRelease(int toIndex, List tags)
- {
- // Pre-release versions use the immediately previous tag as baseline
- if (toIndex > 0)
+ // Build list of preceding VersionCommitTags (oldest first, excluding target)
+ var precedingCount = toIndex >= 0 ? toIndex : tags.Count;
+ var precedingVersions = new List(precedingCount);
+ for (var i = 0; i < precedingCount; i++)
{
- // Target version exists in history, use previous tag
- return tags[toIndex - 1];
+ var hash = await GetHashForTagAsync(tags[i].Tag) ?? string.Empty;
+ precedingVersions.Add(new VersionCommitTag(tags[i], hash));
}
- // Target version not in history, use most recent tag as baseline
- if (toIndex == -1)
- {
- return tags[^1];
- }
-
- // If toIndex == 0, this is the first tag, no baseline
- return null;
- }
-
- ///
- /// Determines the baseline version for a release (non-pre-release).
- ///
- /// Index of target version in tag history.
- /// List of tags.
- /// Baseline version or null.
- private static VersionTag? DetermineBaselineForRelease(int toIndex, List tags)
- {
- // Release versions skip pre-releases and use previous release as baseline
- var startIndex = DetermineSearchStartIndex(toIndex, tags.Count);
-
- // Search backward for previous non-pre-release version
- for (var i = startIndex; i >= 0; i--)
- {
- if (!tags[i].IsPreRelease)
- {
- return tags[i];
- }
- }
-
- return null;
- }
-
- ///
- /// Determines the starting index for searching for previous releases.
- ///
- /// Index of target version in tag history.
- /// Total number of tags.
- /// Starting index for search, or -1 if no search needed.
- private static int DetermineSearchStartIndex(int toIndex, int tagCount)
- {
- // Target version exists in history, start search from previous position
- if (toIndex > 0)
- {
- return toIndex - 1;
- }
-
- // Target version not in history, start from most recent tag
- if (toIndex == -1)
- {
- return tagCount - 1;
- }
+ // Find baseline using base class methods
+ var baseline = toTagInfo.IsPreRelease
+ ? FindBaselineForPreRelease(precedingVersions, toHash)
+ : FindBaselineForRelease(precedingVersions);
- // Target is first tag, no previous release exists
- return -1;
+ // Return baseline version and hash if one was found
+ return (baseline?.VersionTag, baseline?.CommitHash);
}
///
diff --git a/src/DemaConsulting.BuildMark/RepoConnectors/RepoConnectorBase.cs b/src/DemaConsulting.BuildMark/RepoConnectors/RepoConnectorBase.cs
index 914c593..df00eec 100644
--- a/src/DemaConsulting.BuildMark/RepoConnectors/RepoConnectorBase.cs
+++ b/src/DemaConsulting.BuildMark/RepoConnectors/RepoConnectorBase.cs
@@ -146,4 +146,63 @@ protected static int FindVersionIndex(List versions, VersionTag targ
// Version not found in list
return -1;
}
+
+ ///
+ /// Finds the baseline version for a pre-release target.
+ /// Returns the most recent entry from whose commit hash
+ /// differs from , skipping any entries that share the
+ /// same commit hash as the target (which would produce an empty changelog).
+ ///
+ ///
+ /// Version-commit tags that precede the target, ordered oldest first.
+ ///
+ /// Commit hash of the target version.
+ ///
+ /// The most recent preceding entry with a different commit hash, or null if none exists.
+ ///
+ internal static VersionCommitTag? FindBaselineForPreRelease(
+ List precedingVersions,
+ string targetCommitHash)
+ {
+ // Search from newest to oldest, skipping entries with the same commit hash
+ for (var i = precedingVersions.Count - 1; i >= 0; i--)
+ {
+ // Return the first entry that has a different commit hash
+ if (precedingVersions[i].CommitHash != targetCommitHash)
+ {
+ return precedingVersions[i];
+ }
+ }
+
+ // No entry with a different commit hash found
+ return null;
+ }
+
+ ///
+ /// Finds the baseline version for a release target.
+ /// Returns the most recent entry from that is not a
+ /// pre-release, skipping any pre-release entries.
+ ///
+ ///
+ /// Version-commit tags that precede the target, ordered oldest first.
+ ///
+ ///
+ /// The most recent preceding non-pre-release entry, or null if none exists.
+ ///
+ internal static VersionCommitTag? FindBaselineForRelease(
+ List precedingVersions)
+ {
+ // Search from newest to oldest, skipping pre-release entries
+ for (var i = precedingVersions.Count - 1; i >= 0; i--)
+ {
+ // Return the first non-pre-release entry found
+ if (!precedingVersions[i].VersionTag.IsPreRelease)
+ {
+ return precedingVersions[i];
+ }
+ }
+
+ // No non-pre-release entry found
+ return null;
+ }
}
diff --git a/test/DemaConsulting.BuildMark.Tests/IntegrationTests.cs b/test/DemaConsulting.BuildMark.Tests/IntegrationTests.cs
index 786178d..d35200c 100644
--- a/test/DemaConsulting.BuildMark.Tests/IntegrationTests.cs
+++ b/test/DemaConsulting.BuildMark.Tests/IntegrationTests.cs
@@ -399,6 +399,75 @@ public void BuildMark_Report_ShowsVersionRangeFromPreviousRelease()
}
}
+ ///
+ /// Test that the report selects the nearest predecessor with a different commit hash
+ /// as the baseline when generating build notes for a pre-release version.
+ ///
+ [Fact]
+ public void BuildMark_Report_BaselinePreRelease_SkipsSameCommitPredecessor()
+ {
+ // Arrange: create a temporary report file path
+ var reportFile = Path.GetTempFileName();
+ try
+ {
+ // Create context with a connector that provides a pre-release where the immediate
+ // predecessor shares the same commit hash (should be skipped), leaving v1.1.0 as baseline
+ using var context = Context.Create(
+ ["--build-version", "1.2.0-beta.2", "--report", reportFile, "--silent"],
+ () => new PreReleaseSameCommitConnector());
+
+ // Act: run the program
+ Program.Run(context);
+
+ // Assert: report uses v1.1.0 as the baseline (same-commit predecessor was skipped)
+ Assert.Equal(0, context.ExitCode);
+ var content = File.ReadAllText(reportFile);
+ Assert.Contains("v1.1.0", content);
+ Assert.Contains("v1.1.0...1.2.0-beta.2", content);
+ }
+ finally
+ {
+ if (File.Exists(reportFile))
+ {
+ File.Delete(reportFile);
+ }
+ }
+ }
+
+ ///
+ /// Test that the report generates from the beginning of history when no previous
+ /// version tag qualifies as a baseline.
+ ///
+ [Fact]
+ public void BuildMark_Report_BaselineFirstRelease_GeneratesFromBeginningOfHistory()
+ {
+ // Arrange: create a temporary report file path
+ var reportFile = Path.GetTempFileName();
+ try
+ {
+ // Create context with a connector that provides a first release with no baseline
+ using var context = Context.Create(
+ ["--build-version", "1.0.0", "--report", reportFile, "--silent"],
+ () => new FirstReleaseConnector());
+
+ // Act: run the program
+ Program.Run(context);
+
+ // Assert: report generates successfully and shows N/A for the missing baseline
+ Assert.Equal(0, context.ExitCode);
+ var content = File.ReadAllText(reportFile);
+ Assert.Contains("1.0.0", content);
+ Assert.Contains("N/A", content);
+ }
+ finally
+ {
+ if (File.Exists(reportFile))
+ {
+ File.Delete(reportFile);
+ }
+ }
+ }
+
///
/// Test that the report includes known issues when the flag is set.
///
@@ -1215,6 +1284,37 @@ public Task GetBuildInformationAsync(VersionTag? version = nul
null));
}
}
+
+ ///
+ /// Mock connector that simulates a pre-release where the immediate predecessor
+ /// shares the same commit hash (skipped), leaving v1.1.0 as the baseline.
+ ///
+ private sealed class PreReleaseSameCommitConnector : IRepoConnector
+ {
+ ///
+ public Task GetBuildInformationAsync(VersionTag? version = null)
+ {
+ // Target is 1.2.0-beta.2; its immediate predecessor 1.2.0-beta.1 shares the same
+ // commit hash and must be skipped, so v1.1.0 is chosen as the baseline instead.
+ var currentTag = new VersionCommitTag(VersionTag.Create("1.2.0-beta.2"), "target-hash");
+ var baselineTag = new VersionCommitTag(VersionTag.Create("v1.1.0"), "prev-hash");
+ var changelogLink = new WebLink("Full Changelog", "https://example.com/compare/v1.1.0...1.2.0-beta.2");
+ return Task.FromResult(new BuildInformation(baselineTag, currentTag, [], [], [], changelogLink));
+ }
+ }
+
+ ///
+ /// Mock connector that simulates a first release with no baseline (no preceding tags).
+ ///
+ private sealed class FirstReleaseConnector : IRepoConnector
+ {
+ ///
+ public Task GetBuildInformationAsync(VersionTag? version = null)
+ {
+ var currentTag = new VersionCommitTag(VersionTag.Create("v1.0.0"), "first-hash");
+ return Task.FromResult(new BuildInformation(null, currentTag, [], [], [], null));
+ }
+ }
}
diff --git a/test/DemaConsulting.BuildMark.Tests/ProgramTests.cs b/test/DemaConsulting.BuildMark.Tests/ProgramTests.cs
index da4505b..e7a4851 100644
--- a/test/DemaConsulting.BuildMark.Tests/ProgramTests.cs
+++ b/test/DemaConsulting.BuildMark.Tests/ProgramTests.cs
@@ -29,6 +29,7 @@ namespace DemaConsulting.BuildMark.Tests;
///
/// Tests for the Program class.
///
+[Collection("ProgramTests")]
public class ProgramTests
{
///
@@ -465,3 +466,11 @@ public Task GetBuildInformationAsync(VersionTag? version = nul
}
}
}
+
+///
+/// Disables parallel execution for the collection to prevent
+/// race conditions when tests redirect the shared and
+/// streams via Console.SetOut / Console.SetError.
+///
+[CollectionDefinition("ProgramTests", DisableParallelization = true)]
+public sealed class ProgramTestsCollection;
diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorBaseTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorBaseTests.cs
index 6af56b6..d16454e 100644
--- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorBaseTests.cs
+++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorBaseTests.cs
@@ -199,4 +199,132 @@ public void RepoConnectorBase_FindVersionIndex_VersionNotInList_ReturnsMinusOne(
// Assert - Should return -1 when version is not found
Assert.True(foundIndex == -1, "Should return -1 when the target version is not in the list");
}
+
+ ///
+ /// Test that FindBaselineForPreRelease skips same-commit tags and returns the most recent
+ /// preceding tag that has a different commit hash.
+ ///
+ [Fact]
+ public void RepoConnectorBase_FindBaselineForPreRelease_SameCommitSkipped_ReturnsPreviousDistinctCommit()
+ {
+ // Arrange - three preceding tags; first two share the target's commit hash, third differs
+ const string targetHash = "aaaa";
+ List precedingVersions =
+ [
+ new(VersionTag.Create("v1.0.0"), "bbbb"), // different hash - oldest
+ new(VersionTag.Create("v2.0.0-beta.1"), targetHash), // same hash
+ new(VersionTag.Create("v2.0.0-beta.2"), targetHash) // same hash - newest
+ ];
+
+ // Act - find baseline for a pre-release with the shared hash
+ var baseline = RepoConnectorBase.FindBaselineForPreRelease(precedingVersions, targetHash);
+
+ // Assert - returns the first tag with a different commit hash (v1.0.0), skipping same-hash entries
+ Assert.True(baseline != null, "Baseline should not be null when a different-commit predecessor exists");
+ Assert.True(
+ baseline!.VersionTag.Tag == "v1.0.0",
+ $"Expected baseline 'v1.0.0' but got '{baseline.VersionTag.Tag}'");
+ }
+
+ ///
+ /// Test that FindBaselineForPreRelease returns null when all preceding tags share
+ /// the same commit hash as the target.
+ ///
+ [Fact]
+ public void RepoConnectorBase_FindBaselineForPreRelease_AllSameCommit_ReturnsNull()
+ {
+ // Arrange - all preceding tags share the same commit hash as the target
+ const string targetHash = "aaaa";
+ List precedingVersions =
+ [
+ new(VersionTag.Create("v1.0.0-rc.1"), targetHash),
+ new(VersionTag.Create("v1.0.0-rc.2"), targetHash)
+ ];
+
+ // Act - find baseline when all predecessors share the target hash
+ var baseline = RepoConnectorBase.FindBaselineForPreRelease(precedingVersions, targetHash);
+
+ // Assert - returns null because no different-commit predecessor exists
+ Assert.True(baseline == null, "Baseline should be null when all predecessors share the same commit hash");
+ }
+
+ ///
+ /// Test that FindBaselineForRelease skips pre-release tags and returns the most recent
+ /// preceding release tag.
+ ///
+ [Fact]
+ public void RepoConnectorBase_FindBaselineForRelease_SkipsPreReleaseTags_ReturnsPreviousReleaseTag()
+ {
+ // Arrange - preceding tags include several pre-releases before the previous release
+ List precedingVersions =
+ [
+ new(VersionTag.Create("v1.0.0"), "hash1"), // release - oldest
+ new(VersionTag.Create("v2.0.0-alpha.1"), "hash2"), // pre-release
+ new(VersionTag.Create("v2.0.0-beta.1"), "hash3"), // pre-release - newest
+ ];
+
+ // Act - find baseline for a release target (e.g., v2.0.0)
+ var baseline = RepoConnectorBase.FindBaselineForRelease(precedingVersions);
+
+ // Assert - returns v1.0.0, the most recent non-pre-release tag
+ Assert.True(baseline != null, "Baseline should not be null when a preceding release tag exists");
+ Assert.True(
+ baseline!.VersionTag.Tag == "v1.0.0",
+ $"Expected baseline 'v1.0.0' but got '{baseline.VersionTag.Tag}'");
+ }
+
+ ///
+ /// Test that FindBaselineForRelease returns null when only pre-release tags precede
+ /// the release target.
+ ///
+ [Fact]
+ public void RepoConnectorBase_FindBaselineForRelease_NoPreviousRelease_ReturnsNull()
+ {
+ // Arrange - only pre-release tags exist before the target release
+ List precedingVersions =
+ [
+ new(VersionTag.Create("v1.0.0-alpha.1"), "hash1"),
+ new(VersionTag.Create("v1.0.0-beta.1"), "hash2")
+ ];
+
+ // Act - find baseline when no release tag precedes the target
+ var baseline = RepoConnectorBase.FindBaselineForRelease(precedingVersions);
+
+ // Assert - returns null because no preceding release tag exists
+ Assert.True(baseline == null, "Baseline should be null when only pre-release tags precede the target");
+ }
+
+ ///
+ /// Test that FindBaselineForRelease returns null when the version list is empty
+ /// (first release scenario).
+ ///
+ [Fact]
+ public void RepoConnectorBase_FindBaselineForRelease_NoPreviousVersion_ReturnsNull()
+ {
+ // Arrange - empty preceding version list (first release in repository)
+ List precedingVersions = [];
+
+ // Act - find baseline when no preceding versions exist
+ var baseline = RepoConnectorBase.FindBaselineForRelease(precedingVersions);
+
+ // Assert - returns null because no preceding versions exist
+ Assert.True(baseline == null, "Baseline should be null when no preceding versions exist");
+ }
+
+ ///
+ /// Test that FindBaselineForPreRelease returns null when the version list is empty
+ /// (first pre-release scenario).
+ ///
+ [Fact]
+ public void RepoConnectorBase_FindBaselineForPreRelease_NoPreviousVersion_ReturnsNull()
+ {
+ // Arrange - empty preceding version list (first tag in repository)
+ List precedingVersions = [];
+
+ // Act - find baseline when no preceding versions exist
+ var baseline = RepoConnectorBase.FindBaselineForPreRelease(precedingVersions, "aaaa");
+
+ // Assert - returns null because no preceding versions exist
+ Assert.True(baseline == null, "Baseline should be null when no preceding versions exist");
+ }
}