@cratis/pi 7.102.0 → 7.103.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/package/corpus/harnesses/copilot/instructions/profiles.instructions.md +10 -1
  2. package/package/corpus/harnesses/copilot/instructions/pull-requests.instructions.md +2 -0
  3. package/package/corpus/harnesses/copilot/instructions/secrets.instructions.md +4 -0
  4. package/package/corpus/hooks/README.md +11 -1
  5. package/package/corpus/hooks/scripts/cratis-check-pr.mjs +12 -1
  6. package/package/corpus/hooks/scripts/cratis-guard-secrets.sh +79 -23
  7. package/package/corpus/hooks/scripts/cratis-guard-writes.sh +145 -40
  8. package/package/corpus/rules/profiles.md +10 -1
  9. package/package/corpus/rules/pull-requests.md +2 -0
  10. package/package/corpus/rules/secrets.md +4 -0
  11. package/package/corpus/skills/analyzing-dotnet-performance/LICENSE +23 -0
  12. package/package/corpus/skills/analyzing-dotnet-performance/NOTICE +36 -0
  13. package/package/corpus/skills/analyzing-dotnet-performance/SKILL.md +214 -0
  14. package/package/corpus/skills/analyzing-dotnet-performance/references/async-patterns.md +122 -0
  15. package/package/corpus/skills/analyzing-dotnet-performance/references/collections-and-linq.md +220 -0
  16. package/package/corpus/skills/analyzing-dotnet-performance/references/critical-patterns.md +291 -0
  17. package/package/corpus/skills/analyzing-dotnet-performance/references/io-and-serialization.md +135 -0
  18. package/package/corpus/skills/analyzing-dotnet-performance/references/memory-and-strings.md +219 -0
  19. package/package/corpus/skills/analyzing-dotnet-performance/references/regex-patterns.md +95 -0
  20. package/package/corpus/skills/analyzing-dotnet-performance/references/structural-patterns.md +38 -0
  21. package/package/corpus/skills/binlog-generation/LICENSE +23 -0
  22. package/package/corpus/skills/binlog-generation/NOTICE +22 -0
  23. package/package/corpus/skills/binlog-generation/SKILL.md +129 -0
  24. package/package/corpus/skills/check-bin-obj-clash/LICENSE +23 -0
  25. package/package/corpus/skills/check-bin-obj-clash/NOTICE +27 -0
  26. package/package/corpus/skills/check-bin-obj-clash/SKILL.md +127 -0
  27. package/package/corpus/skills/check-bin-obj-clash/references/common-causes-and-fixes.md +206 -0
  28. package/package/corpus/skills/coverage-analysis/LICENSE +23 -0
  29. package/package/corpus/skills/coverage-analysis/NOTICE +32 -0
  30. package/package/corpus/skills/coverage-analysis/SKILL.md +198 -0
  31. package/package/corpus/skills/coverage-analysis/references/guidelines.md +68 -0
  32. package/package/corpus/skills/coverage-analysis/references/output-format.md +91 -0
  33. package/package/corpus/skills/coverage-analysis/references/report-generation.md +58 -0
  34. package/package/corpus/skills/coverage-analysis/references/setup-discovery.md +307 -0
  35. package/package/corpus/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 +80 -0
  36. package/package/corpus/skills/coverage-analysis/scripts/CoverageData.ps1 +272 -0
  37. package/package/corpus/skills/coverage-analysis/scripts/Extract-MethodCoverage.ps1 +65 -0
  38. package/package/corpus/skills/cratis-screenplay-command-surface/SKILL.md +26 -1
  39. package/package/corpus/skills/cratis-screenplay-projections/SKILL.md +17 -0
  40. package/package/corpus/skills/cratis-screenplay-render-and-gap-fill/SKILL.md +17 -0
  41. package/package/corpus/skills/cratis-screenplay-specifications/SKILL.md +8 -0
  42. package/package/corpus/skills/cratis-screenplay-toolchain/SKILL.md +1 -1
  43. package/package/corpus/skills/cratis-screenplay-toolchain/references/cheat-sheet-example.md +1 -0
  44. package/package/corpus/skills/cratis-screenplay-toolchain/references/fast-path.md +7 -0
  45. package/package/corpus/skills/cratis-screenplay-toolchain/references/traps.md +1 -0
  46. package/package/corpus/skills/cratis-screenplay-ui-composition/SKILL.md +40 -0
  47. package/package/corpus/skills/cratis-screenplay-ui-composition/references/response-continuations.md +1 -0
  48. package/package/corpus/skills/cratis-screenplay-ui-composition/references/worked-flow.md +1 -0
  49. package/package/corpus/skills/cratis-stage-rendering-and-sandbox/references/admission.md +3 -2
  50. package/package/corpus/skills/csharp-refactoring/LICENSE +23 -0
  51. package/package/corpus/skills/csharp-refactoring/NOTICE +22 -0
  52. package/package/corpus/skills/csharp-refactoring/SKILL.md +175 -0
  53. package/package/corpus/skills/csharp-refactoring/references/operation-catalog.md +37 -0
  54. package/package/corpus/skills/directory-build-organization/LICENSE +23 -0
  55. package/package/corpus/skills/directory-build-organization/NOTICE +21 -0
  56. package/package/corpus/skills/directory-build-organization/SKILL.md +205 -0
  57. package/package/corpus/skills/directory-build-organization/references/common-patterns.md +56 -0
  58. package/package/corpus/skills/directory-build-organization/references/multi-level-examples.md +167 -0
  59. package/package/corpus/skills/directory-build-organization/references/targetframework-props-pitfall.md +54 -0
  60. package/package/corpus/skills/dotnet-aot-compat/LICENSE +23 -0
  61. package/package/corpus/skills/dotnet-aot-compat/NOTICE +18 -0
  62. package/package/corpus/skills/dotnet-aot-compat/SKILL.md +265 -0
  63. package/package/corpus/skills/dotnet-aot-compat/references/polyfills.md +43 -0
  64. package/package/corpus/skills/dotnet-trace-collect/LICENSE +23 -0
  65. package/package/corpus/skills/dotnet-trace-collect/NOTICE +65 -0
  66. package/package/corpus/skills/dotnet-trace-collect/SKILL.md +116 -0
  67. package/package/corpus/skills/dotnet-trace-collect/references/dotnet-monitor.md +97 -0
  68. package/package/corpus/skills/dotnet-trace-collect/references/dotnet-trace-collect-linux.md +84 -0
  69. package/package/corpus/skills/dotnet-trace-collect/references/dotnet-trace-collect.md +91 -0
  70. package/package/corpus/skills/dotnet-trace-collect/references/perfcollect.md +200 -0
  71. package/package/corpus/skills/dotnet-trace-collect/references/perfview.md +141 -0
  72. package/package/corpus/skills/dotnet-trace-collect/references/tool-selection.md +160 -0
  73. package/package/corpus/skills/dump-collect/LICENSE +23 -0
  74. package/package/corpus/skills/dump-collect/NOTICE +40 -0
  75. package/package/corpus/skills/dump-collect/SKILL.md +97 -0
  76. package/package/corpus/skills/dump-collect/references/container-dumps.md +412 -0
  77. package/package/corpus/skills/dump-collect/references/coreclr-dumps.md +108 -0
  78. package/package/corpus/skills/dump-collect/references/nativeaot-dumps.md +280 -0
  79. package/package/corpus/skills/eliminate-redundant-restores/LICENSE +23 -0
  80. package/package/corpus/skills/eliminate-redundant-restores/NOTICE +19 -0
  81. package/package/corpus/skills/eliminate-redundant-restores/SKILL.md +112 -0
  82. package/package/corpus/skills/extension-points/LICENSE +23 -0
  83. package/package/corpus/skills/extension-points/NOTICE +18 -0
  84. package/package/corpus/skills/extension-points/SKILL.md +248 -0
  85. package/package/corpus/skills/filter-syntax/LICENSE +23 -0
  86. package/package/corpus/skills/filter-syntax/NOTICE +17 -0
  87. package/package/corpus/skills/filter-syntax/SKILL.md +179 -0
  88. package/package/corpus/skills/including-generated-files/LICENSE +23 -0
  89. package/package/corpus/skills/including-generated-files/NOTICE +17 -0
  90. package/package/corpus/skills/including-generated-files/SKILL.md +139 -0
  91. package/package/corpus/skills/microbenchmarking/LICENSE +23 -0
  92. package/package/corpus/skills/microbenchmarking/NOTICE +36 -0
  93. package/package/corpus/skills/microbenchmarking/SKILL.md +131 -0
  94. package/package/corpus/skills/microbenchmarking/references/bdn-internals-and-tuning.md +143 -0
  95. package/package/corpus/skills/microbenchmarking/references/comparison-strategies.md +200 -0
  96. package/package/corpus/skills/microbenchmarking/references/diagnosers-and-exporters.md +150 -0
  97. package/package/corpus/skills/microbenchmarking/references/project-setup-and-running.md +189 -0
  98. package/package/corpus/skills/microbenchmarking/references/writing-benchmarks.md +310 -0
  99. package/package/corpus/skills/msbuild/LICENSE +23 -0
  100. package/package/corpus/skills/msbuild/NOTICE +55 -0
  101. package/package/corpus/skills/msbuild/SKILL.md +117 -0
  102. package/package/corpus/skills/msbuild/references/antipatterns/additional-antipatterns.md +291 -0
  103. package/package/corpus/skills/msbuild/references/antipatterns/incremental-build-inputs-outputs.md +57 -0
  104. package/package/corpus/skills/msbuild/references/antipatterns/private-assets.md +29 -0
  105. package/package/corpus/skills/msbuild/references/binlog-failure-analysis.md +85 -0
  106. package/package/corpus/skills/msbuild/references/binlog-generation.md +69 -0
  107. package/package/corpus/skills/msbuild/references/build-perf-baseline.md +493 -0
  108. package/package/corpus/skills/msbuild/references/build-perf-diagnostics.md +268 -0
  109. package/package/corpus/skills/msbuild/references/copy-to-output-directory.md +183 -0
  110. package/package/corpus/skills/msbuild/references/eval-performance.md +190 -0
  111. package/package/corpus/skills/msbuild/references/extension-points.md +281 -0
  112. package/package/corpus/skills/msbuild/references/incremental-build.md +365 -0
  113. package/package/corpus/skills/msbuild/references/msbuild-antipatterns.md +401 -0
  114. package/package/corpus/skills/orleans/LICENSE +21 -0
  115. package/package/corpus/skills/orleans/NOTICE +64 -0
  116. package/package/corpus/skills/orleans/SKILL.md +140 -0
  117. package/package/corpus/skills/orleans/references/anti-patterns.md +660 -0
  118. package/package/corpus/skills/orleans/references/configuration-api.md +933 -0
  119. package/package/corpus/skills/orleans/references/examples.md +66 -0
  120. package/package/corpus/skills/orleans/references/grain-api.md +784 -0
  121. package/package/corpus/skills/orleans/references/grains.md +75 -0
  122. package/package/corpus/skills/orleans/references/hosting.md +71 -0
  123. package/package/corpus/skills/orleans/references/implementation.md +591 -0
  124. package/package/corpus/skills/orleans/references/official-docs-index.md +250 -0
  125. package/package/corpus/skills/orleans/references/patterns.md +811 -0
  126. package/package/corpus/skills/orleans/references/persistence-api.md +495 -0
  127. package/package/corpus/skills/orleans/references/primitive-selection.md +35 -0
  128. package/package/corpus/skills/orleans/references/scheduling-and-services.md +295 -0
  129. package/package/corpus/skills/orleans/references/serialization-api.md +299 -0
  130. package/package/corpus/skills/orleans/references/streaming-api.md +396 -0
  131. package/package/corpus/skills/orleans/references/testing-patterns.md +210 -0
  132. package/package/corpus/skills/platform-detection/LICENSE +23 -0
  133. package/package/corpus/skills/platform-detection/NOTICE +17 -0
  134. package/package/corpus/skills/platform-detection/SKILL.md +230 -0
  135. package/package/corpus/skills/platform-detection/references/command-mode.md +27 -0
  136. package/package/corpus/skills/profiling/LICENSE +21 -0
  137. package/package/corpus/skills/profiling/NOTICE +50 -0
  138. package/package/corpus/skills/profiling/SKILL.md +144 -0
  139. package/package/corpus/skills/profiling/references/commands.md +439 -0
  140. package/package/corpus/skills/profiling/references/patterns.md +586 -0
  141. package/package/corpus/skills/profiling/references/profiling.md +190 -0
  142. package/package/corpus/skills/run-tests/LICENSE +23 -0
  143. package/package/corpus/skills/run-tests/NOTICE +19 -0
  144. package/package/corpus/skills/run-tests/SKILL.md +305 -0
  145. package/package/corpus/skills/target-authoring/LICENSE +23 -0
  146. package/package/corpus/skills/target-authoring/NOTICE +17 -0
  147. package/package/corpus/skills/target-authoring/SKILL.md +165 -0
  148. package/package/corpus/skills/vectorization/LICENSE +23 -0
  149. package/package/corpus/skills/vectorization/NOTICE +17 -0
  150. package/package/corpus/skills/vectorization/SKILL.md +189 -0
  151. package/package/profile-catalog.json +85 -1
  152. package/package.json +1 -1
