Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/standards/reqstream-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.:
Expand All @@ -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 \
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Options:
--validate Run self-validation
--results <file> Write validation results to file (TRX or JUnit format; use .trx or .xml extension)
--log <file> Write output to log file
--lint Lint requirements files for structural issues
--lint Lint requirements files for issues (including orphaned requirements when root tags are configured; combine with --enforce to fail on them)
Comment thread
Copilot marked this conversation as resolved.
Resolved
--depth <depth> Default markdown header depth for all reports (default: 1)
--requirements <pattern> Requirements files glob pattern
--report <file> Export requirements to markdown file
Expand Down Expand Up @@ -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)

Expand Down
30 changes: 29 additions & 1 deletion src/DemaConsulting.ReqStream/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
/// <exception cref="System.IO.IOException">Propagates from <see cref="Modeling.Requirements.Load"/> or file export methods if file I/O fails.</exception>
/// <exception cref="System.Xml.XmlException">Propagates from <see cref="Modeling.Requirements.Load"/> if a requirements file contains malformed XML.</exception>
/// <exception cref="System.Exception">Any exception not caught internally by <see cref="Modeling.Requirements.Load"/>, <see cref="SelfTest.Validation.Run"/>, <see cref="Tracing.TraceMatrix"/> construction, or file export methods will propagate to <see cref="Main"/> where it is handled by the unexpected-exception catch block.</exception>
public static void Run(Context context)

Check warning on line 110 in src/DemaConsulting.ReqStream/Program.cs

View workflow job for this annotation

GitHub Actions / Build / Build macos-latest

Refactor this method to reduce its Cognitive Complexity from 23 to the 15 allowed.

Check warning on line 110 in src/DemaConsulting.ReqStream/Program.cs

View workflow job for this annotation

GitHub Actions / Build / Build windows-latest

Refactor this method to reduce its Cognitive Complexity from 23 to the 15 allowed.

Check warning on line 110 in src/DemaConsulting.ReqStream/Program.cs

View workflow job for this annotation

GitHub Actions / Build / Build ubuntu-latest

Refactor this method to reduce its Cognitive Complexity from 23 to the 15 allowed.
{
// Priority 1: Version query
if (context.Version)
Expand Down Expand Up @@ -148,6 +148,34 @@
{
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<string>(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<string> write = context.Enforce ? context.WriteError : context.WriteWarning;
var severity = context.Enforce ? "Error" : "Warning";
ReportOrphans(orphanResult, mergedRootTags, write, severity);
}
}

return;
}

Expand Down Expand Up @@ -185,7 +213,7 @@
context.WriteLine(" --silent Suppress console output");
context.WriteLine(" --validate Run self-validation");
context.WriteLine(" --results <file> 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 <file> Write output to log file");
context.WriteLine(" --depth <depth> Default markdown header depth for all reports (default: 1)");
context.WriteLine(" --requirements <pattern> Requirements files glob pattern");
Expand Down
133 changes: 133 additions & 0 deletions test/DemaConsulting.ReqStream.Tests/ProgramTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,139 @@ public void Program_Run_WithLintFlag_OnlyOutputsIssues()
Assert.DoesNotContain("No issues found", logContent);
}

/// <summary>
/// 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.
/// </summary>
Comment thread
Copilot marked this conversation as resolved.
Resolved
[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);
}

/// <summary>
/// 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.
/// </summary>
[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);
}

/// <summary>
/// Test Run with lint flag and root tags configured but no orphans present produces no
/// orphan-related output.
/// </summary>
[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);
}

/// <summary>
/// Test Run with enforcement mode and failed tests fails.
/// </summary>
Expand Down
Loading