From 971e884c6d1a1c95e3aa94b8d66919f6a78be683 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Wed, 2 Sep 2026 07:14:18 -0400 Subject: [PATCH 1/3] Make --lint also detect orphaned requirements when root tags are configured Previously --lint only checked structural/schema issues (duplicate ids, malformed YAML, etc.) via LoadResult.Issues, bypassing orphan detection entirely. Agents running --lint expected to see orphan-shaped requirements alongside other lint issues in a single invocation. --lint now also computes the merged root-tag set and runs FindOrphans when root tags are configured, reporting orphans as a non-fatal warning by default, or as an error (failing the run) when combined with --enforce - matching the existing warning/error convention used by full requirements processing. No behavior change when root tags are not configured. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/standards/reqstream-usage.md | 7 +- README.md | 7 +- src/DemaConsulting.ReqStream/Program.cs | 30 +++- .../ProgramTests.cs | 133 ++++++++++++++++++ 4 files changed, 172 insertions(+), 5 deletions(-) diff --git a/.github/standards/reqstream-usage.md b/.github/standards/reqstream-usage.md index 8db78ab..7fd053e 100644 --- a/.github/standards/reqstream-usage.md +++ b/.github/standards/reqstream-usage.md @@ -132,7 +132,9 @@ This check always runs against the complete loaded requirement graph and is **in `--filter`**, which only narrows report/matrix output elsewhere in the same invocation. Orphan checking triggers automatically whenever the merged root-tag set is non-empty - there is -no separate `--orphans` flag: +no separate `--orphans` flag, and it runs alongside `--lint` as well as full requirements +processing, so agents do not need to remember two separate invocations to see both structural +lint issues and orphaned requirements: - **Without `--enforce`**: orphans are reported as a non-fatal warning (does not affect the exit code), e.g.: @@ -147,7 +149,8 @@ no separate `--orphans` flag: missing test coverage, regardless of whether `--tests` was supplied. `--enforce` independently enforces (a) test coverage if a trace matrix was built and (b) orphan-freedom if root tags are configured; the pre-existing "nothing to enforce" error is reported only when **neither** - applies. + applies. `--lint --enforce` enforces orphan-freedom the same way, without requiring a full + `--requirements`/`--report`/`--matrix` invocation. ```bash dotnet reqstream --requirements requirements.yaml \ diff --git a/README.md b/README.md index add7b79..ab4a7e4 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Options: --validate Run self-validation --results Write validation results to file (TRX or JUnit format; use .trx or .xml extension) --log Write output to log file - --lint Lint requirements files for structural issues + --lint Lint requirements files for structural issues (including orphaned requirements when root tags are configured; combine with --enforce to fail on them) --depth Default markdown header depth for all reports (default: 1) --requirements Requirements files glob pattern --report Export requirements to markdown file @@ -404,8 +404,11 @@ reqstream --requirements "**/*.yaml" --root-tags product,quality - Simply supplying `--root-tags` (or declaring a `root-tags:` key in a requirements YAML file) is enough to enable the check — no separate flag is needed +- `--lint` also performs orphan detection when root tags are configured, so agents running `--lint` see + both structural lint issues and orphaned requirements in a single invocation - Without `--enforce`, orphans are printed as a warning and do not affect the exit code -- With `--enforce`, orphans become a build-breaking error — independent of whether `--tests` was supplied +- With `--enforce` (whether combined with `--lint` or full requirements processing), orphans become a + build-breaking error — independent of whether `--tests` was supplied - If no root tags are configured (from either source), orphan-checking is skipped entirely (fully backward compatible) diff --git a/src/DemaConsulting.ReqStream/Program.cs b/src/DemaConsulting.ReqStream/Program.cs index 8df7e9f..842317f 100644 --- a/src/DemaConsulting.ReqStream/Program.cs +++ b/src/DemaConsulting.ReqStream/Program.cs @@ -148,6 +148,34 @@ public static void Run(Context context) { var result = Requirements.Load(context.RequirementsFiles.ToArray()); result.ReportIssues(context); + + // Orphan detection is itself a structural lint concern - an orphaned requirement + // is just as much a shape problem with the requirements tree as a duplicate id or + // a dangling children reference - so run it alongside the other lint checks + // whenever loading succeeded and root tags are configured. This reuses the exact + // same detection ProcessRequirements uses below, so --lint and full requirements + // processing never disagree about which requirements are orphaned, and agents do + // not need to run a separate invocation to see orphan issues. + if (result.Requirements != null) + { + var mergedRootTags = new HashSet(result.Requirements.RootTags, StringComparer.Ordinal); + if (context.RootTags != null) + { + mergedRootTags.UnionWith(context.RootTags); + } + + var orphanResult = result.Requirements.FindOrphans(mergedRootTags); + if (orphanResult.OrphanIds.Count > 0) + { + // Non-fatal warning by default, matching --requirements' behavior; escalate + // to a fatal error when --enforce is also supplied, so "--lint --enforce" + // fails the same way "--requirements ... --enforce" does. + Action write = context.Enforce ? context.WriteError : context.WriteWarning; + var severity = context.Enforce ? "Error" : "Warning"; + ReportOrphans(orphanResult, mergedRootTags, write, severity); + } + } + return; } @@ -185,7 +213,7 @@ private static void PrintHelp(Context context) context.WriteLine(" --silent Suppress console output"); context.WriteLine(" --validate Run self-validation"); context.WriteLine(" --results Write validation results to file (.trx or .xml extension required)"); - context.WriteLine(" --lint Lint requirements files for issues"); + context.WriteLine(" --lint Lint requirements files for issues (including orphaned requirements when root tags are configured; combine with --enforce to fail on them)"); context.WriteLine(" --log Write output to log file"); context.WriteLine(" --depth Default markdown header depth for all reports (default: 1)"); context.WriteLine(" --requirements Requirements files glob pattern"); diff --git a/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs b/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs index c208708..a9fdea1 100644 --- a/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs @@ -1179,6 +1179,139 @@ public void Program_Run_WithLintFlag_OnlyOutputsIssues() Assert.DoesNotContain("No issues found", logContent); } + /// + /// Test Run with lint flag and root tags configured reports orphans as a non-fatal + /// warning, so --lint alone surfaces orphan-shaped requirements without needing a + /// separate --requirements invocation. + /// + [Fact] + public void Program_Run_WithLintFlagAndRootTags_ReportsOrphansAsWarning() + { + // Arrange: a root-tagged requirement and an unreachable orphan requirement + var reqFile = _testDirectory.GetFilePath("requirements.yaml"); + File.WriteAllText(reqFile, @" +root-tags: [product] +sections: + - title: Test Section + requirements: + - id: ROOT-001 + title: Root Requirement + tags: [product] + - id: ORPHAN-001 + title: Orphaned Requirement +"); + + var logFile = _testDirectory.GetFilePath("lint-orphan-warning.log"); + + var originalDir = Directory.GetCurrentDirectory(); + try + { + Directory.SetCurrentDirectory(_testDirectory.DirectoryPath); + + // Act: run with --lint only, no --enforce + using var context = Context.Create(["--lint", "--requirements", "*.yaml", "--silent", "--log", logFile]); + Program.Run(context); + + // Assert: exit code unaffected, warning text (not error text) printed with the orphan id + Assert.Equal(0, context.ExitCode); + } + finally + { + Directory.SetCurrentDirectory(originalDir); + } + + var logContent = File.ReadAllText(logFile); + Assert.Contains("Warning: 1 of 2 requirements is orphaned", logContent); + Assert.Contains("ORPHAN-001", logContent); + } + + /// + /// Test Run with lint flag combined with --enforce fails when root tags are configured + /// and orphans are present, matching the failure behavior of --requirements --enforce. + /// + [Fact] + public void Program_Run_WithLintFlagAndEnforce_FailsOnOrphans() + { + // Arrange: a root-tagged requirement and an unreachable orphan requirement + var reqFile = _testDirectory.GetFilePath("requirements.yaml"); + File.WriteAllText(reqFile, @" +root-tags: [product] +sections: + - title: Test Section + requirements: + - id: ROOT-001 + title: Root Requirement + tags: [product] + - id: ORPHAN-001 + title: Orphaned Requirement +"); + + var logFile = _testDirectory.GetFilePath("lint-orphan-error.log"); + + var originalDir = Directory.GetCurrentDirectory(); + try + { + Directory.SetCurrentDirectory(_testDirectory.DirectoryPath); + + // Act: run with --lint and --enforce together + using var context = Context.Create(["--lint", "--enforce", "--requirements", "*.yaml", "--silent", "--log", logFile]); + Program.Run(context); + + // Assert: exit code reflects failure, error text (not warning text) printed with the orphan id + Assert.Equal(1, context.ExitCode); + } + finally + { + Directory.SetCurrentDirectory(originalDir); + } + + var logContent = File.ReadAllText(logFile); + Assert.Contains("Error: 1 of 2 requirements is orphaned", logContent); + Assert.Contains("ORPHAN-001", logContent); + } + + /// + /// Test Run with lint flag and root tags configured but no orphans present produces no + /// orphan-related output. + /// + [Fact] + public void Program_Run_WithLintFlagAndRootTagsNoOrphans_ProducesNoOrphanOutput() + { + // Arrange: a root-tagged requirement with no orphans + var reqFile = _testDirectory.GetFilePath("requirements.yaml"); + File.WriteAllText(reqFile, @" +root-tags: [product] +sections: + - title: Test Section + requirements: + - id: ROOT-001 + title: Root Requirement + tags: [product] +"); + + var logFile = _testDirectory.GetFilePath("lint-no-orphans.log"); + + var originalDir = Directory.GetCurrentDirectory(); + try + { + Directory.SetCurrentDirectory(_testDirectory.DirectoryPath); + + // Act: run with --lint only, root tags configured, no orphans + using var context = Context.Create(["--lint", "--requirements", "*.yaml", "--silent", "--log", logFile]); + Program.Run(context); + + // Assert: lint succeeds with no orphan-related output + Assert.Equal(0, context.ExitCode); + } + finally + { + Directory.SetCurrentDirectory(originalDir); + } + + var logContent = File.ReadAllText(logFile); + Assert.DoesNotContain("orphaned", logContent); + } + /// /// Test Run with enforcement mode and failed tests fails. /// From 11c68113c4614b15fc638752cf9cbc73c10199de Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Wed, 2 Sep 2026 08:58:28 -0400 Subject: [PATCH 2/3] Fix README help-output example to match actual --lint help text Address PR #214 review comment: the README's help-output example still said 'structural issues' with mismatched spacing, but Program.PrintHelp actually emits 'Lint requirements files for issues'. Corrected wording and column alignment so the documented example matches real --help output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ab4a7e4..702f2a9 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Options: --validate Run self-validation --results Write validation results to file (TRX or JUnit format; use .trx or .xml extension) --log Write output to log file - --lint Lint requirements files for structural issues (including orphaned requirements when root tags are configured; combine with --enforce to fail on them) + --lint Lint requirements files for issues (including orphaned requirements when root tags are configured; combine with --enforce to fail on them) --depth Default markdown header depth for all reports (default: 1) --requirements Requirements files glob pattern --report Export requirements to markdown file From 240b538de965f4be1e477e663bcc9b38a8bd237a Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Wed, 2 Sep 2026 09:16:06 -0400 Subject: [PATCH 3/3] Shorten --lint help text and fix option ordering per review feedback - Trim the --lint description to a concise phrase ('...for issues (including orphans)') instead of a full sentence, matching the short style of other CLI option descriptions. - Reorder README's --help example so --lint appears before --log, matching Program.PrintHelp's actual emission order. - Clarify the ProgramTests doc-comment: --lint still requires --requirements to specify input files; what it avoids is a separate full requirements-processing invocation (--report/--matrix). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- src/DemaConsulting.ReqStream/Program.cs | 2 +- test/DemaConsulting.ReqStream.Tests/ProgramTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 702f2a9..366d99c 100644 --- a/README.md +++ b/README.md @@ -94,8 +94,8 @@ Options: --silent Suppress console output --validate Run self-validation --results Write validation results to file (TRX or JUnit format; use .trx or .xml extension) + --lint Lint requirements files for issues (including orphans) --log Write output to log file - --lint Lint requirements files for issues (including orphaned requirements when root tags are configured; combine with --enforce to fail on them) --depth Default markdown header depth for all reports (default: 1) --requirements Requirements files glob pattern --report Export requirements to markdown file diff --git a/src/DemaConsulting.ReqStream/Program.cs b/src/DemaConsulting.ReqStream/Program.cs index 842317f..810bff0 100644 --- a/src/DemaConsulting.ReqStream/Program.cs +++ b/src/DemaConsulting.ReqStream/Program.cs @@ -213,7 +213,7 @@ private static void PrintHelp(Context context) context.WriteLine(" --silent Suppress console output"); context.WriteLine(" --validate Run self-validation"); context.WriteLine(" --results Write validation results to file (.trx or .xml extension required)"); - context.WriteLine(" --lint Lint requirements files for issues (including orphaned requirements when root tags are configured; combine with --enforce to fail on them)"); + context.WriteLine(" --lint Lint requirements files for issues (including orphans)"); context.WriteLine(" --log Write output to log file"); context.WriteLine(" --depth Default markdown header depth for all reports (default: 1)"); context.WriteLine(" --requirements Requirements files glob pattern"); diff --git a/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs b/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs index a9fdea1..6d52420 100644 --- a/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs @@ -1182,7 +1182,7 @@ public void Program_Run_WithLintFlag_OnlyOutputsIssues() /// /// Test Run with lint flag and root tags configured reports orphans as a non-fatal /// warning, so --lint alone surfaces orphan-shaped requirements without needing a - /// separate --requirements invocation. + /// separate full requirements-processing invocation (e.g. --report/--matrix). /// [Fact] public void Program_Run_WithLintFlagAndRootTags_ReportsOrphansAsWarning()