@@ -0,0 +1,58 @@
1
+ # Coverage Analysis — ReportGenerator HTML/CSV reports
2
+
3
+ Read this file **only** when the user explicitly asked for HTML/CSV reports and
4
+ the direct coverage/risk answer has already been delivered.
5
+
6
+ ## Verify or install ReportGenerator
7
+
8
+ ```powershell
9
+ $rgAvailable = $false
10
+ $rgCommand = Get-Command reportgenerator -ErrorAction SilentlyContinue
11
+ if ($rgCommand) {
12
+ $rgAvailable = $true
13
+ Write-Host "RG_INSTALLED:already-present"
14
+ } else {
15
+ $rgToolPath = Join-Path "<COVERAGE_DIR>" ".tools"
16
+ # Cratis: Downloads or installs a tool: requires the user's approval; prefer a pinned version (`dotnet tool install <tool> --version <x>`)
17
+ dotnet tool install dotnet-reportgenerator-globaltool --tool-path $rgToolPath
18
+ if ($LASTEXITCODE -eq 0) {
19
+ $env:PATH = "$rgToolPath$([System.IO.Path]::PathSeparator)$env:PATH"
20
+ $rgCommand = Get-Command reportgenerator -ErrorAction SilentlyContinue
21
+ if ($rgCommand) {
22
+ $rgAvailable = $true
23
+ Write-Host "RG_INSTALLED:true (tool-path: $rgToolPath)"
24
+ } else {
25
+ Write-Host "RG_INSTALLED:false"
26
+ Write-Host "RG_INSTALL_ERROR:reportgenerator-not-available"
27
+ }
28
+ } else {
29
+ Write-Host "RG_INSTALLED:false"
30
+ Write-Host "RG_INSTALL_ERROR:reportgenerator-not-available"
31
+ }
32
+ }
33
+ Write-Host "RG_AVAILABLE:$rgAvailable"
34
+ ```
35
+
36
+ If installation fails (no internet), keep `RG_AVAILABLE:false`, leave the existing user-facing summary as the final output, and note that HTML reports were skipped.
37
+
38
+ ## Step 7: Generate HTML/CSV reports
39
+
40
+ ```powershell
41
+ $reportsDir = Join-Path "<COVERAGE_DIR>" "reports"
42
+ if ($rgAvailable) {
43
+ reportgenerator `
44
+ -reports:"<semicolon-separated COBERTURA paths>" `
45
+ -targetdir:$reportsDir `
46
+ -reporttypes:"Html;TextSummary;MarkdownSummaryGithub;CsvSummary" `
47
+ -title:"Coverage Report" `
48
+ -tag:"coverage-analysis-skill"
49
+
50
+ Get-Content (Join-Path $reportsDir "Summary.txt") -ErrorAction SilentlyContinue
51
+ } else {
52
+ Write-Host "REPORTGENERATOR_SKIPPED:true"
53
+ }
54
+ ```
55
+
56
+ After report generation completes successfully, you may follow up with a short
57
+ message pointing the user to the generated HTML report (one paragraph, no need
58
+ to repeat the summary).
@@ -0,0 +1,307 @@
1
+ # Coverage Analysis — setup and discovery
2
+
3
+ ## Contents
4
+
5
+ - Step 1: Locate the solution or project
6
+ - Step 2: Create the output directory
7
+ - Step 2b: Discover or accept existing Cobertura XML (required for the existing-data path)
8
+ - Step 2c: Recommend ignoring `TestResults/`
9
+
10
+ Read this file only when the user did not supply usable coverage evidence and
11
+ the request requires workspace discovery or fresh collection. Do not read or run
12
+ these probes for a supplied excerpt or valid Cobertura path.
13
+
14
+ ## Step 1: Locate the solution or project
15
+
16
+ Given the user's path (default: current directory), find the entry point.
17
+ If the user or the repository's documented coverage command selects a specific
18
+ `.sln`, `.slnx`, `.slnf`, or `.csproj`, pass that exact file as `$root`; do not
19
+ replace it with a discovered alternative. Directory discovery stops on multiple
20
+ solutions or, when no solution exists, multiple projects. Request an explicit
21
+ entry point rather than selecting the first.
22
+
23
+ ```powershell
24
+ $root = "<user-or-repository-selected-file-or-directory>"
25
+ $requested = Get-Item -LiteralPath $root -ErrorAction Stop
26
+ $entry = $null
27
+ if (-not $requested.PSIsContainer) {
28
+ if ($requested.Extension -notin @('.sln', '.slnx', '.slnf', '.csproj')) {
29
+ throw "Unsupported entry point: $($requested.FullName)"
30
+ }
31
+ $entry = $requested
32
+ $root = $requested.DirectoryName
33
+ } else {
34
+ $root = $requested.FullName
35
+ $candidates = @(Get-ChildItem -LiteralPath $root -File -Recurse -Depth 2 -ErrorAction Stop |
36
+ Where-Object { $_.Extension -in @('.sln', '.slnx') -and $_.FullName -notmatch '[/\\](obj|bin)[/\\]' })
37
+ if ($candidates.Count -eq 0) {
38
+ $candidates = @(Get-ChildItem -LiteralPath $root -Filter "*.csproj" -File -Recurse -Depth 2 -ErrorAction Stop |
39
+ Where-Object { $_.FullName -notmatch '[/\\](obj|bin)[/\\]' })
40
+ }
41
+ if ($candidates.Count -gt 1) {
42
+ throw "Ambiguous entry point under $root. Specify one .sln, .slnx, .slnf, or .csproj: $($candidates.FullName -join ', ')"
43
+ }
44
+ if ($candidates.Count -eq 1) { $entry = $candidates[0] }
45
+ }
46
+ if ($entry) {
47
+ $entryType = if ($entry.Extension -eq '.csproj') { 'Project' } else { 'Solution' }
48
+ Write-Host "ENTRY_TYPE:$entryType"; Write-Host "ENTRY:$($entry.FullName)"
49
+ } else {
50
+ Write-Host "ENTRY_TYPE:NotFound"
51
+ }
52
+
53
+ # Test projects: search the requested directory, then its containing Git root only.
54
+ $searchRoots = @($root)
55
+ $gitOutput = @(git -C $root rev-parse --show-toplevel 2>&1)
56
+ if ($LASTEXITCODE -eq 0) {
57
+ $gitRoot = [string]$gitOutput[0]
58
+ } else {
59
+ $gitError = $gitOutput | Out-String
60
+ if ($gitError -notmatch 'not a git repository \(or any of the parent directories\): \.git') {
61
+ throw "Git root lookup failed: $gitError"
62
+ }
63
+ $gitRoot = $null
64
+ }
65
+ if ($gitRoot) { $gitRoot = [System.IO.Path]::GetFullPath($gitRoot) }
66
+ if ($gitRoot -and $gitRoot -ne $root) {
67
+ $sep = [System.IO.Path]::DirectorySeparatorChar
68
+ $prefix = $gitRoot.TrimEnd($sep) + $sep
69
+ if (-not $root.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
70
+ throw "Git root is not a containing directory: $gitRoot"
71
+ }
72
+ $searchRoots += $gitRoot
73
+ }
74
+
75
+ function Get-SolutionProjects($solution) {
76
+ $text = Get-Content -LiteralPath $solution.FullName -Raw -ErrorAction Stop
77
+ if ($solution.Extension -eq '.slnx') {
78
+ $xml = [xml]$text
79
+ if ($xml.DocumentElement.Name -ne 'Solution') { throw "Invalid solution: $($solution.FullName)" }
80
+ $paths = @($xml.SelectNodes('//Project[@Path]') | ForEach-Object { $_.Path })
81
+ } elseif ($solution.Extension -eq '.sln') {
82
+ if ($text -notmatch 'Microsoft Visual Studio Solution File, Format Version') {
83
+ throw "Invalid solution: $($solution.FullName)"
84
+ }
85
+ $paths = @([regex]::Matches($text, '(?m)^Project\("[^"]+"\)\s*=\s*"[^"]*",\s*"([^"]+)",') |
86
+ ForEach-Object { $_.Groups[1].Value } |
87
+ Where-Object { [System.IO.Path]::GetExtension($_) -eq '.csproj' })
88
+ } else {
89
+ throw "Unsupported underlying solution: $($solution.FullName)"
90
+ }
91
+ foreach ($path in $paths) {
92
+ if ([System.IO.Path]::GetExtension($path) -eq '.csproj') {
93
+ $relative = $path.Replace('\', [System.IO.Path]::DirectorySeparatorChar)
94
+ [System.IO.Path]::GetFullPath((Join-Path $solution.DirectoryName $relative))
95
+ }
96
+ }
97
+ }
98
+
99
+ # Scope before reading/classifying projects: solution membership, filter membership,
100
+ # or the single project that the selected test command actually targets.
101
+ if ($entry) {
102
+ if ($entry.Extension -eq '.csproj') {
103
+ $projectPaths = @($entry.FullName)
104
+ } elseif ($entry.Extension -eq '.slnf') {
105
+ $filter = Get-Content -LiteralPath $entry.FullName -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
106
+ if (-not $filter.solution.path -or $null -eq $filter.solution.projects) {
107
+ throw "Invalid solution filter: $($entry.FullName)"
108
+ }
109
+ $solutionPath = $filter.solution.path.Replace('\', [System.IO.Path]::DirectorySeparatorChar)
110
+ $solution = Get-Item -LiteralPath (Join-Path $entry.DirectoryName $solutionPath) -ErrorAction Stop
111
+ $members = @(Get-SolutionProjects $solution)
112
+ $projectPaths = @($filter.solution.projects | ForEach-Object {
113
+ $relative = $_.Replace('\', [System.IO.Path]::DirectorySeparatorChar)
114
+ $project = Get-Item -LiteralPath (Join-Path $solution.DirectoryName $relative) -ErrorAction Stop
115
+ if ($members -notcontains $project.FullName) { throw "Filtered project is not in the solution: $($project.FullName)" }
116
+ $project.FullName
117
+ })
118
+ } else {
119
+ $projectPaths = @(Get-SolutionProjects $entry)
120
+ }
121
+ $projects = @($projectPaths | Select-Object -Unique | ForEach-Object {
122
+ Get-Item -LiteralPath $_ -ErrorAction Stop
123
+ })
124
+ $testProjects = @($projects | Where-Object {
125
+ (Select-String -LiteralPath $_.FullName -Pattern 'Microsoft\.NET\.Test\.Sdk|xunit|nunit|MSTest\.TestAdapter|"MSTest"|MSTest\.TestFramework|TUnit' -Quiet -ErrorAction Stop) -or
126
+ $_.Name -match '(?i)(test|spec)'
127
+ })
128
+ } else {
129
+ $testProjects = @()
130
+ foreach ($sr in $searchRoots) {
131
+ $projects = @(Get-ChildItem -LiteralPath $sr -Filter "*.csproj" -File -Recurse -Depth 5 -ErrorAction Stop |
132
+ Where-Object { $_.FullName -notmatch '[/\\](obj|bin)[/\\]' })
133
+ $testProjects = @($projects | Where-Object {
134
+ Select-String -LiteralPath $_.FullName -Pattern 'Microsoft\.NET\.Test\.Sdk|xunit|nunit|MSTest\.TestAdapter|"MSTest"|MSTest\.TestFramework|TUnit' -Quiet -ErrorAction Stop
135
+ })
136
+ if ($testProjects.Count -eq 0) {
137
+ $testProjects = @($projects | Where-Object { $_.Name -match '(?i)(test|spec)' })
138
+ }
139
+ if ($testProjects.Count -gt 0) {
140
+ if ($sr -ne $root) { Write-Host "SEARCHED:$sr" }
141
+ break
142
+ }
143
+ }
144
+ }
145
+ Write-Host "TEST_PROJECTS:$($testProjects.Count)"
146
+ $testProjects | ForEach-Object { Write-Host "TEST_PROJECT:$($_.FullName)" }
147
+
148
+ # Project-system classification controls whether the automatic dotnet/provider path is safe.
149
+ $classicTestProjects = @($testProjects | Where-Object {
150
+ $text = Get-Content -LiteralPath $_.FullName -Raw -ErrorAction Stop
151
+ $hasSdk = $text -match '<Project[^>]+\bSdk\s*=' -or $text -match '<Sdk\b'
152
+ $hasPackagesConfig = Test-Path (Join-Path $_.DirectoryName "packages.config")
153
+ $hasClassicSignals = $text -match '\bToolsVersion\s*=' -or
154
+ $text -match 'Microsoft\.(Common\.props|CSharp\.targets)' -or
155
+ $text -match '<Compile\s+Include='
156
+ $hasPackagesConfig -or (-not $hasSdk -and $hasClassicSignals)
157
+ })
158
+ Write-Host "CLASSIC_TEST_PROJECTS:$($classicTestProjects.Count)"
159
+ $classicTestProjects | ForEach-Object { Write-Host "CLASSIC_TEST_PROJECT:$($_.FullName)" }
160
+ $sdkTestProjects = @($testProjects | Where-Object {
161
+ $classicTestProjects.FullName -notcontains $_.FullName
162
+ })
163
+ Write-Host "SDK_TEST_PROJECTS:$($sdkTestProjects.Count)"
164
+ $sdkTestProjects | ForEach-Object { Write-Host "SDK_TEST_PROJECT:$($_.FullName)" }
165
+
166
+ # Resolve the test output root (where coverage-analysis artifacts will be written)
167
+ if ($testProjects.Count -eq 0) {
168
+ if ($gitRoot) {
169
+ $testOutputRoot = $gitRoot
170
+ } else {
171
+ $testOutputRoot = $root
172
+ }
173
+ } elseif ($testProjects.Count -eq 1) {
174
+ $testOutputRoot = $testProjects[0].DirectoryName
175
+ } else {
176
+ # Multiple test projects — find their deepest common parent directory
177
+ $dirs = $testProjects | ForEach-Object { $_.DirectoryName }
178
+ $common = $dirs[0]
179
+ foreach ($d in $dirs[1..($dirs.Count-1)]) {
180
+ $sep = [System.IO.Path]::DirectorySeparatorChar
181
+ while (-not $d.StartsWith("$common$sep", [System.StringComparison]::OrdinalIgnoreCase) -and $d -ne $common) {
182
+ $prevCommon = $common
183
+ $common = Split-Path $common -Parent
184
+ # Terminate if we can no longer move up (at filesystem root or no parent)
185
+ if ([string]::IsNullOrEmpty($common) -or $common -eq $prevCommon) {
186
+ $common = $null
187
+ break
188
+ }
189
+ }
190
+ }
191
+ if ([string]::IsNullOrEmpty($common)) {
192
+ # Fallback when no common parent directory exists (e.g., projects on different drives)
193
+ if ($gitRoot) {
194
+ $testOutputRoot = $gitRoot
195
+ } else {
196
+ $testOutputRoot = $root
197
+ }
198
+ } else {
199
+ $testOutputRoot = $common
200
+ }
201
+ }
202
+ Write-Host "TEST_OUTPUT_ROOT:$testOutputRoot"
203
+ ```
204
+
205
+ - If `ENTRY_TYPE:NotFound` and SDK-style test projects were found → use the test projects directly as `dotnet test` entry points.
206
+ - If `ENTRY_TYPE:NotFound` and classic test projects were found → use only the repository's documented coverage command; do not infer `dotnet test`.
207
+ - If `ENTRY_TYPE:NotFound` and no test projects found → stop: `No .sln, .slnx, or test projects found under <path>. Provide the path to your .NET solution, filter, or project.`
208
+ - A missing/unreadable requested path, failed lookup, or ambiguous entry point is a discovery blocker, not measured zero coverage or evidence of an empty suite. Do not continue collection after a discovery error.
209
+ - Discovery is an inventory, not permission to expand collection scope. Pass the
210
+ exact selected entry point to `run-tests` and collect only its requested
211
+ project graph. Do not replace a selected `.csproj` or `.slnf` with every test
212
+ project found under the containing Git root. Apply the classic-project safety
213
+ rules below within that selected graph.
214
+ - Selected-entry arrays contain only the projects targeted by that entry:
215
+ solution members, filter members, or one explicit project. Transitive build
216
+ dependencies are not additional test-run targets. For a generated or
217
+ nonstandard entry graph that these formats cannot represent, stop and use
218
+ the repository's evaluated target inventory rather than scanning siblings.
219
+ - If `TEST_PROJECTS:0` and `EXISTING_COBERTURA_COUNT` > 0 (Step 2b) → continue with existing Cobertura XML analysis (no `dotnet test` run).
220
+ - If `TEST_PROJECTS:0` and `EXISTING_COBERTURA_COUNT` == 0 → stop: `No test projects found (expected projects with 'Test' or 'Spec' in the name), and no existing Cobertura XML was provided. Add a test project or provide a Cobertura file path.`
221
+ - If `CLASSIC_TEST_PROJECTS` is nonzero and no existing Cobertura XML is found,
222
+ search scripts/CI/docs for a repository-owned coverage command. Use it if it
223
+ emits Cobertura.
224
+ - If classic projects are the only test projects and no repository command
225
+ exists, stop: `Classic non-SDK or packages.config test project detected. The
226
+ automatic SDK-style coverage-provider path would modify this project
227
+ incorrectly. Run the repository's supported coverage workflow and provide its
228
+ Cobertura XML.`
229
+ This is a hard stop: do not create or run a temporary SDK project against the
230
+ classic source, because its coverage would belong to the substitute assembly,
231
+ not the requested test project.
232
+ - In a mixed solution, run automatic collection only for `SDK_TEST_PROJECTS`.
233
+ Never run the solution entry point if it would include classic projects.
234
+ Clearly label the result partial until repository-owned Cobertura data for the
235
+ classic projects is also available.
236
+
237
+ ## Step 2: Create the output directory
238
+
239
+ ```powershell
240
+ $coverageDir = Join-Path $testOutputRoot "TestResults" "coverage-analysis"
241
+ if (Test-Path $coverageDir) { Remove-Item $coverageDir -Recurse -Force }
242
+ New-Item -ItemType Directory -Path $coverageDir -Force | Out-Null
243
+ Write-Host "COVERAGE_DIR:$coverageDir"
244
+ ```
245
+
246
+ This step only manages the `TestResults/coverage-analysis/` subdirectory (skill-owned outputs). It must never delete user-supplied Cobertura files — those live one level up at `TestResults/coverage.cobertura.xml` (or wherever the user pointed). If the user provided a path that *is* `TestResults/coverage-analysis/...`, copy the file aside before this step recreates the directory.
247
+
248
+ ## Step 2b: Discover or accept existing Cobertura XML (required for the existing-data path)
249
+
250
+ If the user supplied a Cobertura XML path explicitly, use it. Otherwise probe well-known locations and any path the user mentioned:
251
+
252
+ ```powershell
253
+ # 1. Honor a user-supplied path first (highest priority)
254
+ $coberturaFiles = @()
255
+ if ($userSuppliedCoberturaPath) {
256
+ $coberturaFiles = @(Get-Item -LiteralPath $userSuppliedCoberturaPath -ErrorAction Stop)
257
+ }
258
+
259
+ # 2. Otherwise scan TestResults/ at the repo/test root for any *.cobertura.xml
260
+ if ($coberturaFiles.Count -eq 0) {
261
+ $searchPaths = @(
262
+ (Join-Path $testOutputRoot "TestResults"),
263
+ (Join-Path $root "TestResults")
264
+ ) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
265
+ foreach ($sp in $searchPaths) {
266
+ $found = @(Get-ChildItem -LiteralPath $sp -Filter "*.cobertura.xml" -Recurse -ErrorAction Stop |
267
+ Where-Object { $_.FullName -notmatch '[/\\]coverage-analysis[/\\]raw[/\\]' })
268
+ if ($found.Count -gt 0) { $coberturaFiles = $found; break }
269
+ }
270
+ }
271
+
272
+ Write-Host "EXISTING_COBERTURA_COUNT:$($coberturaFiles.Count)"
273
+ $coberturaFiles | ForEach-Object { Write-Host "EXISTING_COBERTURA:$($_.FullName)" }
274
+ ```
275
+
276
+ - If `EXISTING_COBERTURA_COUNT` > 0 → skip fresh collection and analyze these paths.
277
+ - If `EXISTING_COBERTURA_COUNT` == 0 and all test projects are SDK-style →
278
+ invoke `run-tests` to select the repository-compatible platform/provider
279
+ command and collect Cobertura.
280
+ - If `EXISTING_COBERTURA_COUNT` == 0 and only classic/packages.config projects
281
+ exist → use a repository-owned coverage command that emits Cobertura;
282
+ otherwise stop with the message above.
283
+ - If `EXISTING_COBERTURA_COUNT` == 0 and both classic and SDK-style projects
284
+ exist → ask `run-tests` to collect only for `SDK_TEST_PROJECTS` and mark the
285
+ result partial until classic-project Cobertura is available.
286
+
287
+ ## Step 2c: Recommend ignoring `TestResults/`
288
+
289
+ ```powershell
290
+ $pattern = "**/TestResults/"
291
+ $gitRoot = (git -C $testOutputRoot rev-parse --show-toplevel 2>$null)
292
+ if ($gitRoot) { $gitRoot = [System.IO.Path]::GetFullPath($gitRoot) }
293
+ if ($gitRoot) {
294
+ $gitignorePath = Join-Path $gitRoot ".gitignore"
295
+ $alreadyIgnored = $false
296
+ if (Test-Path $gitignorePath) {
297
+ $alreadyIgnored = (Select-String -Path $gitignorePath -Pattern '^\s*(\*\*/)?TestResults/?\s*$' -Quiet)
298
+ }
299
+ if ($alreadyIgnored) {
300
+ Write-Host "GITIGNORE_RECOMMENDATION:already-present"
301
+ } else {
302
+ Write-Host "GITIGNORE_RECOMMENDATION:$pattern"
303
+ }
304
+ } else {
305
+ Write-Host "GITIGNORE_RECOMMENDATION:$pattern"
306
+ }
307
+ ```
@@ -0,0 +1,80 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Compute CRAP = complexity^2 * (1 - line coverage)^3 + complexity.
4
+ .DESCRIPTION
5
+ Missing complexity or executable lines yield unknown, never complexity=1/coverage=0.
6
+ Legacy output retains OVERALL_*, TOTAL_METHODS, FLAGGED_METHODS and HOTSPOTS markers
7
+ only when all requested measurements are representable. -OutputFormat json-v1
8
+ emits one provenance-bearing JSON document. Diagnostics go to stderr.
9
+ Thresholds and hotspot ordering use raw scores; only displayed scores are rounded.
10
+ Exit codes: 0 complete, 2 invalid input, 3 incomplete/unrepresentable evidence.
11
+ #>
12
+ param(
13
+ [Parameter(Mandatory)][string[]]$CoberturaPath,
14
+ [ValidateRange(0,[int]::MaxValue)][int]$CrapThreshold = 30,
15
+ [ValidateRange(1,[int]::MaxValue)][int]$TopN = 10,
16
+ [ValidateSet('legacy', 'json-v1')][string]$OutputFormat = 'legacy'
17
+ )
18
+
19
+ try {
20
+ . "$PSScriptRoot\CoverageData.ps1"
21
+ } catch {
22
+ $diagnostic = @{ code = 'DEPENDENCY_UNAVAILABLE'; severity = 'error'; message = $_.Exception.Message; path = "$PSScriptRoot\CoverageData.ps1" }
23
+ [Console]::Error.WriteLine("$($diagnostic.code): $($diagnostic.message)")
24
+ if ($OutputFormat -eq 'json-v1') {
25
+ @{
26
+ schema_version = '1'; tool = 'Compute-CrapScores'; tool_version = '1.0'; status = 'error'
27
+ scope = @{}; inputs = @(); diagnostics = @($diagnostic); data = @{}
28
+ } | ConvertTo-Json -Depth 5
29
+ }
30
+ exit 2
31
+ }
32
+ $scope = @{ identity = 'reported-module-file-class-signature'; source_revision = $null; top = $TopN; crap_threshold = $CrapThreshold }
33
+ try {
34
+ $coverage = Get-CoverageData $CoberturaPath
35
+ } catch {
36
+ $diagnostics = @([pscustomobject]@{ code = 'INVALID_INPUT'; severity = 'error'; message = $_.Exception.Message })
37
+ Write-CoverageDiagnostics $diagnostics
38
+ if ($OutputFormat -eq 'json-v1') { Write-CoverageJson 'Compute-CrapScores' 'error' $scope @() $diagnostics @{} }
39
+ exit 2
40
+ }
41
+ foreach ($method in $coverage.Methods) {
42
+ $score = if ($null -ne $method.Complexity -and $method.LineCoverageStatus -eq 'exact') {
43
+ [math]::Pow($method.Complexity, 2) * [math]::Pow(1 - $method.CoveredLines / [double]$method.TotalLines, 3) + $method.Complexity
44
+ } else { $null }
45
+ $method | Add-Member -NotePropertyName CrapScore -NotePropertyValue $score
46
+ }
47
+ $ranked = @($coverage.Methods | Where-Object { $null -ne $_.CrapScore } |
48
+ Sort-Object -Property @{Expression='CrapScore';Descending=$true}, Class, Method, File, Signature)
49
+ $hotspots = @($ranked | Select-Object -First $TopN)
50
+ $unknown = @($coverage.Methods | Where-Object { $null -eq $_.CrapScore }).Count
51
+ $coverage.Summary.flagged_methods = @($ranked | Where-Object { $_.CrapScore -gt $CrapThreshold }).Count
52
+ $coverage.Summary.unknown_crap_methods = $unknown
53
+ foreach ($method in $ranked) {
54
+ $method.CrapScore = [math]::Round($method.CrapScore, 2)
55
+ }
56
+ foreach ($metric in @('line', 'branch')) {
57
+ if ($coverage.Summary["overall_${metric}_coverage_status"] -in @('unknown', 'bounded')) {
58
+ Add-CoverageDiagnostic $coverage.Diagnostics 'AGGREGATE_UNKNOWN' "Overall $metric coverage cannot be derived from overlapping report counters." ''
59
+ }
60
+ }
61
+ $partial = -not $coverage.Methods.Count -or $unknown -gt 0 -or
62
+ $coverage.Summary.overall_line_coverage_status -in @('unknown', 'bounded') -or
63
+ $coverage.Summary.overall_branch_coverage_status -in @('unknown', 'bounded')
64
+ Write-CoverageDiagnostics $coverage.Diagnostics
65
+ if ($OutputFormat -eq 'json-v1') {
66
+ Write-CoverageJson 'Compute-CrapScores' $(if ($partial) { 'partial' } else { 'complete' }) $scope $coverage.Inputs $coverage.Diagnostics @{
67
+ methods = $coverage.Methods; hotspots = $hotspots; summary = $coverage.Summary
68
+ }
69
+ } elseif (-not $partial) {
70
+ $line = if ($coverage.Summary.overall_line_coverage_status -eq 'not-applicable') { 0 } else { $coverage.Summary.overall_line_coverage }
71
+ $branch = if ($coverage.Summary.overall_branch_coverage_status -eq 'not-applicable') { 0 } else { $coverage.Summary.overall_branch_coverage }
72
+ Write-Output "OVERALL_LINE_COVERAGE:$line"
73
+ Write-Output "OVERALL_BRANCH_COVERAGE:$branch"
74
+ Write-Output "TOTAL_METHODS:$($coverage.Methods.Count)"
75
+ Write-Output "FLAGGED_METHODS:$($coverage.Summary.flagged_methods)"
76
+ $legacy = @($hotspots | Select-Object Class, Method, Signature, File, TotalLines, CoveredLines, LineCoverage, Complexity, CrapScore)
77
+ Write-Output "HOTSPOTS:$(ConvertTo-Json -InputObject $legacy -Compress)"
78
+ }
79
+ if ($partial) { exit 3 }
80
+ exit 0