forked from Gitlink/gitlink-cli
400 lines
18 KiB
PowerShell
400 lines
18 KiB
PowerShell
# GitLink Research - Scenario 3: Compliance & Reproducibility Check (PowerShell)
|
|
param(
|
|
[string]$Owner, [string]$Repo, [string]$LocalPath = ".",
|
|
[string]$Output = "", [switch]$NoWiki, [switch]$DryRun
|
|
)
|
|
|
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
Import-Module "$ScriptDir\..\lib\common.psm1" -Force -WarningAction SilentlyContinue
|
|
|
|
$ErrorActionPreference = "Continue"
|
|
Check-Auth
|
|
$resolved = Resolve-OwnerRepo -Owner $Owner -Repo $Repo
|
|
$Owner = $resolved.Owner; $Repo = $resolved.Repo
|
|
$Today = Get-Date -Format "yyyy-MM-dd"
|
|
$OutputDir = Join-Path $ScriptDir "..\output"
|
|
if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null }
|
|
$OutputFile = if ($Output) { $Output } else { Join-Path $OutputDir "reproducibility-${Repo}-${Today}.html" }
|
|
|
|
Log-Title "GitLink Research - Compliance & Reproducibility"
|
|
|
|
# Get repo metadata for description and DOI detection
|
|
Log-Step "Fetching repo metadata..."
|
|
$repoJson = Invoke-GLCheck @("repo", "+info", "--owner", $Owner, "--repo", $Repo)
|
|
$repoDesc = if ($repoJson.data.description) { $repoJson.data.description } else { "" }
|
|
$doiFound = ""
|
|
if ($repoDesc -match '10\.\d{4,}/[\w.\-/]+') { $doiFound = $Matches[0] }
|
|
|
|
# Scoring: 0, 0.5, 1.0 per dimension
|
|
$dLicense = @{Score=0; Detail=""}; $dNoSecret = @{Score=0; Detail=""}
|
|
$dReadme = @{Score=0; Detail=""}; $dDeps = @{Score=0; Detail=""}
|
|
$dBuild = @{Score=0; Detail=""}; $dCI = @{Score=0; Detail=""}
|
|
$dTest = @{Score=0; Detail=""}; $dData = @{Score=0; Detail=""}
|
|
|
|
# -- Helper: ensure local clone of target repo for compliance scan --
|
|
function Ensure-LocalClone {
|
|
param([string]$Owner, [string]$Repo, [string]$LocalPath)
|
|
|
|
$result = @{ ScanPath = $null; NeedCleanup = $false }
|
|
|
|
# Check if current directory is already a clone of the target repo
|
|
if (Test-Path (Join-Path $LocalPath ".git")) {
|
|
Push-Location $LocalPath
|
|
try {
|
|
$remoteUrl = git remote get-url origin 2>$null
|
|
if ($remoteUrl -and ($remoteUrl -match [regex]::Escape("$Owner/$Repo"))) {
|
|
Pop-Location
|
|
Log-Info "Local path is already a clone of $Owner/$Repo"
|
|
$result.ScanPath = $LocalPath
|
|
return $result
|
|
}
|
|
} catch { }
|
|
Pop-Location
|
|
}
|
|
|
|
# Clone target repo to temp directory for compliance scan
|
|
$suffix = [System.Guid]::NewGuid().ToString().Substring(0, 8)
|
|
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "gl-compliance-${Repo}-${suffix}"
|
|
Log-Info "Cloning $Owner/$Repo to temp directory for compliance scan..."
|
|
|
|
$cloneUrl = if ($env:GITLINK_TOKEN) {
|
|
"https://oauth2:$env:GITLINK_TOKEN@gitlink.org.cn/$Owner/$Repo.git"
|
|
} else {
|
|
"https://gitlink.org.cn/$Owner/$Repo.git"
|
|
}
|
|
|
|
$cloneOutput = git clone --depth 1 $cloneUrl $tempDir 2>&1
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Log-Warn "HTTPS clone failed, trying SSH..."
|
|
$sshUrl = "git@gitlink.org.cn:$Owner/$Repo.git"
|
|
$cloneOutput = git clone --depth 1 $sshUrl $tempDir 2>&1
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Log-Warn "Clone failed for both HTTPS and SSH. Skipping compliance scan."
|
|
Log-Warn "Reason: $($cloneOutput -join '; ')"
|
|
return $result
|
|
}
|
|
}
|
|
|
|
Log-Ok "Cloned successfully to $tempDir"
|
|
$result.ScanPath = $tempDir
|
|
$result.NeedCleanup = $true
|
|
return $result
|
|
}
|
|
|
|
# 1. Compliance scan
|
|
Log-Step "1/7 Compliance scan..."
|
|
$cloneInfo = Ensure-LocalClone -Owner $Owner -Repo $Repo -LocalPath $LocalPath
|
|
if ($cloneInfo.ScanPath -and (Test-Path (Join-Path $cloneInfo.ScanPath ".git"))) {
|
|
Push-Location $cloneInfo.ScanPath
|
|
$compResult = Invoke-GL @("compliance", "+scan")
|
|
Pop-Location
|
|
if ($cloneInfo.NeedCleanup) {
|
|
# Git objects are read-only on Windows; strip attributes first
|
|
Get-ChildItem -Path $cloneInfo.ScanPath -Recurse -Force -ErrorAction SilentlyContinue |
|
|
ForEach-Object { $_.Attributes = 'Normal' }
|
|
Remove-Item -Recurse -Force $cloneInfo.ScanPath -ErrorAction SilentlyContinue
|
|
if (Test-Path $cloneInfo.ScanPath) {
|
|
# Fallback: let cmd handle stubborn files
|
|
cmd /c "rd /s /q `"$($cloneInfo.ScanPath)`"" 2>$null
|
|
}
|
|
if (-not (Test-Path $cloneInfo.ScanPath)) {
|
|
Log-Info "Cleaned up temp clone."
|
|
} else {
|
|
Log-Warn "Could not fully remove temp clone: $($cloneInfo.ScanPath)"
|
|
}
|
|
}
|
|
if ($compResult.ok) {
|
|
$licenseOk = if ($null -ne $compResult.data.license.status) { $compResult.data.license.status } else { "" }
|
|
if ($licenseOk -match "ok|clean|found") { $dLicense.Score = 1.0; $dLicense.Detail = "Licensed (OK)" }
|
|
elseif ($licenseOk -eq "warning") { $dLicense.Score = 0.5; $dLicense.Detail = "License non-standard" }
|
|
else { $dLicense.Score = 0; $dLicense.Detail = "No LICENSE file" }
|
|
|
|
# Fix: proper PS5.1 null check instead of "if @()"
|
|
$secFindings = if ($compResult.data.secrets.findings) { $compResult.data.secrets.findings } else { @() }
|
|
$secCount = if ($secFindings -is [array]) { $secFindings.Count } else { 0 }
|
|
$piiFindings = if ($compResult.data.exposure.findings) { $compResult.data.exposure.findings } else { @() }
|
|
$piiCount = if ($piiFindings -is [array]) { $piiFindings.Count } else { 0 }
|
|
|
|
if ($secCount -eq 0 -and $piiCount -eq 0) { $dNoSecret.Score = 1.0; $dNoSecret.Detail = "No secrets/PII found" }
|
|
elseif ($secCount + $piiCount -le 3) { $dNoSecret.Score = 0.5; $dNoSecret.Detail = "Few suspicious items found" }
|
|
else { $dNoSecret.Score = 0; $dNoSecret.Detail = "Multiple secret/PII leaks" }
|
|
}
|
|
} else {
|
|
Log-Warn "Cannot access repo for compliance scan, skipping this dimension"
|
|
$dLicense.Detail = "Not scanned (repo inaccessible)"
|
|
$dNoSecret.Detail = "Not scanned (repo inaccessible)"
|
|
}
|
|
|
|
# 2. README
|
|
Log-Step "2/7 README completeness..."
|
|
try {
|
|
$readmeResult = Invoke-GL @("api", "GET", "raw/$Owner/$Repo/master/README.md")
|
|
if ($readmeResult.ok) { $readmeText = if ($null -ne $readmeResult.data) { $readmeResult.data } else { "" } } else { $readmeText = "" }
|
|
} catch { $readmeText = "" }
|
|
|
|
$sectionCount = 0
|
|
foreach ($kw in @("# ", "## ", "Install", "Usage", "License", "Contribut", "Citation")) {
|
|
if ($readmeText -match $kw) { $sectionCount++ }
|
|
}
|
|
if ($sectionCount -ge 5) { $dReadme.Score = 1.0; $dReadme.Detail = "README complete, $sectionCount sections" }
|
|
elseif ($sectionCount -ge 3) { $dReadme.Score = 0.5; $dReadme.Detail = "README partial, $sectionCount sections" }
|
|
else { $dReadme.Score = 0; $dReadme.Detail = "README missing or too short" }
|
|
Log-Info " README sections: $sectionCount"
|
|
|
|
# 3. Dependencies
|
|
Log-Step "3/7 Dependency declaration..."
|
|
$subResult = Invoke-GL @("api", "GET", "/v1/$Owner/$Repo/sub_entries?ref=master")
|
|
$depFiles = 0
|
|
$depList = ""
|
|
$names = @()
|
|
if ($subResult.ok -and ($subResult.data -is [array])) {
|
|
$names = @($subResult.data | ForEach-Object { if ($null -ne $_.name) { $_.name } else { "" } })
|
|
foreach ($df in @("package.json","go.mod","requirements.txt","pyproject.toml","Cargo.toml","CMakeLists.txt","pom.xml","build.gradle","Gemfile","Makefile")) {
|
|
if ($names -contains $df) { $depFiles++; $depList += "$df, " }
|
|
}
|
|
}
|
|
if ($depFiles -ge 1) { $dDeps.Score = 1.0; $dDeps.Detail = "Standard dep file(s): $depList".TrimEnd(', ') }
|
|
elseif ($readmeText -match "dependenc|requirement|install") { $dDeps.Score = 0.5; $dDeps.Detail = "Deps mentioned in README" }
|
|
else { $dDeps.Score = 0; $dDeps.Detail = "No dependency declaration" }
|
|
Log-Info " Dep files: $depFiles"
|
|
|
|
# 4. Build
|
|
Log-Step "4/7 Build instructions..."
|
|
$buildScore = 0
|
|
if ($readmeText -match "build|install|compile|make|run") { $buildScore++ }
|
|
if ($names | Where-Object { $_ -match "Makefile|Dockerfile|docker-compose" }) { $buildScore++ }
|
|
if ($names | Where-Object { $_ -match "\.github/workflows|\.gitlab-ci|Jenkinsfile" }) { $buildScore++ }
|
|
|
|
if ($buildScore -ge 3) { $dBuild.Score = 1.0; $dBuild.Detail = "Detailed build docs + automation" }
|
|
elseif ($buildScore -ge 1) { $dBuild.Score = 0.5; $dBuild.Detail = "Partial build instructions" }
|
|
else { $dBuild.Score = 0; $dBuild.Detail = "No build instructions" }
|
|
Log-Info " Build score: $buildScore/3"
|
|
|
|
# 5. CI
|
|
Log-Step "5/7 CI configuration..."
|
|
$ciResult = Invoke-GL @("ci", "+builds", "--owner", $Owner, "--repo", $Repo, "--limit", "10")
|
|
$ciBuilds = if ($ciResult.ok) { @($ciResult.data).Count } else { 0 }
|
|
$ciOk = 0
|
|
if ($ciResult.ok) {
|
|
foreach ($b in @($ciResult.data)) {
|
|
$status = if ($b.status) { $b.status } else { "" }
|
|
if ($status -eq "success" -or $status -eq "completed") { $ciOk++ }
|
|
}
|
|
}
|
|
if ($ciBuilds -gt 0 -and $ciOk -ge 1) { $dCI.Score = 1.0; $dCI.Detail = "CI configured & passing ($ciOk/$ciBuilds)" }
|
|
elseif ($ciBuilds -gt 0) { $dCI.Score = 0.5; $dCI.Detail = "CI exists but failing" }
|
|
else { $dCI.Score = 0; $dCI.Detail = "No CI configured" }
|
|
Log-Info " CI builds: $ciBuilds"
|
|
|
|
# 6. Tests
|
|
Log-Step "6/7 Test evidence..."
|
|
$testScore = 0
|
|
if ($names | Where-Object { $_ -match "^test/|^tests/|^spec/|^__tests__/" }) { $testScore++ }
|
|
if ($names | Where-Object { $_ -match "_test\.|\.test\.|_spec\.|\.spec\.|test_" }) { $testScore++ }
|
|
if ($readmeText -match "test|validate") { $testScore++ }
|
|
|
|
if ($testScore -ge 3) { $dTest.Score = 1.0; $dTest.Detail = "Test dir + files + instructions" }
|
|
elseif ($testScore -ge 1) { $dTest.Score = 0.5; $dTest.Detail = "Partial test evidence" }
|
|
else { $dTest.Score = 0; $dTest.Detail = "No test evidence" }
|
|
Log-Info " Test evidence: $testScore/3"
|
|
|
|
# 7. Data
|
|
Log-Step "7/7 Data availability..."
|
|
$dataScore = 0
|
|
$dataEvidence = ""
|
|
if ($readmeText -match "dataset|data/|zenodo|figshare|kaggle|huggingface") { $dataScore++; $dataEvidence = "keyword found" }
|
|
if ($readmeText -match "https?://[^\s]+(?:zenodo|figshare|data\.|dataset)") { $dataScore++; $dataEvidence += ", data link found" }
|
|
if ($doiFound) { $dataScore++; $dataEvidence += ", DOI/article found" }
|
|
|
|
if ($dataScore -ge 2) { $dData.Score = 1.0; $dData.Detail = "Clear data statement: $dataEvidence" }
|
|
elseif ($dataScore -ge 1) { $dData.Score = 0.5; $dData.Detail = "Partial data statement: $dataEvidence" }
|
|
else { $dData.Score = 0; $dData.Detail = "No data availability statement" }
|
|
Log-Info " Data score: $dataScore/3"
|
|
|
|
# Total score
|
|
$weights = @(0.15, 0.15, 0.15, 0.15, 0.10, 0.10, 0.10, 0.10)
|
|
$scores = @($dLicense.Score, $dNoSecret.Score, $dReadme.Score, $dDeps.Score, $dBuild.Score, $dCI.Score, $dTest.Score, $dData.Score)
|
|
$totalScore = 0
|
|
for ($i = 0; $i -lt 8; $i++) { $totalScore += $scores[$i] * $weights[$i] }
|
|
$totalScore = [Math]::Round($totalScore * 100, 1)
|
|
|
|
$grade = if ($totalScore -ge 85) { "A" } elseif ($totalScore -ge 70) { "B" } elseif ($totalScore -ge 55) { "C" } elseif ($totalScore -ge 40) { "D" } else { "F" }
|
|
$gradeLabel = if ($grade -eq "A") { "Excellent - highly reproducible" } elseif ($grade -eq "B") { "Good - largely reproducible" } elseif ($grade -eq "C") { "Fair - partially reproducible" } elseif ($grade -eq "D") { "Poor - difficult to reproduce" } else { "Fail - barely reproducible" }
|
|
$gradeColor = if ($grade -eq "A") { "#2e7d32" } elseif ($grade -eq "B") { "#558b2f" } elseif ($grade -eq "C") { "#f57c00" } elseif ($grade -eq "D") { "#e65100" } else { "#c62828" }
|
|
|
|
# Build recommendation strings
|
|
$recLicense = if ($dLicense.Score -ne 1.0) { "Add MIT/Apache-2.0/GPL-3.0 license file" } else { "-" }
|
|
$recSecret = if ($dNoSecret.Score -ne 1.0) { "Remove leaked secrets, use environment variables" } else { "-" }
|
|
$recReadme = if ($dReadme.Score -ne 1.0) { "Add purpose, install, usage, license, citation sections" } else { "-" }
|
|
$recDeps = if ($dDeps.Score -ne 1.0) { "Add package.json/go.mod/requirements.txt etc." } else { "-" }
|
|
$recBuild = if ($dBuild.Score -ne 1.0) { "Add Makefile/Dockerfile + build steps in README" } else { "-" }
|
|
$recCI = if ($dCI.Score -ne 1.0) { "Configure GitLink CI or GitHub Actions" } else { "-" }
|
|
$recTest = if ($dTest.Score -ne 1.0) { "Add unit/integration tests, document test commands" } else { "-" }
|
|
$recData = if ($dData.Score -ne 1.0) { "Document dataset sources, provide Zenodo/Figshare link" } else { "-" }
|
|
|
|
# Icon helpers
|
|
function ScoreIcon($s) {
|
|
if ($s -eq 1.0) { return "✅" }
|
|
elseif ($s -eq 0.5) { return "⚠️" }
|
|
else { return "❌" }
|
|
}
|
|
function ScorePercent($s) { [Math]::Round($s * 100) }
|
|
|
|
# ===== Generate HTML Report =====
|
|
Log-Step "Generating HTML scorecard..."
|
|
$dims = @("License","No Secrets/PII","README","Dependencies","Build","CI","Tests","Data")
|
|
$dets = @($dLicense, $dNoSecret, $dReadme, $dDeps, $dBuild, $dCI, $dTest, $dData)
|
|
$recs = @($recLicense, $recSecret, $recReadme, $recDeps, $recBuild, $recCI, $recTest, $recData)
|
|
$pcts = @(0.15, 0.15, 0.15, 0.15, 0.10, 0.10, 0.10, 0.10)
|
|
|
|
# Build dimension table rows
|
|
$dimRows = ""
|
|
for ($i = 0; $i -lt 8; $i++) {
|
|
$icon = ScoreIcon $scores[$i]
|
|
$pct = ScorePercent $scores[$i]
|
|
$suggestion = $recs[$i]
|
|
$detail = $dets[$i].Detail
|
|
$name = $dims[$i]
|
|
$dimRows += @"
|
|
<tr>
|
|
<td>$name</td>
|
|
<td>$icon</td>
|
|
<td>$detail</td>
|
|
<td>$suggestion</td>
|
|
</tr>
|
|
"@
|
|
}
|
|
|
|
$htmlContent = @"
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>$Owner/$Repo —Reproducibility Scorecard</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; color: #333; }
|
|
.header { background: linear-gradient(135deg, #1a237e 0%, #3949ab 100%); color: #fff; padding: 40px 30px; }
|
|
.header h1 { font-size: 26px; margin-bottom: 6px; }
|
|
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
.row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; margin-bottom: 24px; }
|
|
.panel { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,.08); }
|
|
.panel h2 { font-size: 18px; margin-bottom: 16px; color: #1a237e; border-bottom: 2px solid #3949ab; padding-bottom: 8px; }
|
|
.chart { width: 100%; height: 350px; }
|
|
.grade-circle { text-align: center; padding: 20px; }
|
|
.grade-letter { font-size: 72px; font-weight: 900; color: $gradeColor; }
|
|
.grade-score { font-size: 24px; color: #888; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
th, td { padding: 10px 14px; text-align: left; border-bottom: 1px solid #eee; font-size: 13px; }
|
|
th { background: #f5f7fa; color: #555; }
|
|
.footer { text-align: center; padding: 24px; color: #999; font-size: 12px; }
|
|
@media (max-width: 768px) { .row { grid-template-columns: 1fr; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>$Owner/$Repo —Research Reproducibility Scorecard</h1>
|
|
<div style="opacity:0.8;font-size:14px;">$Today</div>
|
|
</div>
|
|
<div class="container">
|
|
|
|
<div class="row">
|
|
<div class="panel grade-circle">
|
|
<div class="grade-letter">$grade</div>
|
|
<div class="grade-score">$totalScore / 100</div>
|
|
<div style="margin-top:12px;color:#888;">$gradeLabel</div>
|
|
</div>
|
|
<div class="panel">
|
|
<h2>Radar Chart</h2>
|
|
<div id="radarChart" class="chart"></div>
|
|
</div>
|
|
<div class="panel">
|
|
<h2>Dimension Scores</h2>
|
|
<table>
|
|
<tr><th>Dimension</th><th>Score</th><th>Weight</th></tr>
|
|
<tr><td>License</td><td>$(ScorePercent $dLicense.Score)%</td><td>15%</td></tr>
|
|
<tr><td>No Secrets/PII</td><td>$(ScorePercent $dNoSecret.Score)%</td><td>15%</td></tr>
|
|
<tr><td>README</td><td>$(ScorePercent $dReadme.Score)%</td><td>15%</td></tr>
|
|
<tr><td>Dependencies</td><td>$(ScorePercent $dDeps.Score)%</td><td>15%</td></tr>
|
|
<tr><td>Build</td><td>$(ScorePercent $dBuild.Score)%</td><td>10%</td></tr>
|
|
<tr><td>CI</td><td>$(ScorePercent $dCI.Score)%</td><td>10%</td></tr>
|
|
<tr><td>Tests</td><td>$(ScorePercent $dTest.Score)%</td><td>10%</td></tr>
|
|
<tr><td>Data</td><td>$(ScorePercent $dData.Score)%</td><td>10%</td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h2>Detailed Assessment & Recommendations</h2>
|
|
<table>
|
|
<tr><th>Dimension</th><th>Rating</th><th>Evidence</th><th>Suggestion</th></tr>
|
|
$dimRows
|
|
</table>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="footer">Generated by GitLink Research Assistant —$Today</div>
|
|
|
|
<script>
|
|
var radarChart = echarts.init(document.getElementById('radarChart'));
|
|
radarChart.setOption({
|
|
radar: {
|
|
indicator: [
|
|
{ name: 'License', max: 100 },
|
|
{ name: 'No Secrets', max: 100 },
|
|
{ name: 'README', max: 100 },
|
|
{ name: 'Deps', max: 100 },
|
|
{ name: 'Build', max: 100 },
|
|
{ name: 'CI', max: 100 },
|
|
{ name: 'Tests', max: 100 },
|
|
{ name: 'Data', max: 100 }
|
|
],
|
|
center: ['50%', '55%'],
|
|
radius: '70%'
|
|
},
|
|
series: [{
|
|
type: 'radar',
|
|
data: [{
|
|
value: [
|
|
$(ScorePercent $dLicense.Score),
|
|
$(ScorePercent $dNoSecret.Score),
|
|
$(ScorePercent $dReadme.Score),
|
|
$(ScorePercent $dDeps.Score),
|
|
$(ScorePercent $dBuild.Score),
|
|
$(ScorePercent $dCI.Score),
|
|
$(ScorePercent $dTest.Score),
|
|
$(ScorePercent $dData.Score)
|
|
],
|
|
name: 'Reproducibility',
|
|
areaStyle: { color: 'rgba(57,73,171,0.3)' },
|
|
lineStyle: { color: '#3949ab' }
|
|
}]
|
|
}]
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"@
|
|
|
|
if (-not $DryRun) {
|
|
$htmlContent | Out-File -FilePath $OutputFile -Encoding UTF8
|
|
Log-Ok "HTML scorecard generated: $OutputFile"
|
|
} else {
|
|
Log-Warn "[DRY RUN] Would generate: $OutputFile"
|
|
}
|
|
|
|
# Console summary
|
|
Divider
|
|
Write-Host "====== Reproducibility Scorecard ======" -ForegroundColor White
|
|
Write-Host " Total score: $totalScore/100 —$grade ($gradeLabel)"
|
|
$dimNames = @("License","No Secrets/PII","README","Dependencies","Build","CI","Tests","Data")
|
|
for ($i = 0; $i -lt 8; $i++) {
|
|
$icon = if ($scores[$i] -eq 1.0) { "OK" } elseif ($scores[$i] -eq 0.5) { "~" } else { "!!" }
|
|
Write-Host " $icon $($dimNames[$i]) ($(ScorePercent $scores[$i])%): $($dets[$i].Detail)"
|
|
}
|
|
Write-Host " Report: $OutputFile"
|
|
Divider
|
|
Log-Ok "Check complete"
|