Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -94,8 +94,8 @@ Options:
--silent Suppress console output
--validate Run self-validation
--results <file> Write validation results to file (TRX or JUnit format; use .trx or .xml extension)
--lint Lint requirements files for issues (including orphans)
--log <file> Write output to log file
--lint Lint requirements files for structural issues
--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 ubuntu-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 macos-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 orphans)");
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 full requirements-processing invocation (e.g. --report/--matrix).
/// </summary>
[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