diff --git a/CHANGELOG.md b/CHANGELOG.md index 081b01f9..2f98795c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### [Unreleased] +#### Added + +- **Opt-in `--fail-on-diff` CI gating** — Normal completed comparisons continue to return `0` by default. With `--fail-on-diff`, nildiff now returns dedicated exit code `5` when the final reportable Added/Removed/Modified sets are non-empty. The decision is made only after every enabled report, audit log, and post-process action completes, and ignored extensions or other suppressed/filtered differences do not trigger the gate. Affected: `Runner/CliParser.cs`, `Runner/CliOptions.cs`, `ProgramRunner.cs`, `Runner/ProgramRunner.Types.cs`, `Runner/ProgramRunner.Wizard.cs`, `Runner/ProgramRunner.HelpText.cs`, `README.md`, `USER_GUIDE.md`. Tests: `CliOptionsTests`, `ProgramRunnerTests`. + #### Changed - **CLI parsing now returns one structured result and rejects surplus positional arguments** — `CliParser` now separates `oldFolder`, `newFolder`, and optional `reportLabel` while consuming options in one pass. Existing two- and three-positional forms, option placement, automatic labels, and `--creator` behavior are preserved; a fourth positional argument now prints usage and exits with code `2`. `CliOptions` uses named properties with defaults instead of a 35-field positional constructor. Affected: `Runner/CliParser.cs`, `Runner/CliOptions.cs`, `ProgramRunner.cs`, `Runner/ProgramRunner.Wizard.cs`, `Runner/RunPreflightValidator.cs`, `USER_GUIDE.md`. Tests: `CliOptionsTests`, `ProgramRunnerTests`, `CliOverrideApplierTests`, `SpinnerThemesTests`. @@ -1670,6 +1674,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### [Unreleased] +#### 追加 + +- **opt-in の `--fail-on-diff` CI ゲート** — 正常に完了した比較は既定で従来どおり `0` を返します。`--fail-on-diff` を指定した場合は、最終的なレポート対象の Added/Removed/Modified が空でなければ専用終了コード `5` を返します。判定は有効なレポート、監査ログ、ポストプロセス処理をすべて完了した後にだけ行い、無視拡張子やその他の抑制・フィルタ済み差分はゲートを発火させません。対象: `Runner/CliParser.cs`, `Runner/CliOptions.cs`, `ProgramRunner.cs`, `Runner/ProgramRunner.Types.cs`, `Runner/ProgramRunner.Wizard.cs`, `Runner/ProgramRunner.HelpText.cs`, `README.md`, `USER_GUIDE.md`。テスト: `CliOptionsTests`, `ProgramRunnerTests`。 + #### 変更 - **CLI 解析を単一の構造化結果へ集約し、余分な位置引数を拒否** — `CliParser` はオプションを 1 回の走査で消費しながら、`oldFolder`、`newFolder`、任意の `reportLabel` を分離するようになりました。既存の 2/3 位置引数形式、オプション位置、自動ラベル、`--creator` の動作は維持し、4 個目の位置引数は使い方を表示して終了コード `2` で拒否します。`CliOptions` は 35 フィールドの位置指定コンストラクタではなく、既定値付きの名前付きプロパティを使用します。対象: `Runner/CliParser.cs`, `Runner/CliOptions.cs`, `ProgramRunner.cs`, `Runner/ProgramRunner.Wizard.cs`, `Runner/RunPreflightValidator.cs`, `USER_GUIDE.md`。テスト: `CliOptionsTests`, `ProgramRunnerTests`, `CliOverrideApplierTests`, `SpinnerThemesTests`。 diff --git a/FolderDiffIL4DotNet.Tests/CliOptionsTests.Combined.cs b/FolderDiffIL4DotNet.Tests/CliOptionsTests.Combined.cs index 9ec0ce6c..446f4c73 100644 --- a/FolderDiffIL4DotNet.Tests/CliOptionsTests.Combined.cs +++ b/FolderDiffIL4DotNet.Tests/CliOptionsTests.Combined.cs @@ -21,6 +21,7 @@ public void ParseCliOptions_AllFlagsCombined_ParsedCorrectly() "/old", "/new", "lbl", "--no-pause", "--no-il-cache", "--skip-il", "--no-timestamp-warnings", "--creator", "--creator-il-ignore-profile", "buildserver-winforms", "--print-config", "--dry-run", + "--fail-on-diff", "--coffee", "--bell", "--wizard", "--log-format", "json", }); @@ -33,6 +34,7 @@ public void ParseCliOptions_AllFlagsCombined_ParsedCorrectly() Assert.Equal("buildserver-winforms", opts.CreatorIlIgnoreProfile); Assert.True(opts.PrintConfig); Assert.True(opts.DryRun); + Assert.True(opts.FailOnDiff); Assert.True(opts.Coffee); Assert.True(opts.Bell); Assert.True(opts.Wizard); diff --git a/FolderDiffIL4DotNet.Tests/CliOptionsTests.cs b/FolderDiffIL4DotNet.Tests/CliOptionsTests.cs index 31fadc99..cbcfd9cb 100644 --- a/FolderDiffIL4DotNet.Tests/CliOptionsTests.cs +++ b/FolderDiffIL4DotNet.Tests/CliOptionsTests.cs @@ -33,6 +33,7 @@ public void ParseCliOptions_NullArgs_ReturnsAllDefaults() Assert.Null(opts.CreatorIlIgnoreProfile); Assert.False(opts.PrintConfig); Assert.False(opts.DryRun); + Assert.False(opts.FailOnDiff); Assert.False(opts.Coffee); Assert.False(opts.Beer); Assert.False(opts.Matcha); @@ -85,6 +86,7 @@ public void ParseCliOptions_PositionalArgsOnly_ReturnsAllDefaultFlags() Assert.Null(opts.CreatorIlIgnoreProfile); Assert.False(opts.PrintConfig); Assert.False(opts.DryRun); + Assert.False(opts.FailOnDiff); Assert.False(opts.Coffee); Assert.False(opts.Beer); Assert.False(opts.Matcha); @@ -177,6 +179,17 @@ public void ParseCliOptions_DoctorFlag_SetsDoctor(string arg) Assert.Null(opts.ParseError); } + [Theory] + [InlineData("--fail-on-diff")] + [InlineData("--FAIL-ON-DIFF")] + public void ParseCliOptions_FailOnDiffFlag_SetsFailOnDiff(string arg) + { + var opts = CliParser.Parse(new[] { arg }); + + Assert.True(opts.FailOnDiff); + Assert.Null(opts.ParseError); + } + // ----------------------------------------------------------------------- // --credits // ----------------------------------------------------------------------- diff --git a/FolderDiffIL4DotNet.Tests/ProgramRunnerTests.FailOnDiff.cs b/FolderDiffIL4DotNet.Tests/ProgramRunnerTests.FailOnDiff.cs new file mode 100644 index 00000000..e7a4cbc4 --- /dev/null +++ b/FolderDiffIL4DotNet.Tests/ProgramRunnerTests.FailOnDiff.cs @@ -0,0 +1,157 @@ +// ProgramRunnerTests.FailOnDiff.cs — --fail-on-diff CLI gating integration tests +// ProgramRunnerTests.FailOnDiff.cs — --fail-on-diff CLI ゲートの統合テスト + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using FolderDiffIL4DotNet.Services; +using FolderDiffIL4DotNet.Tests.Helpers; +using Xunit; + +namespace FolderDiffIL4DotNet.Tests +{ + public sealed partial class ProgramRunnerTests + { + [Fact] + public async Task RunAsync_ReportableDifferencesWithoutFailOnDiff_ReturnsSuccess() + { + string tempRoot = CreateFailOnDiffTempRoot(); + try + { + string oldDir = CreateDirectory(tempRoot, "old"); + string newDir = CreateDirectory(tempRoot, "new"); + File.WriteAllText(Path.Combine(newDir, "added.txt"), "added"); + + await WithConfigFileAsync("""{"SkipIL":true}""", async () => + { + var result = await RunFailOnDiffComparisonAsync(tempRoot, oldDir, newDir, includeFailOnDiff: false); + + Assert.Equal(0, result.ExitCode); + Assert.True(File.Exists(Path.Combine(result.ReportDirectory, "diff_report.md"))); + }); + } + finally + { + TryDeleteDirectory(tempRoot); + } + } + + [Fact] + public async Task RunAsync_FailOnDiffWithoutReportableDifferences_ReturnsSuccess() + { + string tempRoot = CreateFailOnDiffTempRoot(); + try + { + string oldDir = CreateDirectory(tempRoot, "old"); + string newDir = CreateDirectory(tempRoot, "new"); + File.WriteAllText(Path.Combine(oldDir, "same.txt"), "same"); + File.WriteAllText(Path.Combine(newDir, "same.txt"), "same"); + + await WithConfigFileAsync("""{"SkipIL":true}""", async () => + { + var result = await RunFailOnDiffComparisonAsync(tempRoot, oldDir, newDir, includeFailOnDiff: true); + + Assert.Equal(0, result.ExitCode); + Assert.True(File.Exists(Path.Combine(result.ReportDirectory, "diff_report.md"))); + }); + } + finally + { + TryDeleteDirectory(tempRoot); + } + } + + [Fact] + public async Task RunAsync_FailOnDiffWithFinalAddedRemovedAndModifiedEntries_ReturnsFiveAfterGeneratingArtifacts() + { + string tempRoot = CreateFailOnDiffTempRoot(); + try + { + string oldDir = CreateDirectory(tempRoot, "old"); + string newDir = CreateDirectory(tempRoot, "new"); + File.WriteAllText(Path.Combine(oldDir, "removed.txt"), "removed"); + File.WriteAllText(Path.Combine(newDir, "added.txt"), "added"); + File.WriteAllText(Path.Combine(oldDir, "modified.txt"), "before"); + File.WriteAllText(Path.Combine(newDir, "modified.txt"), "after"); + + await WithConfigFileAsync("""{"SkipIL":true}""", async () => + { + var result = await RunFailOnDiffComparisonAsync(tempRoot, oldDir, newDir, includeFailOnDiff: true); + + Assert.Equal(5, result.ExitCode); + Assert.True(File.Exists(Path.Combine(result.ReportDirectory, "diff_report.md"))); + Assert.True(File.Exists(Path.Combine(result.ReportDirectory, "diff_report.html"))); + Assert.True(File.Exists(Path.Combine(result.ReportDirectory, AuditLogGenerateService.AUDIT_LOG_FILE_NAME))); + }); + } + finally + { + TryDeleteDirectory(tempRoot); + } + } + + [Fact] + public async Task RunAsync_FailOnDiffWithOnlyIgnoredExtensionDifference_ReturnsSuccess() + { + string tempRoot = CreateFailOnDiffTempRoot(); + try + { + string oldDir = CreateDirectory(tempRoot, "old"); + string newDir = CreateDirectory(tempRoot, "new"); + File.WriteAllText(Path.Combine(newDir, "ignored.tmp"), "ignored"); + + await WithConfigFileAsync("""{"SkipIL":true,"IgnoredExtensions":[".tmp"]}""", async () => + { + var result = await RunFailOnDiffComparisonAsync(tempRoot, oldDir, newDir, includeFailOnDiff: true); + + Assert.Equal(0, result.ExitCode); + Assert.True(File.Exists(Path.Combine(result.ReportDirectory, "diff_report.md"))); + }); + } + finally + { + TryDeleteDirectory(tempRoot); + } + } + + private static string CreateFailOnDiffTempRoot() + => Path.Combine(Path.GetTempPath(), "fd-fail-on-diff-" + Guid.NewGuid().ToString("N")); + + private static string CreateDirectory(string root, string name) + { + string path = Path.Combine(root, name); + Directory.CreateDirectory(path); + return path; + } + + private static async Task<(int ExitCode, string ReportDirectory)> RunFailOnDiffComparisonAsync( + string tempRoot, + string oldDir, + string newDir, + bool includeFailOnDiff) + { + string reportsRoot = CreateDirectory(tempRoot, "reports"); + string reportLabel = "report_" + Guid.NewGuid().ToString("N"); + var args = new List + { + oldDir, + newDir, + reportLabel, + "--skip-il", + "--no-pause", + "--no-banner", + "--output", + reportsRoot, + }; + if (includeFailOnDiff) + { + args.Add("--fail-on-diff"); + } + + var runner = new ProgramRunner(new TestLogger(logFileAbsolutePath: "test.log"), new ConfigService()); + int exitCode = await runner.RunAsync(args.ToArray()); + return (exitCode, Path.Combine(reportsRoot, reportLabel)); + } + } +} diff --git a/FolderDiffIL4DotNet.Tests/ProgramRunnerTests.HelpVersion.cs b/FolderDiffIL4DotNet.Tests/ProgramRunnerTests.HelpVersion.cs index ea3e38bb..a047b688 100644 --- a/FolderDiffIL4DotNet.Tests/ProgramRunnerTests.HelpVersion.cs +++ b/FolderDiffIL4DotNet.Tests/ProgramRunnerTests.HelpVersion.cs @@ -46,6 +46,8 @@ public async Task RunAsync_HelpFlag_ExitsZeroWithoutInitializingLogger() Assert.Contains("--skip-il", output, StringComparison.Ordinal); Assert.Contains("--no-banner", output, StringComparison.Ordinal); Assert.Contains("--doctor", output, StringComparison.Ordinal); + Assert.Contains("--fail-on-diff", output, StringComparison.Ordinal); + Assert.Contains("5 Reportable differences found", output, StringComparison.Ordinal); Assert.Contains("env+supported CLI overrides", output, StringComparison.Ordinal); Assert.Contains("without semantic validation", output, StringComparison.Ordinal); Assert.Contains("config.json + env overrides before runtime CLI overrides", output, StringComparison.Ordinal); diff --git a/ProgramRunner.cs b/ProgramRunner.cs index 9402de01..a3a136cb 100644 --- a/ProgramRunner.cs +++ b/ProgramRunner.cs @@ -118,7 +118,7 @@ public async Task RunAsync(string[] args) } PromptForExitKeyIfNeeded(opts); - return (int)result.ExitCode; + return (int)result.ResolveExitCode(opts.FailOnDiff); } /// diff --git a/README.md b/README.md index 0de99d4c..2d29dadf 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Common options: | `--output ` | Write reports under a custom output directory. | | `--skip-il` | Skip IL comparison and use SHA256/text comparison only. | | `--threads ` | Override comparison parallelism for this run. | +| `--fail-on-diff` | After generating all artifacts, exit with code `5` when final Added/Removed/Modified entries remain. | | `--print-config` | Print the effective builder state after env-var and supported CLI overrides without semantic validation. | | `--validate-config` | Validate `config.json` plus `FOLDERDIFF_*` environment-variable overrides before runtime CLI overrides are applied. | | `--open-reports` | Open the reports folder and exit. | @@ -188,6 +189,7 @@ nildiff [report-label] [options] | `--output ` | カスタム出力ディレクトリ配下にレポートを書き出します。 | | `--skip-il` | IL 比較をスキップし、SHA256/text 比較のみを使います。 | | `--threads ` | この実行だけ比較並列度を上書きします。 | +| `--fail-on-diff` | 全成果物の生成後、最終的な Added/Removed/Modified が残る場合にコード `5` で終了します。 | | `--print-config` | 環境変数と対応 CLI オーバーライドを適用した builder 状態を、セマンティック検証なしでそのまま出力するため、範囲外を含む effective config の診断にも使えます。 | | `--validate-config` | [`config.json`](config.json) に `FOLDERDIFF_*` 環境変数オーバーライドを適用した状態を、実行時 CLI オーバーライド適用前に検証します。 | | `--open-reports` | レポートフォルダを開いて終了します。 | diff --git a/Runner/CliOptions.cs b/Runner/CliOptions.cs index 600ac561..8fbfff73 100644 --- a/Runner/CliOptions.cs +++ b/Runner/CliOptions.cs @@ -26,6 +26,7 @@ internal sealed record CliOptions internal bool PrintConfig { get; init; } internal bool ValidateConfig { get; init; } internal bool DryRun { get; init; } + internal bool FailOnDiff { get; init; } internal bool Coffee { get; init; } internal bool Beer { get; init; } internal bool Matcha { get; init; } diff --git a/Runner/CliParser.cs b/Runner/CliParser.cs index 70a65860..5567d90d 100644 --- a/Runner/CliParser.cs +++ b/Runner/CliParser.cs @@ -29,6 +29,7 @@ internal static class CliParser private const string OPT_PRINT_CONFIG = "--print-config"; private const string OPT_VALIDATE_CONFIG = "--validate-config"; private const string OPT_DRY_RUN = "--dry-run"; + private const string OPT_FAIL_ON_DIFF = "--fail-on-diff"; private const string OPT_COFFEE = "--coffee"; private const string OPT_BEER = "--beer"; private const string OPT_MATCHA = "--matcha"; @@ -56,7 +57,7 @@ internal static class CliParser internal static CliOptions Parse(string[] args) { bool showHelp = false, showVersion = false, showBanner = false, noBanner = false, doctor = false, noPause = false; - bool noIlCache = false, clearCache = false, skipIl = false, noTimestampWarnings = false, printConfig = false, validateConfig = false, dryRun = false; + bool noIlCache = false, clearCache = false, skipIl = false, noTimestampWarnings = false, printConfig = false, validateConfig = false, dryRun = false, failOnDiff = false; bool coffee = false, beer = false, matcha = false, whisky = false, wine = false, ramen = false, sushi = false, bell = false, wizard = false, showCredits = false; bool randomSpinner = false; bool creator = false; @@ -181,6 +182,9 @@ internal static CliOptions Parse(string[] args) case OPT_DRY_RUN: dryRun = true; break; + case OPT_FAIL_ON_DIFF: + failOnDiff = true; + break; case OPT_COFFEE: // Last-wins: clear other spinner flags so CLI order determines winner // 最後勝ち: 他のスピナーフラグをクリアしてCLI引数順で決定 @@ -314,6 +318,7 @@ internal static CliOptions Parse(string[] args) PrintConfig = printConfig, ValidateConfig = validateConfig, DryRun = dryRun, + FailOnDiff = failOnDiff, Coffee = coffee, Beer = beer, Matcha = matcha, diff --git a/Runner/ProgramRunner.HelpText.cs b/Runner/ProgramRunner.HelpText.cs index 06fa9094..4af767e2 100644 --- a/Runner/ProgramRunner.HelpText.cs +++ b/Runner/ProgramRunner.HelpText.cs @@ -37,6 +37,8 @@ public sealed partial class ProgramRunner " Shows existing report folders before the label prompt; press Enter to auto-generate.\n" + " Drag-and-drop friendly.\n" + " --dry-run Enumerate files and show statistics without running comparison.\n" + + " --fail-on-diff Exit with code 5 when final reportable differences remain.\n" + + " Reports and other artifacts are still generated before exit.\n" + " --coffee Use coffee-themed spinner animation during execution.\n" + " --beer Use beer-themed spinner animation during execution.\n" + " --matcha Use matcha tea ceremony spinner animation during execution.\n" + @@ -68,6 +70,7 @@ public sealed partial class ProgramRunner " 2 Invalid arguments or input paths.\n" + " 3 Configuration load or parse error.\n" + " 4 Diff execution/report generation failure, or --doctor with no IL disassembler.\n" + + " 5 Reportable differences found with --fail-on-diff.\n" + " 1 Unexpected internal error.\n\n" + "Tip:\n" + " Use --print-config to display the effective configuration\n" + diff --git a/Runner/ProgramRunner.Types.cs b/Runner/ProgramRunner.Types.cs index 7aa34fc8..1a313319 100644 --- a/Runner/ProgramRunner.Types.cs +++ b/Runner/ProgramRunner.Types.cs @@ -41,6 +41,12 @@ private enum ProgramExitCode /// ExecutionFailed = 4, + /// + /// Reportable differences were found when requested by --fail-on-diff. + /// --fail-on-diff 指定時にレポート対象の差分が見つかりました。 + /// + DifferencesFound = 5, + /// /// Unclassifiable unexpected error. / 分類不能な想定外エラーです。 /// @@ -70,6 +76,14 @@ public static ProgramRunResult Success(RunCompletionState completionState) public static ProgramRunResult Failure(ProgramExitCode exitCode) => new(exitCode, _noWarnings); + public ProgramExitCode ResolveExitCode(bool failOnDiff) + { + bool hasReportableDifferences = AddedCount > 0 || RemovedCount > 0 || ModifiedCount > 0; + return ExitCode == ProgramExitCode.Success && failOnDiff && hasReportableDifferences + ? ProgramExitCode.DifferencesFound + : ExitCode; + } + private ProgramRunResult(ProgramExitCode exitCode, RunCompletionState completionState) { ExitCode = exitCode; diff --git a/Runner/ProgramRunner.Wizard.cs b/Runner/ProgramRunner.Wizard.cs index 5a3676d7..62bfc67b 100644 --- a/Runner/ProgramRunner.Wizard.cs +++ b/Runner/ProgramRunner.Wizard.cs @@ -96,7 +96,7 @@ private async Task RunWizardAsync(Runner.CliOptions opts) }; var result = await RunWithResultAsync(runOptions); OutputCompletionWarnings(result.HasSha256MismatchWarnings, result.HasTimestampRegressionWarnings, result.HasILFilterWarnings); - return (int)result.ExitCode; + return (int)result.ResolveExitCode(runOptions.FailOnDiff); } /// diff --git a/USER_GUIDE.md b/USER_GUIDE.md index ef9f9510..4a260dd1 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -206,6 +206,7 @@ Normal diff runs accept exactly two or three positional arguments. A fourth posi | `--creator-il-ignore-profile ` | Apply a maintainer-managed IL ignore profile and force [`ShouldIgnoreILLinesContainingConfiguredStrings`](#config-en-shouldignoreillinescontainingconfiguredstrings) to `true`. The profile strings are merged into [`ILIgnoreLineContainingStrings`](#config-en-ilignorelinecontainingstrings). Current built-in profile: `buildserver-winforms`. | | `--wizard` | Interactive mode: prompts for old folder, new folder, and an optional report label. Before the report-label prompt, it prints the existing report folder names under the active Reports root so you can avoid collisions or reuse part of an existing label. Press Enter on the report-label prompt to auto-generate a high-resolution timestamp label. Drag-and-drop friendly — auto-strips surrounding quotes, `file://` URI prefixes, backslash-escaped spaces, and percent-encoded characters. | | `--dry-run` | Enumerate files and show statistics without running comparison. | +| `--fail-on-diff` | Opt in to CI gating: after all reports, audit logs, and other enabled artifacts are generated, return exit code `5` when final reportable Added/Removed/Modified entries remain. Differences removed by `IgnoredExtensions`, IL-noise suppression, or other comparison filters do not trigger code `5`. Without this flag, a completed comparison still returns `0` even when it reports differences. | | `--coffee` | Use coffee-themed spinner animation during execution (easter egg). | | `--beer` | Use beer-themed spinner animation during execution (easter egg). | | `--matcha` | Use matcha tea ceremony spinner animation during execution (easter egg). | @@ -236,6 +237,9 @@ dotnet run "/path/old" "/path/new" "label" --config /etc/my-config.json --no-pau # Omit the report label to auto-generate a high-resolution timestamp dotnet run "/path/old" "/path/new" --no-pause +# Generate every artifact, then fail a CI step only if reportable differences remain +dotnet run "/path/old" "/path/new" "ci-gate" --fail-on-diff --no-pause + # Inspect the effective configuration (config.json + env vars + supported CLI overrides) without running a diff dotnet run -- --print-config dotnet run -- --config /etc/my-config.json --print-config @@ -255,6 +259,9 @@ Process exit codes: | `2` | Invalid arguments or input paths | Missing positional args, non-existent directories, illegal report label, preflight failures (see below) | | `3` | Configuration error | JSON syntax error, missing config file, malformed `--config` path, semantic validation failure (`--validate-config` invalid) | | `4` | Execution failure | Runtime error during diff comparison or report generation, `--doctor` with no available IL disassembler, or `--open-*` launcher/path-creation failure | +| `5` | Reportable differences found | A completed run with `--fail-on-diff` has one or more final Added/Removed/Modified entries; all enabled artifacts have already been generated | + +`--fail-on-diff` gates on the same final Added/Removed/Modified sets written to the reports. Ignored files, suppressed IL-only noise, and other filtered-out entries do not produce code `5`. Before loading configuration, three preflight checks run against the reports output path (all failures produce exit code `2`): 1. **Path length** — the constructed `Reports/