@caddis/cli 0.4.24 → 0.4.25
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.
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/skills/_registry.md +1 -0
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/_registry.md +0 -1
- package/bundles/codex/.codex/skills/devops/adopt-app/SKILL.md +174 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/_template/profile.json +55 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/batch-job/profile.json +68 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/dotnet-service/profile.json +67 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/java-service/profile.json +62 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/node-service/profile.json +66 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/php-db/profile.json +75 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/python-web/profile.json +73 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/static-site/profile.json +87 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/references/config.example.json +18 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/references/profile-contract.md +207 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/references/standard.md +304 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/scripts/AdoptCommon.ps1 +812 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/scripts/Get-AppInventory.ps1 +236 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/scripts/New-AdoptionPlan.ps1 +311 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/scripts/Resolve-AppProfile.ps1 +246 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/scripts/Test-AppConformance.ps1 +343 -0
- package/bundles/manifest.json +3 -3
- package/package.json +1 -1
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/SKILL.md +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/_template/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/batch-job/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/dotnet-service/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/java-service/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/node-service/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/php-db/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/python-web/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/static-site/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/references/config.example.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/references/profile-contract.md +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/references/standard.md +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/AdoptCommon.ps1 +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/Get-AppInventory.ps1 +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/New-AdoptionPlan.ps1 +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/Resolve-AppProfile.ps1 +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/Test-AppConformance.ps1 +0 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Phase 2. Decide what KIND of app this is, per component, and write profile.json.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
An app is rarely one thing. A folder can honestly be a web API and a batch CLI at the
|
|
7
|
+
same time, and a real repository often holds several components: a service, a static
|
|
8
|
+
front end, a vendored third-party product.
|
|
9
|
+
|
|
10
|
+
So this script reports EVERY profile that matches each component folder, not just the
|
|
11
|
+
best one. Silently picking the winner hides a decision a person has to make - and the
|
|
12
|
+
dogfood found a repository carrying an abandoned Java scaffold that a best-match
|
|
13
|
+
detector would have called a Java service.
|
|
14
|
+
|
|
15
|
+
Nothing here is a guess you have to accept. The output records the score, the runners
|
|
16
|
+
up and the evidence, and the script exits 2 when no profile matches at all, because
|
|
17
|
+
"no stack" is never the right answer for an app that is running.
|
|
18
|
+
|
|
19
|
+
.PARAMETER Name
|
|
20
|
+
The name used for the inventory in phase 1.
|
|
21
|
+
|
|
22
|
+
.PARAMETER ForceProfile
|
|
23
|
+
Force one profile for the root component instead of detecting it. Use it when you
|
|
24
|
+
know better than the rules, and say why in the plan. NOT named -Profile: $Profile is a PowerShell automatic
|
|
25
|
+
variable (the path to the shell profile), and shadowing it inside a script is a trap.
|
|
26
|
+
|
|
27
|
+
.EXAMPLE
|
|
28
|
+
./Resolve-AppProfile.ps1 -Name legacy-app
|
|
29
|
+
.EXAMPLE
|
|
30
|
+
./Resolve-AppProfile.ps1 -Name batch-engine -ForceProfile batch-job
|
|
31
|
+
#>
|
|
32
|
+
|
|
33
|
+
[CmdletBinding()]
|
|
34
|
+
param(
|
|
35
|
+
[Parameter(Mandatory)][string]$Name,
|
|
36
|
+
[string]$OutDir,
|
|
37
|
+
[string]$ProfilesDir,
|
|
38
|
+
[string]$ForceProfile,
|
|
39
|
+
[int]$MaxComponents = 12
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
$ErrorActionPreference = 'Stop'
|
|
43
|
+
. (Join-Path $PSScriptRoot 'AdoptCommon.ps1')
|
|
44
|
+
|
|
45
|
+
$dir = Resolve-AdoptOutDir -Name $Name -OutDir $OutDir
|
|
46
|
+
$inventory = Read-AdoptArtifact -Dir $dir -FileName 'inventory.json'
|
|
47
|
+
|
|
48
|
+
Write-AdoptHeading "Stack profile: $Name"
|
|
49
|
+
|
|
50
|
+
# ---- the profiles themselves must meet their own contract ----------------------------
|
|
51
|
+
$profiles = @(Get-AdoptProfiles -ProfilesDir $ProfilesDir)
|
|
52
|
+
$broken = @()
|
|
53
|
+
foreach ($p in $profiles) {
|
|
54
|
+
# No @() here. Test-AdoptProfileContract returns `, $problems`, so the array arrives as
|
|
55
|
+
# a single object; wrapping it again makes a one-element array of an array, whose Count
|
|
56
|
+
# is 1 whether the profile was clean or not - every profile then reads as broken.
|
|
57
|
+
$probs = Test-AdoptProfileContract -Spec $p
|
|
58
|
+
if ($probs.Count -gt 0) { $broken += "$($p.name): $($probs -join '; ')" }
|
|
59
|
+
}
|
|
60
|
+
if ($broken.Count -gt 0) {
|
|
61
|
+
Write-Error "These profiles do not meet the contract, so no detection can be trusted:`n $($broken -join "`n ")"
|
|
62
|
+
exit 1
|
|
63
|
+
}
|
|
64
|
+
Write-Host " $($profiles.Count) profiles loaded, all meeting the contract"
|
|
65
|
+
|
|
66
|
+
# ---- which folders are components ----------------------------------------------------
|
|
67
|
+
# A component is the root, plus any folder that carries a dependency manifest. Manifests
|
|
68
|
+
# inside an excluded tree do not count: a vendored product's own composer.json is part of
|
|
69
|
+
# that product, not a component of this app.
|
|
70
|
+
$manifestPaths = @($inventory.manifests | ForEach-Object { $_.path })
|
|
71
|
+
$excludedRx = '(?i)(^|[\\/])(node_modules|vendor|\.venv|venv|site-packages|bower_components|dist|build|target|out|\.git)([\\/]|$)'
|
|
72
|
+
|
|
73
|
+
$componentDirs = @('.')
|
|
74
|
+
foreach ($m in $manifestPaths) {
|
|
75
|
+
if ($m -match $excludedRx) { continue }
|
|
76
|
+
$d = Split-Path $m -Parent
|
|
77
|
+
if ([string]::IsNullOrEmpty($d)) { continue }
|
|
78
|
+
$componentDirs += ($d -replace '\\', '/')
|
|
79
|
+
}
|
|
80
|
+
$componentDirs = @($componentDirs | Sort-Object -Unique | Sort-Object { $_.Length })
|
|
81
|
+
|
|
82
|
+
if ($componentDirs.Count -gt $MaxComponents) {
|
|
83
|
+
Write-Host " $($componentDirs.Count) candidate component folders; keeping the $MaxComponents shallowest" -ForegroundColor Yellow
|
|
84
|
+
$componentDirs = @($componentDirs | Select-Object -First $MaxComponents)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# ---- the file list each component is matched against ---------------------------------
|
|
88
|
+
# Detection needs the paths RELATIVE TO THE COMPONENT, so a rule saying `package.json`
|
|
89
|
+
# matches a manifest in apps/api when apps/api is the component being tested.
|
|
90
|
+
$allPaths = @()
|
|
91
|
+
$allPaths += @($inventory.paths)
|
|
92
|
+
$allPaths += @($inventory.manifests | ForEach-Object { $_.path })
|
|
93
|
+
$allPaths += @($inventory.entryPoints | ForEach-Object { $_.path })
|
|
94
|
+
$allPaths += @($inventory.configFiles)
|
|
95
|
+
$allPaths += @($inventory.ci)
|
|
96
|
+
$allPaths += @($inventory.docs)
|
|
97
|
+
$allPaths += @($inventory.tests.markers)
|
|
98
|
+
$allPaths = @($allPaths | Where-Object { $_ } | Sort-Object -Unique)
|
|
99
|
+
|
|
100
|
+
# Any file a detect rule wants to read, fetched once from the source.
|
|
101
|
+
$wantedFiles = @()
|
|
102
|
+
foreach ($p in $profiles) {
|
|
103
|
+
$names = @($p.detect.PSObject.Properties.Name)
|
|
104
|
+
foreach ($key in @('fileContains', 'noneContain')) {
|
|
105
|
+
if ($names -notcontains $key) { continue }
|
|
106
|
+
foreach ($c in @($p.detect.$key)) { foreach ($g in @($c.file)) { $wantedFiles += $g } }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
$wantedFiles = @($wantedFiles | Sort-Object -Unique)
|
|
110
|
+
|
|
111
|
+
$concreteWanted = @()
|
|
112
|
+
foreach ($g in $wantedFiles) {
|
|
113
|
+
if ($g -match '[*?]') {
|
|
114
|
+
$rx = '(?i)^' + [regex]::Escape($g).Replace('\*\*', '.*').Replace('\*', '[^\\/]*').Replace('/', '[\\/]') + '$'
|
|
115
|
+
foreach ($cd in $componentDirs) {
|
|
116
|
+
$prefix = if ($cd -eq '.') { '' } else { "$cd/" }
|
|
117
|
+
foreach ($ap in $allPaths) {
|
|
118
|
+
$relToComp = $ap -replace '\\', '/'
|
|
119
|
+
if ($prefix -and -not $relToComp.StartsWith($prefix)) { continue }
|
|
120
|
+
$tail = if ($prefix) { $relToComp.Substring($prefix.Length) } else { $relToComp }
|
|
121
|
+
if ($tail -match $rx) { $concreteWanted += $ap }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
foreach ($cd in $componentDirs) {
|
|
127
|
+
$prefix = if ($cd -eq '.') { '' } else { "$cd/" }
|
|
128
|
+
$concreteWanted += "$prefix$g"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
$concreteWanted = @($concreteWanted | Sort-Object -Unique)
|
|
133
|
+
|
|
134
|
+
$fileText = @{}
|
|
135
|
+
if ($concreteWanted.Count -gt 0) {
|
|
136
|
+
$fetched = Get-AdoptFileText -Path $inventory.source.path -ComputerName $inventory.source.computerName -RelativePaths $concreteWanted
|
|
137
|
+
foreach ($k in @($fetched.Keys)) { $fileText[($k -replace '\\', '/')] = $fetched[$k] }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# ---- match ---------------------------------------------------------------------------
|
|
141
|
+
$components = @()
|
|
142
|
+
foreach ($cd in $componentDirs) {
|
|
143
|
+
$prefix = if ($cd -eq '.') { '' } else { "$cd/" }
|
|
144
|
+
|
|
145
|
+
$relPaths = @()
|
|
146
|
+
foreach ($ap in $allPaths) {
|
|
147
|
+
$n = $ap -replace '\\', '/'
|
|
148
|
+
if ($prefix) {
|
|
149
|
+
if (-not $n.StartsWith($prefix)) { continue }
|
|
150
|
+
$relPaths += $n.Substring($prefix.Length)
|
|
151
|
+
}
|
|
152
|
+
else { $relPaths += $n }
|
|
153
|
+
}
|
|
154
|
+
if ($relPaths.Count -eq 0) { continue }
|
|
155
|
+
|
|
156
|
+
$localText = @{}
|
|
157
|
+
foreach ($k in @($fileText.Keys)) {
|
|
158
|
+
if ($prefix) {
|
|
159
|
+
if (-not $k.StartsWith($prefix)) { continue }
|
|
160
|
+
$localText[$k.Substring($prefix.Length)] = $fileText[$k]
|
|
161
|
+
}
|
|
162
|
+
else { $localText[$k] = $fileText[$k] }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# NOT $matches: that is PowerShell's automatic $Matches, and the names collide
|
|
166
|
+
# case-insensitively.
|
|
167
|
+
$hits = @()
|
|
168
|
+
foreach ($p in $profiles) {
|
|
169
|
+
$score = Test-AdoptDetect -Detect $p.detect -RelativePaths $relPaths -FileText $localText
|
|
170
|
+
if ($score -gt 0) { $hits += [pscustomobject]@{ name = $p.name; status = $p.status; score = $score } }
|
|
171
|
+
}
|
|
172
|
+
$hits = @($hits | Sort-Object -Property @{ Expression = 'score'; Descending = $true }, @{ Expression = 'name'; Descending = $false })
|
|
173
|
+
if ($hits.Count -eq 0) { continue }
|
|
174
|
+
|
|
175
|
+
$components += [ordered]@{
|
|
176
|
+
path = $cd
|
|
177
|
+
profile = $hits[0].name
|
|
178
|
+
status = $hits[0].status
|
|
179
|
+
score = $hits[0].score
|
|
180
|
+
alsoMatches = @($hits | Select-Object -Skip 1 | ForEach-Object { $_.name })
|
|
181
|
+
evidence = @($relPaths | Where-Object { $_ -notmatch '[\\/]' } | Select-Object -First 8)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# A folder inside an already-matched folder of the SAME profile is part of it, not a
|
|
186
|
+
# second component: apps/api inside a workspace is one service, not two.
|
|
187
|
+
$kept = @()
|
|
188
|
+
foreach ($c in $components) {
|
|
189
|
+
$parent = $kept | Where-Object { $_.profile -eq $c.profile -and ($_.path -eq '.' -or $c.path.StartsWith("$($_.path)/")) }
|
|
190
|
+
if (-not $parent) { $kept += $c }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if ($ForceProfile) {
|
|
194
|
+
$forced = @($profiles | Where-Object { $_.name -eq $ForceProfile })
|
|
195
|
+
if ($forced.Count -eq 0) {
|
|
196
|
+
Write-Error "No profile '$ForceProfile'. Known: $(($profiles | ForEach-Object { $_.name }) -join ', ')"
|
|
197
|
+
exit 1
|
|
198
|
+
}
|
|
199
|
+
$root = @($kept | Where-Object { $_.path -eq '.' })
|
|
200
|
+
if ($root.Count -gt 0) { $kept = @($kept | Where-Object { $_.path -ne '.' }) }
|
|
201
|
+
$kept = @([ordered]@{ path = '.'; profile = $ForceProfile; status = $forced[0].status; score = 0; alsoMatches = @(); evidence = @('forced with -Profile') }) + $kept
|
|
202
|
+
Write-Host " root component FORCED to '$ForceProfile' - record why in the plan" -ForegroundColor Yellow
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
$result = [ordered]@{
|
|
206
|
+
name = $Name
|
|
207
|
+
resolvedAt = (Get-Date).ToUniversalTime().ToString('o')
|
|
208
|
+
profilesDir = (Get-AdoptProfileDir -ProfilesDir $ProfilesDir)
|
|
209
|
+
components = @($kept)
|
|
210
|
+
}
|
|
211
|
+
$out = Write-AdoptArtifact -Dir $dir -FileName 'profile.json' -Object $result
|
|
212
|
+
|
|
213
|
+
Write-AdoptHeading 'Components'
|
|
214
|
+
if ($kept.Count -eq 0) {
|
|
215
|
+
Write-Host " NO PROFILE MATCHED ANY FOLDER." -ForegroundColor Red
|
|
216
|
+
Write-Host " An app that runs has a stack, so this means the rules do not describe it yet." -ForegroundColor Red
|
|
217
|
+
Write-Host " Copy profiles/_template into a new folder, fill it in, and re-run. Set status" -ForegroundColor Red
|
|
218
|
+
Write-Host " to 'draft' - only a stack something has actually automated may be 'proven'." -ForegroundColor Red
|
|
219
|
+
Write-Host ""
|
|
220
|
+
Write-Host " wrote $out"
|
|
221
|
+
exit 2
|
|
222
|
+
}
|
|
223
|
+
foreach ($c in $kept) {
|
|
224
|
+
$also = ''
|
|
225
|
+
if (@($c.alsoMatches).Count -gt 0) { $also = " ALSO MATCHES: $(@($c.alsoMatches) -join ', ')" }
|
|
226
|
+
Write-Host (" {0,-28} {1,-16} [{2}] score {3}{4}" -f $c.path, $c.profile, $c.status, $c.score, $also)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
$ambiguous = @($kept | Where-Object { @($_.alsoMatches).Count -gt 0 })
|
|
230
|
+
if ($ambiguous.Count -gt 0) {
|
|
231
|
+
Write-Host ""
|
|
232
|
+
Write-Host " A component that matches more than one profile is a DECISION, not a defect." -ForegroundColor Yellow
|
|
233
|
+
Write-Host " Confirm each with the owner before the plan is written. Leftover scaffolding from" -ForegroundColor Yellow
|
|
234
|
+
Write-Host " an abandoned rewrite matches its own stack perfectly and is not part of the app." -ForegroundColor Yellow
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
$draft = @($kept | Where-Object { $_.status -ne 'proven' })
|
|
238
|
+
if ($draft.Count -gt 0) {
|
|
239
|
+
Write-Host ""
|
|
240
|
+
Write-Host " $($draft.Count) component(s) are on a DRAFT profile: described, not automated." -ForegroundColor DarkYellow
|
|
241
|
+
Write-Host " Follow it by hand and record what you did, so it can become proven later." -ForegroundColor DarkYellow
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
Write-Host ""
|
|
245
|
+
Write-Host " wrote $out"
|
|
246
|
+
exit 0
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Phase 3. Measure the app against the adoption standard. Writes conformance.json.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
THREE VERDICTS, NOT TWO. Most checklists have PASS and FAIL, which forces a tool to
|
|
7
|
+
answer questions it cannot answer - and a tool that guesses at "who owns this?" or
|
|
8
|
+
"which branch is the truth?" produces a confident wrong answer that nobody rechecks.
|
|
9
|
+
|
|
10
|
+
PASS observed true, from the inventory or the profile.
|
|
11
|
+
GAP observed false. The evidence is named.
|
|
12
|
+
ASK NOT OBSERVABLE FROM FILES. A person must answer it.
|
|
13
|
+
|
|
14
|
+
An unanswered ASK on a blocking item stops the adoption exactly as a GAP does. That is
|
|
15
|
+
the point: the two apps this was built for each had questions only their owner could
|
|
16
|
+
settle, and a checklist that lists them as notes at the bottom lets them be skipped.
|
|
17
|
+
|
|
18
|
+
Record an answer with -Answer, and it is kept in answers.json for the next run:
|
|
19
|
+
|
|
20
|
+
./Test-AppConformance.ps1 -Name app -Answer owner="Team Payments, [name]"
|
|
21
|
+
|
|
22
|
+
EXIT CODES
|
|
23
|
+
0 every blocking item is PASS
|
|
24
|
+
1 a blocking item is a GAP
|
|
25
|
+
2 a blocking item is an unanswered ASK
|
|
26
|
+
|
|
27
|
+
.PARAMETER Answer
|
|
28
|
+
One or more `item-id=answer` pairs. Any non-empty answer satisfies an ASK: the answer
|
|
29
|
+
is the record, this script does not judge it.
|
|
30
|
+
#>
|
|
31
|
+
|
|
32
|
+
[CmdletBinding()]
|
|
33
|
+
param(
|
|
34
|
+
[Parameter(Mandatory)][string]$Name,
|
|
35
|
+
[string]$OutDir,
|
|
36
|
+
[string[]]$Answer = @(),
|
|
37
|
+
[string[]]$Waive = @(),
|
|
38
|
+
[int]$UntrackedThresholdMB = 0
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
$ErrorActionPreference = 'Stop'
|
|
42
|
+
. (Join-Path $PSScriptRoot 'AdoptCommon.ps1')
|
|
43
|
+
|
|
44
|
+
$dir = Resolve-AdoptOutDir -Name $Name -OutDir $OutDir
|
|
45
|
+
$inv = Read-AdoptArtifact -Dir $dir -FileName 'inventory.json'
|
|
46
|
+
$prof = Read-AdoptArtifact -Dir $dir -FileName 'profile.json'
|
|
47
|
+
$cfg = Get-AdoptConfig
|
|
48
|
+
if ($UntrackedThresholdMB -le 0) { $UntrackedThresholdMB = [int]$cfg.untrackedThresholdMB }
|
|
49
|
+
|
|
50
|
+
# ---- answers, kept between runs ------------------------------------------------------
|
|
51
|
+
$answersPath = Join-Path $dir 'answers.json'
|
|
52
|
+
$answers = @{}
|
|
53
|
+
if (Test-Path -LiteralPath $answersPath) {
|
|
54
|
+
$j = Get-Content -LiteralPath $answersPath -Raw | ConvertFrom-Json
|
|
55
|
+
foreach ($p in $j.PSObject.Properties) { $answers[$p.Name] = $p.Value }
|
|
56
|
+
}
|
|
57
|
+
foreach ($a in $Answer) {
|
|
58
|
+
$i = $a.IndexOf('=')
|
|
59
|
+
if ($i -le 0) { Write-Error "-Answer needs the form item-id=answer, got '$a'"; exit 1 }
|
|
60
|
+
$answers[$a.Substring(0, $i).Trim()] = $a.Substring($i + 1).Trim()
|
|
61
|
+
}
|
|
62
|
+
$answers | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $answersPath -Encoding UTF8
|
|
63
|
+
|
|
64
|
+
# ---- facts the checks read -----------------------------------------------------------
|
|
65
|
+
$isRepo = [bool]$inv.git.isRepo
|
|
66
|
+
$remotes = @()
|
|
67
|
+
if ($isRepo -and $inv.git.remotes) { $remotes = @($inv.git.remotes | ForEach-Object { $_.url }) }
|
|
68
|
+
# git could not be consulted, so nothing derived from it may be reported as a fact.
|
|
69
|
+
$gitUsable = (-not $isRepo) -or (Get-AdoptProp $inv.git 'gitAvailable' $false)
|
|
70
|
+
$folders = @($inv.size.folders)
|
|
71
|
+
$componentProfiles = @($prof.components | ForEach-Object { $_.profile })
|
|
72
|
+
$isBatchOnly = ($componentProfiles.Count -gt 0) -and (@($componentProfiles | Where-Object { $_ -notmatch 'batch' }).Count -eq 0)
|
|
73
|
+
|
|
74
|
+
$untrackedBig = @($folders | Where-Object { $_.git -eq 'untracked' -and $_.path -ne '.git' -and $_.mb -ge $UntrackedThresholdMB })
|
|
75
|
+
$trackedJunk = @()
|
|
76
|
+
foreach ($e in @($inv.excludableSummary)) {
|
|
77
|
+
$f = @($folders | Where-Object { $_.path -eq $e.folder })
|
|
78
|
+
if ($f.Count -gt 0 -and $f[0].git -eq 'tracked' -and $e.category -ne 'log') { $trackedJunk += $e }
|
|
79
|
+
}
|
|
80
|
+
$ignoredHeavy = @($folders | Where-Object { $_.git -eq 'ignored' -and $_.mb -ge 100 })
|
|
81
|
+
|
|
82
|
+
# A folder the operator told the scan to skip was NOT examined. It is not empty and it is
|
|
83
|
+
# not clean - it is unknown. On the first real run, `-SkipFolder tools` hid a 16 GB runtime
|
|
84
|
+
# tree and "anything excluded can be got back" reported PASS, which is precisely the
|
|
85
|
+
# failure this skill exists to prevent. Coverage-dependent checks may not pass while any
|
|
86
|
+
# folder is unscanned.
|
|
87
|
+
$skipped = @($folders | Where-Object { $_.skipped })
|
|
88
|
+
$coverageNote = ''
|
|
89
|
+
if ($skipped.Count -gt 0) { $coverageNote = "NOT SCANNED: $((@($skipped | ForEach-Object { $_.path })) -join ', ') - unknown, not clean. " }
|
|
90
|
+
if (-not $gitUsable) { $coverageNote += 'git could not be run, so tracked-vs-ignored is unknown for every folder. ' }
|
|
91
|
+
function Limit-ByCoverage {
|
|
92
|
+
param([string]$Verdict)
|
|
93
|
+
if ($Verdict -eq 'PASS' -and ($script:skipped.Count -gt 0 -or -not $script:gitUsable)) { return 'ASK' }
|
|
94
|
+
return $Verdict
|
|
95
|
+
}
|
|
96
|
+
$lockFiles = @($inv.manifests | Where-Object { $_.name -match '(?i)^(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|poetry\.lock|Pipfile\.lock|composer\.lock|Gemfile\.lock|Cargo\.lock|go\.sum)$' })
|
|
97
|
+
$envTemplates = @($inv.configFiles | Where-Object { $_ -match '(?i)[._-](example|sample|template|dist)$' -or $_ -match '(?i)\.env\.(example|sample|template)' })
|
|
98
|
+
$hasReadme = @($inv.docs | Where-Object { $_ -match '(?i)^readme' }).Count -gt 0
|
|
99
|
+
$hasAgentRules = @($inv.docs | Where-Object { $_ -match '(?i)^(agents|claude)\.md$' }).Count -gt 0
|
|
100
|
+
$runReg = $inv.runRegistrations
|
|
101
|
+
$runCount = 0
|
|
102
|
+
if ($runReg) { $runCount = @($runReg.services).Count + @($runReg.scheduledTasks).Count + @($runReg.webSites).Count }
|
|
103
|
+
$junctionsInTree = @($inv.reparsePoints | Where-Object { $_.path -notmatch '(?i)(^|[\\/])(node_modules|\.venv|venv|vendor)([\\/]|$)' })
|
|
104
|
+
|
|
105
|
+
$expectedRemote = $null
|
|
106
|
+
if (@($cfg.Keys) -contains 'expectedRemotePattern' -and "$($cfg.expectedRemotePattern)" -ne '') {
|
|
107
|
+
$expectedRemote = "$($cfg.expectedRemotePattern)"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ---- the standard --------------------------------------------------------------------
|
|
113
|
+
# Each item: id, title, blocking, and a verdict with evidence. `ask` items carry the
|
|
114
|
+
# question that has to reach a person.
|
|
115
|
+
$items = @()
|
|
116
|
+
|
|
117
|
+
function Add-Item {
|
|
118
|
+
param([string]$Id, [string]$Title, [bool]$Blocking, [string]$Verdict, [string]$Evidence, [string]$Question = '', [string]$Fix = '')
|
|
119
|
+
# Every key is initialised, including the ones only some items use. Under StrictMode a
|
|
120
|
+
# missing key is an error, not a $null, so an item without an answer would throw the
|
|
121
|
+
# moment the report tried to read one.
|
|
122
|
+
$script:items += [ordered]@{
|
|
123
|
+
id = $Id; title = $Title; blocking = $Blocking; verdict = $Verdict
|
|
124
|
+
evidence = $Evidence; question = $Question; fix = $Fix
|
|
125
|
+
answer = ''; waived = $false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
Add-Item 'git-repo' 'The source is in a git repository' $true `
|
|
130
|
+
$(if ($isRepo) { 'PASS' } else { 'GAP' }) `
|
|
131
|
+
$(if ($isRepo) { "$($inv.git.commits) commits, $($inv.git.localBranches) local branches" } else { 'no .git in the source folder' }) `
|
|
132
|
+
'' 'Adoption starts by importing the folder as one commit. Decide first what must NOT be in that commit.'
|
|
133
|
+
|
|
134
|
+
if (-not $gitUsable) {
|
|
135
|
+
Add-Item 'remote-on-team-host' 'The repository lives on the team''s host' $true 'ASK' `
|
|
136
|
+
'there is a repository, but git could not be run where the scan happened, so its remotes were never read' `
|
|
137
|
+
'Run `git remote -v` in that folder yourself: which host does this repository push to?'
|
|
138
|
+
}
|
|
139
|
+
elseif (-not $isRepo) {
|
|
140
|
+
Add-Item 'remote-on-team-host' 'The repository lives on the team''s host' $true 'GAP' 'there is no repository yet' '' 'Create it after the content policy is settled, not before.'
|
|
141
|
+
}
|
|
142
|
+
elseif ($remotes.Count -eq 0) {
|
|
143
|
+
Add-Item 'remote-on-team-host' 'The repository lives on the team''s host' $true 'GAP' `
|
|
144
|
+
'no remote is configured - this folder is the ONLY copy of the history' '' `
|
|
145
|
+
'Port it to the team host. Until then one disk failure is the whole project.'
|
|
146
|
+
}
|
|
147
|
+
elseif ($expectedRemote -and @($remotes | Where-Object { $_ -match $expectedRemote }).Count -eq 0) {
|
|
148
|
+
Add-Item 'remote-on-team-host' 'The repository lives on the team''s host' $true 'GAP' `
|
|
149
|
+
"remotes are $($remotes -join ', '), none matching '$expectedRemote'" '' `
|
|
150
|
+
'A repository on a personal account is not a team asset. Re-home it, then archive the personal copy.'
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
Add-Item 'remote-on-team-host' 'The repository lives on the team''s host' $true 'PASS' ($remotes -join ', ')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# A branch name is not a verdict. With no remote and many branches, only a person knows.
|
|
157
|
+
#
|
|
158
|
+
# `$isRepo -and ...` used to short-circuit to false when there was NO repository, which made
|
|
159
|
+
# this item report PASS for an app that has no branches at all - the tidiest possible way to
|
|
160
|
+
# say nothing. It asks in every case except a repository with a remote and few branches.
|
|
161
|
+
$branchSettled = $isRepo -and $gitUsable -and $remotes.Count -gt 0 -and [int]$inv.git.localBranches -le 3
|
|
162
|
+
Add-Item 'canonical-branch' 'Which branch is the truth is written down' $true `
|
|
163
|
+
$(if ($branchSettled) { 'PASS' } else { 'ASK' }) `
|
|
164
|
+
$(if (-not $isRepo) { 'no repository yet, so the answer decides what the import commit becomes' }
|
|
165
|
+
elseif (-not $gitUsable) { 'git could not be run where the scan happened' }
|
|
166
|
+
else { "checked out: $($inv.git.branch); $($inv.git.localBranches) local, $($inv.git.remoteBranches) remote-tracking" }) `
|
|
167
|
+
'Which branch is the canonical one, and may the others be deleted or archived?'
|
|
168
|
+
|
|
169
|
+
$sourceOnlyBad = ($trackedJunk.Count -gt 0) -or ($untrackedBig.Count -gt 0)
|
|
170
|
+
$so = @()
|
|
171
|
+
foreach ($t in $trackedJunk) { $so += "COMMITTED $($t.category): $($t.folder) ($($t.mb) MB)" }
|
|
172
|
+
foreach ($u in $untrackedBig) { $so += "NEITHER COMMITTED NOR IGNORED: $($u.path) ($($u.mb) MB)" }
|
|
173
|
+
Add-Item 'source-only' 'The repository holds source, not runtimes, data or dependencies' $true `
|
|
174
|
+
$(if ($sourceOnlyBad) { 'GAP' } else { Limit-ByCoverage 'PASS' }) `
|
|
175
|
+
$(if ($so.Count -gt 0) { "$coverageNote$($so -join '; ')" } else { "${coverageNote}nothing excludable is committed, and nothing large is unclassified" }) `
|
|
176
|
+
'' 'Committed content needs a history rewrite, not a .gitignore line. Unclassified content needs a decision per folder: commit, ignore, or move out.'
|
|
177
|
+
|
|
178
|
+
$secretCount = @($inv.secretsSuspected).Count
|
|
179
|
+
Add-Item 'no-secrets' 'No credential is in the working tree' $true `
|
|
180
|
+
$(if ($secretCount -eq 0) { 'PASS' } else { 'GAP' }) `
|
|
181
|
+
$(if ($secretCount -eq 0) { 'the heuristics found none, which is not proof there are none' } else { "$secretCount suspected: $((@($inv.secretsSuspected | Select-Object -First 4 | ForEach-Object { $_.path })) -join ', ')" }) `
|
|
182
|
+
'' 'Read every flagged file. A credential that was ever committed must be ROTATED, not only deleted - history keeps it.'
|
|
183
|
+
|
|
184
|
+
Add-Item 'config-separation' 'Configuration is outside the code, per environment' $false `
|
|
185
|
+
$(if ($envTemplates.Count -gt 0) { 'PASS' } elseif (@($inv.configFiles).Count -eq 0) { 'ASK' } else { 'GAP' }) `
|
|
186
|
+
$(if ($envTemplates.Count -gt 0) { "templates present: $($envTemplates -join ', ')" } else { "$(@($inv.configFiles).Count) config files, none of them a committed template" }) `
|
|
187
|
+
'Where does each environment get its settings, and what supplies the secret values?'
|
|
188
|
+
|
|
189
|
+
Add-Item 'deps-pinned' 'Dependencies are pinned' $false `
|
|
190
|
+
$(if ($lockFiles.Count -gt 0) { 'PASS' } else { 'GAP' }) `
|
|
191
|
+
$(if ($lockFiles.Count -gt 0) { (@($lockFiles | ForEach-Object { $_.path }) -join ', ') } else { 'no lock file found' }) `
|
|
192
|
+
'' 'Without a lock file the build is not reproducible, and a broken deploy cannot be told from a moved dependency.'
|
|
193
|
+
|
|
194
|
+
Add-Item 'tests' 'There is an automated test suite' $false `
|
|
195
|
+
$(if ([int]$inv.tests.files -gt 0) { 'PASS' } else { 'GAP' }) `
|
|
196
|
+
"$($inv.tests.files) test files; markers: $(if (@($inv.tests.markers).Count) { (@($inv.tests.markers) -join ', ') } else { 'none' })"
|
|
197
|
+
|
|
198
|
+
$ciCount = @($inv.ci).Count
|
|
199
|
+
Add-Item 'ci-build' 'CI builds and tests on every push' $false `
|
|
200
|
+
$(if ($ciCount -gt 0) { 'PASS' } else { 'GAP' }) `
|
|
201
|
+
$(if ($ciCount -gt 0) { (@($inv.ci) -join ', ') } else { 'no workflow or pipeline file found' })
|
|
202
|
+
|
|
203
|
+
Add-Item 'ci-deploy' 'Deployment is reproducible, not manual' $false `
|
|
204
|
+
$(if ($ciCount -gt 0) { 'ASK' } else { 'GAP' }) `
|
|
205
|
+
$(if ($ciCount -gt 0) { 'CI exists; whether it DEPLOYS cannot be read from the file list' } else { 'no CI at all' }) `
|
|
206
|
+
'How is this deployed today, step by step, and who can do it?'
|
|
207
|
+
|
|
208
|
+
Add-Item 'runtime-manifest' 'Anything excluded from the repository can be got back' $false `
|
|
209
|
+
$(if ($ignoredHeavy.Count -eq 0) { Limit-ByCoverage 'PASS' } else { 'ASK' }) `
|
|
210
|
+
$(if ($ignoredHeavy.Count -eq 0) { "${coverageNote}nothing large is excluded among the folders that were scanned" } else { "$coverageNote" + "excluded and large: $((@($ignoredHeavy | ForEach-Object { "$($_.path) ($($_.mb) MB)" })) -join ', ')" }) `
|
|
211
|
+
'For each excluded or unscanned folder: what installs or restores it, at which exact version?'
|
|
212
|
+
|
|
213
|
+
$healthCount = @($inv.healthSignals).Count
|
|
214
|
+
Add-Item 'health' 'There is a health signal that fails loudly' $false `
|
|
215
|
+
$(if ($healthCount -gt 0) { 'PASS' } elseif ($isBatchOnly) { 'ASK' } else { 'GAP' }) `
|
|
216
|
+
$(if ($healthCount -gt 0) { "$healthCount file(s) mention a health route or check" } else { 'no health route found' }) `
|
|
217
|
+
$(if ($isBatchOnly) { 'A batch job has no endpoint. What is its success signal - exit code, an output artefact, a row written?' } else { '' }) `
|
|
218
|
+
'A check that returns 200 whatever happens is not a health check. It must fail when the app is broken.'
|
|
219
|
+
|
|
220
|
+
Add-Item 'run-declared' 'How the app runs is declared and version-controlled' $true `
|
|
221
|
+
$(if ($runCount -gt 0) { 'PASS' } else { 'ASK' }) `
|
|
222
|
+
$(if ($runCount -gt 0) {
|
|
223
|
+
# Name them. "1 service" invited the reader to assume it was the app's own; on
|
|
224
|
+
# the first real run it was a database installed inside the app's folder.
|
|
225
|
+
$names = @()
|
|
226
|
+
foreach ($x in @($runReg.services)) { $names += "service $($x.name)" }
|
|
227
|
+
foreach ($x in @($runReg.scheduledTasks)) { $names += "task $($x.name)" }
|
|
228
|
+
foreach ($x in @($runReg.webSites)) { $names += "site $($x.name)" }
|
|
229
|
+
($names -join ', ') + ' - confirm each one actually runs THIS app'
|
|
230
|
+
} else { 'nothing on the scanned host references this folder' }) `
|
|
231
|
+
'Nothing on this host starts the app. What runs it today - on which machine, as which account, on what trigger?'
|
|
232
|
+
|
|
233
|
+
Add-Item 'rollback' 'There is a way back to the previous version' $false 'ASK' `
|
|
234
|
+
'not observable from files' `
|
|
235
|
+
'If a release is bad, what is the exact command or step that restores the previous one, and has it ever been run?'
|
|
236
|
+
|
|
237
|
+
Add-Item 'owner' 'The app has a named owner' $true `
|
|
238
|
+
'ASK' 'not observable from files' `
|
|
239
|
+
'Who owns this app - the person or team who decides changes and is called when it breaks?'
|
|
240
|
+
|
|
241
|
+
Add-Item 'data-recovery' 'State can be restored from a backup' $false `
|
|
242
|
+
$(if ($ignoredHeavy.Count -eq 0 -and @($inv.excludableSummary | Where-Object { $_.category -eq 'db-data' }).Count -eq 0) { Limit-ByCoverage 'PASS' } else { 'ASK' }) `
|
|
243
|
+
"${coverageNote}the repository carries schema and migrations; data comes from a backup" `
|
|
244
|
+
'Where is the backup of this app''s data, how often is it taken, and when was a restore last tested?'
|
|
245
|
+
|
|
246
|
+
Add-Item 'docs-readme' 'A README says what it is and how to run it' $false `
|
|
247
|
+
$(if ($hasReadme) { 'PASS' } else { 'GAP' }) `
|
|
248
|
+
$(if ($hasReadme) { (@($inv.docs | Where-Object { $_ -match '(?i)^readme' }) -join ', ') } else { 'no README at the root' })
|
|
249
|
+
|
|
250
|
+
Add-Item 'agent-rules' 'A rules file the coding agents read' $false `
|
|
251
|
+
$(if ($hasAgentRules) { 'PASS' } else { 'GAP' }) `
|
|
252
|
+
$(if ($hasAgentRules) { (@($inv.docs | Where-Object { $_ -match '(?i)^(agents|claude)\.md$' }) -join ', ') } else { 'none' }) `
|
|
253
|
+
'' 'Without it every agent re-derives the project rules, and each one derives them differently.'
|
|
254
|
+
|
|
255
|
+
Add-Item 'no-junctions' 'No junction or symlink inside the tree git manages' $false `
|
|
256
|
+
$(if ($junctionsInTree.Count -eq 0) { 'PASS' } else { 'GAP' }) `
|
|
257
|
+
$(if ($junctionsInTree.Count -eq 0) { 'none outside dependency folders' } else { "$($junctionsInTree.Count): $((@($junctionsInTree | Select-Object -First 3 | ForEach-Object { $_.path })) -join ', ')" }) `
|
|
258
|
+
'' 'git''s recursive remove FOLLOWS a junction and deletes the target''s contents. Point at shared state with an absolute path instead.'
|
|
259
|
+
|
|
260
|
+
Add-Item 'profile' 'The stack is declared, not assumed' $true `
|
|
261
|
+
$(if (@($prof.components).Count -gt 0) { 'PASS' } else { 'GAP' }) `
|
|
262
|
+
$(if (@($prof.components).Count -gt 0) { (@($prof.components | ForEach-Object { "$($_.path)=$($_.profile)" }) -join ', ') } else { 'no component matched a profile' })
|
|
263
|
+
|
|
264
|
+
# ---- apply answers and waivers -------------------------------------------------------
|
|
265
|
+
foreach ($it in $items) {
|
|
266
|
+
if ($it.verdict -eq 'ASK' -and $answers.ContainsKey($it.id) -and "$($answers[$it.id])".Trim() -ne '') {
|
|
267
|
+
$it.verdict = 'ANSWERED'
|
|
268
|
+
$it.answer = "$($answers[$it.id])"
|
|
269
|
+
}
|
|
270
|
+
if ($Waive -contains $it.id -and $it.verdict -ne 'PASS') {
|
|
271
|
+
# A waiver is recorded as a waiver, never rewritten as a pass. The difference is
|
|
272
|
+
# the whole value of the record six months later.
|
|
273
|
+
$it.waived = $true
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
$blockingBad = @($items | Where-Object { $_.blocking -and $_.verdict -eq 'GAP' -and -not $_.waived })
|
|
278
|
+
$blockingAsk = @($items | Where-Object { $_.blocking -and $_.verdict -eq 'ASK' -and -not $_.waived })
|
|
279
|
+
|
|
280
|
+
$report = [ordered]@{
|
|
281
|
+
name = $Name
|
|
282
|
+
testedAt = (Get-Date).ToUniversalTime().ToString('o')
|
|
283
|
+
partialInventory = [bool]$inv.partial
|
|
284
|
+
counts = [ordered]@{
|
|
285
|
+
pass = @($items | Where-Object { $_.verdict -eq 'PASS' }).Count
|
|
286
|
+
gap = @($items | Where-Object { $_.verdict -eq 'GAP' }).Count
|
|
287
|
+
ask = @($items | Where-Object { $_.verdict -eq 'ASK' }).Count
|
|
288
|
+
answered = @($items | Where-Object { $_.verdict -eq 'ANSWERED' }).Count
|
|
289
|
+
waived = @($items | Where-Object { $_.waived }).Count
|
|
290
|
+
}
|
|
291
|
+
blockingGaps = @($blockingBad | ForEach-Object { $_.id })
|
|
292
|
+
blockingQuestions = @($blockingAsk | ForEach-Object { $_.id })
|
|
293
|
+
items = $items
|
|
294
|
+
}
|
|
295
|
+
$out = Write-AdoptArtifact -Dir $dir -FileName 'conformance.json' -Object $report
|
|
296
|
+
|
|
297
|
+
# ---- report --------------------------------------------------------------------------
|
|
298
|
+
Write-AdoptHeading "Conformance: $Name"
|
|
299
|
+
foreach ($it in $items) {
|
|
300
|
+
$mark = switch ($it.verdict) {
|
|
301
|
+
'PASS' { 'PASS ' }
|
|
302
|
+
'GAP' { 'GAP ' }
|
|
303
|
+
'ASK' { 'ASK ' }
|
|
304
|
+
'ANSWERED' { 'ANSW ' }
|
|
305
|
+
default { '??? ' }
|
|
306
|
+
}
|
|
307
|
+
$colour = switch ($it.verdict) {
|
|
308
|
+
'PASS' { 'Green' }
|
|
309
|
+
'GAP' { if ($it.blocking) { 'Red' } else { 'Yellow' } }
|
|
310
|
+
'ASK' { if ($it.blocking) { 'Red' } else { 'Yellow' } }
|
|
311
|
+
default { 'Gray' }
|
|
312
|
+
}
|
|
313
|
+
$block = if ($it.blocking) { '*' } else { ' ' }
|
|
314
|
+
$waiv = if ($it.waived) { ' [WAIVED]' } else { '' }
|
|
315
|
+
Write-Host (" {0}{1} {2,-22} {3}{4}" -f $mark, $block, $it.id, $it.title, $waiv) -ForegroundColor $colour
|
|
316
|
+
if ($it.evidence) { Write-Host " $($it.evidence)" -ForegroundColor DarkGray }
|
|
317
|
+
if ($it.verdict -eq 'ASK' -and $it.question) { Write-Host " Q: $($it.question)" -ForegroundColor DarkYellow }
|
|
318
|
+
if ($it.verdict -eq 'ANSWERED') { Write-Host " A: $($it.answer)" -ForegroundColor DarkGray }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
Write-Host ""
|
|
322
|
+
Write-Host " * = blocking. $($report.counts.pass) pass, $($report.counts.gap) gaps, $($report.counts.ask) open questions, $($report.counts.answered) answered, $($report.counts.waived) waived."
|
|
323
|
+
if ($report.partialInventory) {
|
|
324
|
+
Write-Host " The inventory was PARTIAL, so every 'nothing found' above is 'nothing found so far'." -ForegroundColor Yellow
|
|
325
|
+
}
|
|
326
|
+
Write-Host " wrote $out"
|
|
327
|
+
|
|
328
|
+
if ($blockingBad.Count -gt 0) {
|
|
329
|
+
Write-Host ""
|
|
330
|
+
Write-Host "NOT READY TO ADOPT - blocking gaps: $((@($blockingBad | ForEach-Object { $_.id })) -join ', ')" -ForegroundColor Red
|
|
331
|
+
exit 1
|
|
332
|
+
}
|
|
333
|
+
if ($blockingAsk.Count -gt 0) {
|
|
334
|
+
Write-Host ""
|
|
335
|
+
Write-Host "NOT READY TO ADOPT - these blocking questions have no answer yet:" -ForegroundColor Red
|
|
336
|
+
foreach ($q in $blockingAsk) { Write-Host " [$($q.id)] $($q.question)" -ForegroundColor Red }
|
|
337
|
+
Write-Host "Record each with -Answer $($blockingAsk[0].id)=`"...`" once the owner has answered." -ForegroundColor Red
|
|
338
|
+
exit 2
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
Write-Host ""
|
|
342
|
+
Write-Host "Every blocking item is satisfied." -ForegroundColor Green
|
|
343
|
+
exit 0
|
package/bundles/manifest.json
CHANGED
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|