<# value-scan / value-dig 产物机械校验(替代人工核对) ------------------------------------------------------------------ 用法: # S1 候选清单 powershell -ExecutionPolicy Bypass -File check.ps1 -Mode scan ` -Product <产物路径> -Template /value-scan/assets/候选清单模板.md # S4 功能点清单 powershell -ExecutionPolicy Bypass -File check.ps1 -Mode dig ` -Product <产物路径> -Template /value-dig/assets/深度模板/功能点清单模板.md # S4 设计思路与取舍 / 改造方案(整篇级文档,只跑通用校验) powershell -ExecutionPolicy Bypass -File check.ps1 -Mode doc -Product -Template 退出码:0 = 无 FAIL;1 = 有 FAIL 说明:FAIL = 必错;WARN = 需人判断 #> param( [Parameter(Mandatory = $true)] [ValidateSet('scan', 'dig', 'doc')] [string]$Mode, [Parameter(Mandatory = $true)] [string]$Product, [string]$Template ) $ErrorActionPreference = 'Stop' $script:fail = New-Object System.Collections.Generic.List[string] $script:warn = New-Object System.Collections.Generic.List[string] $script:pass = New-Object System.Collections.Generic.List[string] function Fail($m) { $script:fail.Add($m) } function Warn($m) { $script:warn.Add($m) } function Pass($m) { $script:pass.Add($m) } $prodPath = (Resolve-Path -LiteralPath $Product).Path $text = [System.IO.File]::ReadAllText($prodPath, [System.Text.Encoding]::UTF8) if ($text.Length -gt 0 -and [int][char]$text[0] -eq 0xFEFF) { $text = $text.Substring(1) } $lines = $text -split "`r?`n" # ---------------------------------------------------------------- 围栏代码块 $fences = @() $inFence = $false; $fStart = 0; $fLang = '' for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match '^\s*```') { if (-not $inFence) { $inFence = $true; $fStart = $i; $fLang = ($lines[$i] -replace '^\s*```', '').Trim() } else { $inFence = $false $fences += [pscustomobject]@{ Start = $fStart; End = $i; Lang = $fLang; Body = ($i - $fStart - 1) } } } } if ($inFence) { Fail "存在未闭合的代码围栏(起始行 $($fStart + 1))" } # ---------------------------------------------------------------- 通用 1:引号逐行配对 $oddQuoteLines = @() for ($i = 0; $i -lt $lines.Count; $i++) { if ((([regex]::Matches($lines[$i], '"')).Count % 2) -ne 0) { $oddQuoteLines += ($i + 1) } } if ($oddQuoteLines.Count -eq 0) { Pass '引号逐行配对' } else { Fail ("引号未配对的行: " + ($oddQuoteLines -join ', ')) } # ---------------------------------------------------------------- 通用 2:mermaid 合法性 $mermaidFences = @($fences | Where-Object { $_.Lang -eq 'mermaid' }) $mmBad = 0 foreach ($f in $mermaidFences) { $body = ($lines[($f.Start + 1)..($f.End - 1)] -join "`n") $firstLine = (($body -split "`r?`n") | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1) $endCount = ([regex]::Matches($body, '(?m)^\s*end\s*$')).Count if ($firstLine -match 'sequenceDiagram') { # 时序图:end 收的是 alt/opt/loop/par/critical/break/rect $blk = ([regex]::Matches($body, '(?m)^\s*(alt|opt|loop|par|critical|break|rect)\b')).Count if ($blk -ne $endCount) { Fail "mermaid 时序图(第 $($f.Start + 1) 行起)alt/opt/loop 等 $blk 个但 end=$endCount"; $mmBad++ } } else { $sg = ([regex]::Matches($body, '(?m)^\s*subgraph\s')).Count if ($sg -ne $endCount) { Fail "mermaid 流程图(第 $($f.Start + 1) 行起)subgraph=$sg 但 end=$endCount"; $mmBad++ } if ($body -match '(?m)^\s*subgraph\s+\S+\s*\[' -and $body -notmatch '(?m)^\s*subgraph\s+\S+\s*\["') { Fail "mermaid(第 $($f.Start + 1) 行起)subgraph 缺引号标题,必须写 subgraph id[`"标题`"]"; $mmBad++ } if ($body -match '(?m)^\s*subgraph\s+\S+\s*\[\(') { Fail "mermaid(第 $($f.Start + 1) 行起)用了 subgraph xxx[(...)],会解析失败"; $mmBad++ } } } if ($mermaidFences.Count -gt 0 -and $mmBad -eq 0) { Pass "mermaid 块 $($mermaidFences.Count) 个通过" } # ---------------------------------------------------------------- 通用 3:表格列数一致 $ti = 0 while ($ti -lt $lines.Count) { if ($lines[$ti] -match '^\s*\|') { $blk = @(); $bs = $ti while ($ti -lt $lines.Count -and $lines[$ti] -match '^\s*\|') { $blk += $lines[$ti]; $ti++ } $counts = @($blk | ForEach-Object { ([regex]::Matches($_, '\|')).Count } | Sort-Object -Unique) if ($counts.Count -gt 1) { Fail "表格列数不一致(第 $($bs + 1) 行起):pipe 数 = $($counts -join '/')" } } else { $ti++ } } # ---------------------------------------------------------------- 通用 3b:表格不得缩进(嵌套在列表内的表多数渲染器不显示) $indentedTable = @() for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match '^[ \t]+\|') { $indentedTable += ($i + 1) } } if ($indentedTable.Count -eq 0) { Pass '表格均顶格(无嵌套缩进表)' } else { Fail ('表格存在缩进(第 ' + ($indentedTable -join ', ') + ' 行起)——嵌套在列表里的表多数渲染器不显示,必须顶格') } # ---------------------------------------------------------------- 通用 4:章节完整性(模板必备节 ⊆ 产出节) if ($Template) { $tPath = (Resolve-Path -LiteralPath $Template).Path $tText = [System.IO.File]::ReadAllText($tPath, [System.Text.Encoding]::UTF8) if ($tText.Length -gt 0 -and [int][char]$tText[0] -eq 0xFEFF) { $tText = $tText.Substring(1) } $tLines = $tText -split "`r?`n" $tHeads = $tLines | Where-Object { $_ -match '^\s*#+\s+\S' } $missingLiteral = @(); $missingPlaceholder = @() foreach ($h in $tHeads) { $t = ($h -replace '^\s*#+\s+', '') -replace '\s+$', '' $hasPh = $t.Contains([string][char]0x27E8) $sentinel = '@@PH@@' $phRx = [string][char]0x27E8 + '[^' + [string][char]0x27E9 + ']*' + [string][char]0x27E9 $rx = [regex]::Escape([regex]::Replace($t, $phRx, $sentinel)).Replace($sentinel, '.*') $hit = $false foreach ($pl in $lines) { if ($pl -match ('^\s*#+\s+' + $rx + '\s*$')) { $hit = $true; break } } if (-not $hit) { if ($hasPh) { $missingPlaceholder += $t } else { $missingLiteral += $t } } } if ($missingLiteral.Count -eq 0) { Pass '章节完整性:模板必备节全部存在' } else { Fail ('缺章节(字面量,必错): ' + ($missingLiteral -join ' | ')) } if ($missingPlaceholder.Count -gt 0) { Warn ('示例性章节未匹配(可能条数不同,需人判): ' + ($missingPlaceholder -join ' | ')) } } # ---------------------------------------------------------------- 定位辅助 function Get-HeadIndex($pattern) { for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match $pattern) { return $i } } return -1 } function Get-NextHeadIndex($from) { for ($i = $from + 1; $i -lt $lines.Count; $i++) { if ($lines[$i] -match '^\s*#+\s+\S') { return $i } } return $lines.Count } # ---------------------------------------------------------------- scan 专属 if ($Mode -eq 'scan') { # 1) 禁止围栏代码块(mermaid 除外) $nonMermaid = @($fences | Where-Object { $_.Lang -ne 'mermaid' }) if ($nonMermaid.Count -eq 0) { Pass '深度闸门:S1 无围栏代码块(仅 mermaid)' } else { Fail ("S1 不允许围栏代码块,发现 $($nonMermaid.Count) 个(第 " + (($nonMermaid | ForEach-Object { $_.Start + 1 }) -join ', ') + ' 行起)') } # 2) 锚点格式:路径#方法名(不带行号,每条最多 2 个) $badAnchor = @() $anchorCount = 0 foreach ($l in $lines) { if ($l -match '^\s*\*\*锚点\*\*\s*[::]\s*(.+?)\s*$') { $anchorCount++ $v = $Matches[1] $items = @($v -split '[、,,]' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) if ($items.Count -gt 2) { $badAnchor += ("锚点超过 2 个($($items.Count) 个): " + $l.Trim()) } foreach ($it in $items) { $a = $it.Trim().Trim('`') if ($a -match ':\d') { $badAnchor += ("S1 锚点不写行号 -> $a") } elseif ($a -notmatch '\w[/\\]\w') { $badAnchor += ("S1 锚点须为 路径#方法名(机制级可到类名) -> $a") } } } } if ($anchorCount -eq 0) { Warn '未找到 **锚点** 行(若产物为空则忽略)' } elseif ($badAnchor.Count -eq 0) { Pass "锚点格式合规($anchorCount 处,路径#方法名)" } else { Fail ('锚点格式不合规: ' + ($badAnchor -join ' ; ')) } # 3) 闸门状态节 if ((Get-HeadIndex '^\s*#+\s*闸门状态') -ge 0) { Pass '有「闸门状态」节(S2 门控有证据)' } else { Fail '缺「闸门状态」节 —— S2 门控没有证据' } # 4) 候选条数(提示性:超 12 才 FAIL,无下限硬卡) $starCount = ($lines | Where-Object { $_ -match '^\s*#+\s*★' }).Count if ($starCount -le 12) { Pass "候选条数 $starCount(≤12)" } else { Fail "候选条数 $starCount 超过 12(颗粒度掉到实现层,需重并)" } if ($starCount -lt 5 -and $starCount -gt 0) { Warn "候选条数 $starCount 少于 5——先确认是否采样不到位,而非真没有" } } # ---------------------------------------------------------------- dig 专属 if ($Mode -eq 'dig') { $skelIdx = Get-HeadIndex '^\s*#+\s*主干调用骨架' $skelEnd = if ($skelIdx -ge 0) { Get-NextHeadIndex $skelIdx } else { -1 } if ($skelIdx -ge 0) { Pass '有「主干调用骨架」节' } else { Warn '未找到「主干调用骨架」节(仅 S4 ① 功能点清单必需)' } # 1) 局部代码块 ≤10 行(骨架块豁免) $exempt = 0 foreach ($f in $fences) { if ($f.Lang -eq 'mermaid') { continue } $isSkel = ($skelIdx -ge 0 -and $f.Start -gt $skelIdx -and $f.Start -lt $skelEnd) if ($isSkel) { $exempt++; continue } if ($f.Body -gt 10) { Warn "局部代码块超过 10 行(第 $($f.Start + 1) 行起,$($f.Body) 行)——行数非硬限,请人判断是关键片段还是源码摘录" } } Pass ("局部代码块行数检查完成(豁免骨架块 $exempt 个)") # 2) 骨架每行必须带方式标记 if ($skelIdx -ge 0) { $skelFence = @($fences | Where-Object { $_.Start -gt $skelIdx -and $_.Start -lt $skelEnd }) | Select-Object -First 1 if (-not $skelFence) { Warn '骨架节里没有代码块' } else { $missTag = @() foreach ($bl in ($lines[($skelFence.Start + 1)..($skelFence.End - 1)])) { if ([string]::IsNullOrWhiteSpace($bl)) { continue } if ($bl -match '^\s*//') { continue } # 分段注释行 if ($bl -match ':\s*$') { continue } # 块头行(xxx(): ),标"谁"不标"怎么做" if ($bl -notmatch '[(\[]') { continue } # 连接词行(↓ 三路汇合) if ($bl -notmatch '//') { $missTag += $bl.Trim() } # 其余动作行必须有方式标记 } if ($missTag.Count -eq 0) { Pass '骨架每行都带方式标记' } else { Fail "骨架有 $($missTag.Count) 行缺方式标记: " + (($missTag | Select-Object -First 3) -join ' ; ') } } } # 3) 证据须含 行号 if ($text -notmatch '\.(java|xml|yaml|yml|sql):\d+') { Fail '未找到任何 `文件:行号` 证据(本阶段必须有可核对的锚点)' } else { Pass '存在 `文件:行号` 形式的证据' } } # ---------------------------------------------------------------- doc 专属 if ($Mode -eq 'doc') { # 改造方案识别:含「现状问题登记」节(设计思路与取舍模板不含此节) $isPlan = (Get-HeadIndex '^\s*#+\s*0\.\s*现状问题登记') -ge 0 if ($isPlan) { # 1) 必有非 mermaid 代码片段(伪代码 / SQL) $codeFences = @($fences | Where-Object { $_.Lang -ne 'mermaid' }) if ($codeFences.Count -ge 1) { Pass "改造方案含关键片段 $($codeFences.Count) 个" } else { Fail '改造方案缺「关键片段」代码块——必须"照着能讲代码"(伪代码/SQL 均可,只禁大段源码摘录)' } # 2) 片段应覆盖"骨架 + 改造点",只给一块多半只在开头充数 if ($codeFences.Count -ge 2) { Pass "关键片段 $($codeFences.Count) 个(骨架 + 改造点)" } elseif ($codeFences.Count -eq 1) { Warn '只有 1 个关键片段——可能只有链路骨架,各改造点未给伪代码,需人判' } } } # ---------------------------------------------------------------- 输出 Write-Output "" Write-Output ("=" * 68) Write-Output "机械校验 Mode=$Mode" Write-Output (" product : " + $prodPath) if ($Template) { Write-Output (" template : " + (Resolve-Path -LiteralPath $Template).Path) } Write-Output ("=" * 68) if ($script:pass.Count -gt 0) { Write-Output "" Write-Output "[PASS]" foreach ($m in $script:pass) { Write-Output (" + " + $m) } } if ($script:warn.Count -gt 0) { Write-Output "" Write-Output "[WARN] 需人判断" foreach ($m in $script:warn) { Write-Output (" ! " + $m) } } if ($script:fail.Count -gt 0) { Write-Output "" Write-Output "[FAIL] 必错" foreach ($m in $script:fail) { Write-Output (" x " + $m) } } Write-Output "" Write-Output ("结果:PASS {0} / WARN {1} / FAIL {2}" -f $script:pass.Count, $script:warn.Count, $script:fail.Count) if ($script:fail.Count -gt 0) { exit 1 } else { exit 0 }