docs(report): 新增 PROJECT_REPORT.md 与 CI/CD 流水线记录

PROJECT_REPORT.md:论文式项目报告(摘要 / 目录 / 7 章 / 参考文献 / 致谢 / 附录 A),
4 张 Mermaid 图同样经真实浏览器渲染验证。所有数字都与证据文件逐个核对过。

.scratch/ci-cd/:本次流水线的全部证据与可重跑脚本
  * 阶段报告:依赖安全扫描 / 许可证合规 / 阶段 0 静态分析 / 阶段 3 测试与性能
  * 证据:分析器原始输出(修复前 84 条、修复后 80 条)、Pester 详细输出、
    性能基线 JSON、黑盒用例结果(阶段 1 的 11 例与阶段 2 回归的 12 例)
  * 可重跑:blackbox-tests.ps1 / blackbox-regression.ps1 / perf-baseline.ps1

两条边界必须写在明处,不能含糊:

  * **VM 内验证只取到修复前的快照**(Pester 183 项全绿)。随后会话审批策略改为 never,
    gsudo 提权被自动拒绝(退出码 999),而 Hyper-V 与 PowerShell Direct 都需要管理员,
    于是修复后的三套件在 VM 内**没有跑成**。报告里明确标注了范围,没有把"宿主跑绿了"
    说成"VM 也跑绿了"。
  * **性能基线是首次建立**,无历史可比,故无退化可判;指标只在同机同宿主下对比,
    跨机比数字没有意义。

另外记一笔:静态分析的口径是"仓库自己的门禁"。剩余 80 条全是风格类(0 Error),且逐条
有依据 —— 行长 160 是配置里写明的有意偏离,PSPlaceCloseBrace 等集中在测试夹具字符串内
(改了会改变断言语义)。严格模式的"零容忍"在这里与仓库自身约定相抵,选择尊重仓库约定
并在报告里登记,而不是制造一个横跨 12 个文件的纯排版大 diff。
This commit is contained in:
Shuery committed 2026-10-02 01:17:40 +08:00
1 parent f4729baca7
commit 45bbd66815
18 files changed
+2974

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
# 宿主性能基线:对 BakNRet 的关键路径做可复现的计时。
# 设计原则:所有指标都是"比值"或"固定夹具下的绝对耗时",可以在同一台机器上重复对比。
param(
[string]$OutJson = '.scratch\ci-cd\20260928-001722\perf_baseline.json',
[int]$Repeat = 5,
[string]$Pwsh = 'pwsh'
)
$ErrorActionPreference = 'Stop'
$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$modulePath = Join-Path $root 'BakNRet\BakNRet.psd1'
$listPath = Join-Path $root 'BackupList.txt'
$cfgPath = Join-Path $root 'BackupConfig.psd1'
function Get-Median([double[]]$Values) {
$s = $Values | Sort-Object
$n = $s.Count
if ($n -eq 0) { return 0 }
if ($n % 2 -eq 1) { return $s[[int](($n - 1) / 2)] }
return ($s[$n / 2 - 1] + $s[$n / 2]) / 2
}
function Measure-Once([scriptblock]$Body) {
$sw = [System.Diagnostics.Stopwatch]::StartNew()
& $Body | Out-Null
$sw.Stop()
return $sw.Elapsed.TotalMilliseconds
}
$results = [ordered]@{
measuredAt = (Get-Date).ToString('s')
machine = $env:COMPUTERNAME
os = [System.Environment]::OSVersion.VersionString
psi = $PSVersionTable.PSVersion.ToString()
cpu = (Get-CimInstance Win32_Processor | Select-Object -First 1 -ExpandProperty Name)
logicalCpu = [int](Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors
workdir = $root
metrics = @{}
notes = @()
}
# ---------------------------------------------------------------- 夹具(固定内容,避免与真实数据联动)
$fixture = Join-Path $env:TEMP ('baknret-perf-' + [guid]::NewGuid().ToString('N').Substring(0, 8))
New-Item -ItemType Directory -Path $fixture -Force | Out-Null
$srcDir = Join-Path $fixture 'src'
New-Item -ItemType Directory -Path $srcDir -Force | Out-Null
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$rnd = [Random]::new(20260928)
$payload = New-Object byte[] 32768
for ($f = 0; $f -lt 200; $f++) {
$sub = Join-Path $srcDir ('d{0:D2}' -f ($f % 10))
if (-not (Test-Path $sub)) { New-Item -ItemType Directory -Path $sub -Force | Out-Null }
$rnd.NextBytes($payload)
[System.IO.File]::WriteAllBytes((Join-Path $sub ("f$f.bin")), $payload)
}
$sw.Stop()
$results.fixture = [ordered]@{ files = 200; bytes = 200 * 32768; createMs = [math]::Round($sw.Elapsed.TotalMilliseconds, 1) }
# ---------------------------------------------------------------- 1. 模块导入
$imports = 1..$Repeat | ForEach-Object {
Measure-Once { & $Pwsh -NoProfile -Command "Import-Module '$modulePath' -Force" }
}
$results.metrics.moduleImportMs = [ordered]@{
min = [math]::Round(($imports | Measure-Object -Minimum).Minimum, 1)
median = [math]::Round((Get-Median $imports), 1)
max = [math]::Round(($imports | Measure-Object -Maximum).Maximum, 1)
samples = $Repeat
}
# ---------------------------------------------------------------- 2. 清单解析
$parseScript = @"
Import-Module '$modulePath' -Force
1..50 | ForEach-Object { Get-Content -LiteralPath '$listPath' | ForEach-Object { ConvertFrom-BackupListLine -Line `$_ } }
"@
$parseFile = Join-Path $fixture 'parse.ps1'
Set-Content -LiteralPath $parseFile -Value $parseScript -Encoding utf8
$parses = 1..$Repeat | ForEach-Object { Measure-Once { & $Pwsh -NoProfile -File $parseFile } }
$results.metrics.backupListParse50xMs = [ordered]@{
min = [math]::Round(($parses | Measure-Object -Minimum).Minimum, 1)
median = [math]::Round((Get-Median $parses), 1)
max = [math]::Round(($parses | Measure-Object -Maximum).Maximum, 1)
samples = $Repeat
}
# ---------------------------------------------------------------- 3. 只读干跑(真实清单 + 空间预估 + manifest)
$dryFile = Join-Path $fixture 'dry.ps1'
Set-Content -LiteralPath $dryFile -Encoding utf8 -Value @"
Set-Location '$root'
& '$root\Backup-Data.ps1' -DryRun -BackupDir '$fixture\Backups' -ConfigPath '$cfgPath' *> `$null
exit `$LASTEXITCODE
"@
$dries = 1..$Repeat | ForEach-Object { Measure-Once { & $Pwsh -NoProfile -File $dryFile } }
$results.metrics.dryRunFullListMs = [ordered]@{
min = [math]::Round(($dries | Measure-Object -Minimum).Minimum, 1)
median = [math]::Round((Get-Median $dries), 1)
max = [math]::Round(($dries | Measure-Object -Maximum).Maximum, 1)
samples = $Repeat
}
# ---------------------------------------------------------------- 4. 7z 压缩吞吐(固定夹具,无压缩)
$sevenZip = (Get-Command 7z -ErrorAction SilentlyContinue).Source
if (-not $sevenZip) {
foreach ($p in 'C:\Programs\Scoop\shims\7z.exe', 'C:\Program Files\7-Zip\7z.exe') { if (Test-Path $p) { $sevenZip = $p; break } }
}
if ($sevenZip) {
$archive = Join-Path $fixture 'bench.7z'
$bench = @()
foreach ($lvl in 0, 5, 9) {
if (Test-Path $archive) { Remove-Item $archive -Force }
$ms = Measure-Once { & $sevenZip a -t7z "-mx=$lvl" -bso0 -bsp0 $archive (Join-Path $srcDir '*') }
$bench += [pscustomobject]@{ level = $lvl; ms = [math]::Round($ms, 1); archiveBytes = (Get-Item $archive).Length }
}
$results.metrics.sevenZipBench = @($bench)
$results.fixture.compressionInputBytes = 200 * 32768
$results.metrics.sevenZipVersion = (& $sevenZip | Select-Object -First 2 | Select-Object -Last 1)
}
else {
$results.notes += '找不到 7z.exe,跳过压缩基准'
}
# ---------------------------------------------------------------- 5. 源树扫描(Get-BakNRetFolderSummary 走真实目录)
$summaryScript = @"
Import-Module '$modulePath' -Force
`$sw = [System.Diagnostics.Stopwatch]::StartNew()
1..3 | ForEach-Object { Get-BakNRetFolderSummary -FolderPath '$srcDir' | Out-Null }
`$sw.Stop()
Write-Output ([math]::Round(`$sw.Elapsed.TotalMilliseconds / 3, 1))
"@
$sumFile = Join-Path $fixture 'summary.ps1'
Set-Content -LiteralPath $sumFile -Value $summaryScript -Encoding utf8
$sumMs = (& $Pwsh -NoProfile -File $sumFile | Select-Object -Last 1)
$results.metrics.folderSummary200Files3xMs = [double]$sumMs
Remove-Item -LiteralPath $fixture -Recurse -Force -ErrorAction SilentlyContinue
$results | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $root $OutJson) -Encoding utf8
Write-Host ('基线已写入 {0}' -f $OutJson)
$results.metrics | ConvertTo-Json -Depth 6