门禁拆分与提速

This commit is contained in:
LIm1tZz7 committed 2026-10-03 23:16:21 +08:00
1 parent 03a1f2bfb2
commit 59027684e6
11 files changed
+642 -22

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
# gate-check — 项目门禁
独立门禁目录,不再放在 `localdata/`(那里只留历史日志)。入口是 `run-gate.ps1`,三条车道并行。
## 快速开始
```powershell
pwsh -NoProfile -File gate-check/run-gate.ps1 # 全量
pwsh -NoProfile -File gate-check/run-gate.ps1 -Clean # 先清各项目自己的构建工件
pwsh -NoProfile -File gate-check/run-gate.ps1 -Full # 连依赖副本一起清(最慢,怀疑旧产物复用时才用)
pwsh -NoProfile -File gate-check/run-gate.ps1 -NoLint # 跳过 cargo clippy
pwsh -NoProfile -File gate-check/run-gate.ps1 -Only server # 只跑一条车道:cargo|stack|comments
```
## 车道与步骤
| 车道 | 步骤 | 说明 |
| --- | --- | --- |
| build | `cargo build --release` | 先单独跑:Haskell 测试要从 `chusql-core/storage/target/release` 加载动态库 |
| cargo | `cargo test --release`、`cargo clippy --all-targets -- -D warnings` | `-NoLint` 跳过 clippy |
| stack | engine → server → web → cli 的 `stack test <pkg> --fast` | 串行:stack 与快照目录不能并发写 |
| comments | `comment-check.ps1`、`comment-purity.ps1` | 注释长度与注释纯度 |
`build` 车道先跑完,再并行启动其余三条;这样动态库刷新与 Haskell 测试不会互相抢文件。
## 退出码与日志
- 每步打印一行 `STEP_EXIT <步骤> <码>`;任一非零则 `GATE_EXIT=1`,否则 `GATE_EXIT=0`。
- 末尾打印 `STEPS=...`(各步骤退出码)与总用时。
- 日志在 `gate-check/logs/<车道>.log`(stderr 在 `.err.log`),终端实时转发同一份内容;文件不进版本库。
- `comment-check.ps1` 有问题数时退出 1;`comment-purity.ps1` 只报告差异,恒退出 0。
## 相对旧脚本的提速点
1. 默认不再 `stack clean --full`:改为零清理;`-Clean` 只清各项目自己的工件,`-Full` 才回到旧行为。
2. 车道并行:Rust 测试 + clippy、四个 Haskell 套件、注释检查同时跑。
3. 输出流式:旧脚本 `| Out-String` 要等整步跑完才吐字,这里逐行实时转发。
4. 单车道开关 `-Only`,改一个模块时不用全量。
## 约定
- 门禁由项目所有者手动运行;agent 只跑模块级测试,不跑全量门禁。
- 期望基线(随里程碑更新):Engine 357 / Server 88 / Web 56 / CLI 10 / Rust 140。
- 计数与主线进度以 `agent_credientials/project-plan.txt` 为准。
+140
View File
@@ -0,0 +1,140 @@
# 注释规范合规检查:文件级注释 ≤3 行且 ≤90 字,函数级注释 ≤30 字
# 有问题的文件不静默:问题数大于 0 时退出码为 1
param([string]$Root = (Split-Path -Parent $PSScriptRoot), [string[]]$Only)
$codeDirs = @('chusql-core','chusql-server','chusql-web','chusql-cli','chusql-bootstrap','chusql-interface','chusql-benchmark','scripts','gate-check')
$files = @()
foreach ($d in $codeDirs) {
$p = Join-Path $Root $d
if (Test-Path -LiteralPath $p) {
$files += Get-ChildItem -LiteralPath $p -Recurse -File -Include *.hs,*.rs,*.js,*.css,*.html,*.ps1,*.sh |
Where-Object { $_.FullName -notmatch '\\\.stack-work\\|\\dist-newstyle\\|\\target\\|\\node_modules\\|\\logs\\|\\tmp\\' }
}
}
if ($Only) { $files = $files | Where-Object { $n = $_.Name; ($Only | Where-Object { $n -like $_ }).Count -gt 0 } }
$files = $files | Sort-Object FullName
$files = $files | Where-Object { $_.FullName -notmatch '\\localdata\\' }
$problems = @()
$scanned = 0
$fnCount = 0
$fileBlockCount = 0
foreach ($f in $files) {
$lines = @(Get-Content -LiteralPath $f.FullName)
$ext = $f.Extension.ToLower()
$scanned++
# ---- 函数级注释长度(只查每个函数/方法的“摘要行”:连续 doc 注释组第一行;# Safety 小节与非函数声明不计)----
$docRe = '^\s*(--\s*\|\s?|///\s?|//!\s?)(.*)$'
for ($i = 0; $i -lt $lines.Count; $i++) {
$l = $lines[$i]
$body = $null
if ($l -notmatch $docRe) {
if ($ext -eq '.js' -and $l -match '^\s*//\s?(.*)$') { $body = $Matches[1] } else { continue }
}
else { $body = $Matches[2] }
if ($i -gt 0) {
$prev = $lines[$i - 1]
$prevDoc = ($prev -match $docRe) -or ($ext -eq '.js' -and $prev -match '^\s*//\s?')
if ($prevDoc) { continue }
}
$k = $i
while ($k -lt $lines.Count -and (($lines[$k] -match $docRe) -or ($lines[$k] -match '^\s*#\[') -or ($ext -eq '.js' -and $lines[$k] -match '^\s*//'))) { $k++ }
$decl = if ($k -lt $lines.Count) { $lines[$k] } else { '' }
$isFn = switch ($ext) {
'.rs' { $decl -match '\bfn\s' }
'.hs' { $decl -match '^\s*[a-z_][A-Za-z0-9_'']*\s*::' }
'.js' { ($decl -match '^\s*(export\s+)?(async\s+)?function\b') -or ($decl -match '^\s*(export\s+)?(const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*(async\s*)?(function\b|\()') }
default { $false }
}
if (-not $isFn) { continue }
$t = $body.Trim()
if ($t -eq '' -or $t[0] -eq '#') { continue }
$fnCount++
if ($t.Length -gt 30) {
$problems += [pscustomobject]@{ Kind = 'FN-LONG'; File = $f.FullName; Line = $i + 1; Info = "$($t.Length)字 $t" }
}
}
# ---- 找出“外部引用”最后一行(含跨行的 import{...} / param(...) 块)----
$lastRef = -1
for ($i = 0; $i -lt $lines.Count; $i++) {
$l = $lines[$i]
$hit = $false
if ($l -match '^(import|use)\s' -or $l -match '^#\[macro_use\]' -or $l -match '^\s*#include\s' -or
$l -match '^@import\s' -or $l -match '^\s*<link\b' -or $l -match '^\s*<script\b.*\bsrc=') { $hit = $true }
if ($ext -ne '.hs' -and $l -match '^(param\s*\(|Set-StrictMode)') { $hit = $true }
if ($ext -eq '.ps1' -and $l -match '^\$ErrorActionPreference\s*=') { $hit = $true }
if ($ext -eq '.sh' -and ($l -match '^set -[eoux]' -or $l -match '^\.\s+"?[^"]*source' -or $l -match '^source\s')) { $hit = $true }
if (-not $hit) { continue }
$depth = ([regex]::Matches($l, '\(')).Count - ([regex]::Matches($l, '\)')).Count
$k = $i
while ($depth -gt 0 -and $k + 1 -lt $lines.Count) {
$k++
$depth += ([regex]::Matches($lines[$k], '\(')).Count - ([regex]::Matches($lines[$k], '\)')).Count
}
if ($ext -eq '.js' -and $l -match '^\s*import\b') {
while ($k + 1 -lt $lines.Count -and $lines[$k] -notmatch ';\s*$') { $k++ }
}
$lastRef = $k
$i = $k
}
# ---- Haskell:module 之前不该再有注释残留 ----
if ($ext -eq '.hs') {
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match '^module\s') { break }
if ($lines[$i] -match '^\s*--' -and $lines[$i] -notmatch '^\{-#') {
$problems += [pscustomobject]@{ Kind = 'COMMENT-BEFORE-MODULE'; File = $f.FullName; Line = $i + 1; Info = $lines[$i].Trim() }
}
}
}
# ---- 文件级注释块 ----
$start = $lastRef + 1
# 跳过紧跟在引用后的空行
while ($start -lt $lines.Count -and $lines[$start].Trim() -eq '') { $start++ }
$block = @()
$j = $start
while ($j -lt $lines.Count) {
$l = $lines[$j]
if ($l.Trim() -eq '') { if ($block.Count -gt 0) { break } else { $j++; continue } }
if ($l -match '^\s*(--|//|#|/\*|\*|<!--|;)') { $block += $l.Trim(); $j++ } else { break }
}
if ($block.Count -eq 0) {
# 退一步:文件最顶部(跳过 shebang 与 {-# ... #-})的注释块也算文件级注释(Rust //! / 脚本头部)
$t = 0
while ($t -lt $lines.Count -and ($lines[$t] -match '^#!' -or $lines[$t] -match '^\{-#' -or $lines[$t].Trim() -eq '')) { $t++ }
$top = @()
while ($t -lt $lines.Count -and $lines[$t] -match '^\s*(--|//|#|/\*|\*|<!--|;)') { $top += $lines[$t].Trim(); $t++ }
if ($top.Count -eq 0) {
$problems += [pscustomobject]@{ Kind = 'NO-FILE-COMMENT'; File = $f.FullName; Line = $start + 1; Info = $(if ($start -lt $lines.Count) { $lines[$start].Trim() } else { '<EOF>' }) }
}
else { $fileBlockCount++ }
}
else {
$fileBlockCount++
$chars = 0
foreach ($b in $block) {
$t = $b -replace '^\s*(--|//|#|/\*|\*|<!--|;)\s?', ''
$t = $t -replace '\s*\*/\s*$', ''
$chars += $t.Trim().Length
}
# 只统计“文件级”块(连续注释行中,如果第一行是 -- | 说明它其实是给紧随声明的 haddock,不算文件头)
$isHaddock = $block[0] -match '^--\s*\|\s' -or $block[0] -match '^///'
if (-not $isHaddock -and ($block.Count -gt 3 -or $chars -gt 90)) {
$problems += [pscustomobject]@{ Kind = 'FILE-BLOCK-LONG'; File = $f.FullName; Line = $start + 1; Info = "$($block.Count)行/$chars字" }
}
}
}
"扫描文件: $scanned"
"函数级注释条数: $fnCount"
"检出文件级注释块: $fileBlockCount"
"问题数: $($problems.Count)"
if ($problems.Count -gt 0) {
$problems | Format-Table -AutoSize -Wrap | Out-String -Width 220
exit 1
}
exit 0
+62
View File
@@ -0,0 +1,62 @@
# 注释纯度审计:去掉纯注释行后把工作区与 git index 逐行比对
# 相同说明改动只有注释;有差异的文件会被列出,仅作人工判断不设退出码
param([string]$Root = (Split-Path -Parent $PSScriptRoot))
Set-Location $Root
$dirs = @('chusql-core','chusql-server','chusql-web','chusql-cli','chusql-bootstrap','chusql-interface','chusql-benchmark','scripts','gate-check')
$files = @()
foreach ($d in $dirs) {
$p = Join-Path $Root $d
if (Test-Path -LiteralPath $p) {
$files += Get-ChildItem -LiteralPath $p -Recurse -File -Include *.hs,*.rs,*.js,*.css,*.html,*.ps1,*.sh |
Where-Object { $_.FullName -notmatch '\\\.stack-work\\|\\dist-newstyle\\|\\target\\|\\node_modules\\|\\localdata\\|\\logs\\|\\tmp\\' }
}
}
$files = $files | Sort-Object FullName
function Is-CommentLine([string]$l, [string]$ext) {
switch ($ext) {
'.hs' { return $l -match '^\s*--' }
'.rs' { return $l -match '^\s*//' }
'.js' { return $l -match '^\s*(//|/\*|\*)' }
'.css' { return $l -match '^\s*(/\*|\*|//)' }
'.html' { return $l -match '^\s*(<!--|-->)' }
default { return $l -match '^\s*#' }
}
}
function Strip-Comments([string[]]$lines, [string]$ext) {
$out = New-Object System.Collections.Generic.List[string]
foreach ($l in $lines) { if (-not (Is-CommentLine $l $ext)) { $out.Add($l.TrimEnd()) } }
return , $out
}
$pure = 0; $new = 0; $diffFiles = @()
foreach ($f in $files) {
$rel = (Resolve-Path -LiteralPath $f.FullName -Relative) -replace '^\.\\', '' -replace '\\', '/'
git cat-file -e ":$rel" 2>$null | Out-Null
$tracked = ($LASTEXITCODE -eq 0)
if (-not $tracked) { $new++; continue }
$wt = Strip-Comments @(Get-Content -LiteralPath $f.FullName) $f.Extension.ToLower()
$idx = Strip-Comments @(git show ":$rel") $f.Extension.ToLower()
if ($wt.Count -eq $idx.Count) {
$same = $true
for ($i = 0; $i -lt $wt.Count; $i++) { if ($wt[$i] -cne $idx[$i]) { $same = $false; break } }
if ($same) { $pure++; continue }
}
# 统计差异行
$set = @{}
foreach ($l in $idx) { if ($set.ContainsKey($l)) { $set[$l]++ } else { $set[$l] = 1 } }
$extra = @()
foreach ($l in $wt) {
if ($set.ContainsKey($l) -and $set[$l] -gt 0) { $set[$l]-- } else { $extra += $l }
}
$removed = @()
foreach ($k in $set.Keys) { if ($set[$k] -gt 0) { $removed += ("{0}x {1}" -f $set[$k], $k) } }
$diffFiles += [pscustomobject]@{ File = $rel; Added = $extra.Count; Removed = $removed.Count; Sample = (($extra | Select-Object -First 3) -join ' | ') }
}
"扫描文件: $($files.Count)"
"仅注释变更(去注释后与 index 完全相同): $pure"
"index 无基线(新文件,跳过): $new"
"含非注释差异的文件: $($diffFiles.Count)"
if ($diffFiles.Count -gt 0) { $diffFiles | Format-Table -AutoSize -Wrap | Out-String -Width 240 }
+119
View File
@@ -0,0 +1,119 @@
# 门禁入口:并行跑 Rust / Haskell / 注释三条车道,日志实时输出
# 用法见 README.md;-Clean 重建本地依赖,-Only 只跑一条车道
param(
[string]$Root = (Split-Path -Parent $PSScriptRoot),
[switch]$Clean,
[switch]$Full,
[switch]$NoLint,
[string[]]$Only
)
$ErrorActionPreference = 'Stop'
$LogDir = Join-Path $PSScriptRoot 'logs'
$TmpDir = Join-Path $PSScriptRoot 'tmp'
New-Item -ItemType Directory -Force -Path $LogDir, $TmpDir | Out-Null
$env:PATH = (Join-Path $Root 'chusql-core\storage\target\release') + ';' + $env:PATH
$env:TEMP = $TmpDir
$env:TMP = $TmpDir
Set-Location -LiteralPath $Root
$colors = @{ build = 'DarkGray'; cargo = 'Yellow'; stack = 'Cyan'; comments = 'Green' }
$laneNames = @('cargo', 'stack', 'comments')
if ($Only) {
$laneNames = @($laneNames | Where-Object { $Only -contains $_ })
if ($laneNames.Count -eq 0) { throw "unknown lane: $($Only -join ',')" }
}
# 读日志里新出现的行并带车道前缀打印,只保留行数游标
function Show-New([object]$Lane) {
foreach ($part in @('Log', 'ErrLog')) {
$path = $Lane.$part
$seen = [int]$Lane."Seen$part"
$lines = @()
try { $lines = @(Get-Content -LiteralPath $path -Encoding UTF8 -ErrorAction Stop) } catch { $lines = @() }
if ($lines.Count -gt $seen) {
foreach ($line in $lines[$seen..($lines.Count - 1)]) {
Write-Host ("{0} {1}" -f $Lane.Tag, $line) -ForegroundColor $colors[$Lane.Name]
}
$Lane."Seen$part" = $lines.Count
}
}
}
# 起一条车道:独立进程写自己的日志,主进程只做流式转发
function Start-Lane([string]$Name) {
$log = Join-Path $LogDir "$Name.log"
$errLog = Join-Path $LogDir "$Name.err.log"
Remove-Item -LiteralPath $log, $errLog -Force -ErrorAction SilentlyContinue
$stepArgs = @('-NoProfile', '-File', (Join-Path $PSScriptRoot 'run-step.ps1'), '-Root', $Root, '-Kind', $Name)
if ($NoLint) { $stepArgs += '-NoLint' }
$proc = Start-Process -FilePath 'pwsh' -ArgumentList $stepArgs -RedirectStandardOutput $log `
-RedirectStandardError $errLog -NoNewWindow -PassThru
return [pscustomobject]@{
Name = $Name; Proc = $proc; Log = $log; ErrLog = $errLog
Tag = "[$Name]"; SeenLog = 0; SeenErrLog = 0; Exit = $null
}
}
# 等一批车道跑完,边等边转发输出
function Wait-Lanes($Lanes) {
while ($true) {
$running = 0
foreach ($lane in $Lanes) {
$lane.Proc.Refresh()
Show-New $lane
if (-not $lane.Proc.HasExited) { $running++ }
}
if ($running -eq 0) { break }
Start-Sleep -Milliseconds 300
}
Start-Sleep -Milliseconds 400
foreach ($lane in $Lanes) { Show-New $lane; $lane.Exit = $lane.Proc.ExitCode }
}
# 清理本地依赖工件:-Clean 只清各项目自己的,-Full 连依赖副本一起清
function Invoke-Clean {
foreach ($dir in @('chusql-core\engine', 'chusql-server', 'chusql-web', 'chusql-cli')) {
$argv = @('clean')
if ($Full) { $argv += '--full' }
Write-Host "== stack $($argv -join ' ') in $dir" -ForegroundColor DarkGray
Push-Location -LiteralPath (Join-Path $Root $dir)
try { & stack @argv 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray } }
finally { Pop-Location }
}
}
$started = Get-Date
if ($Clean -or $Full) { Invoke-Clean }
# 先单独建好 Rust 动态库:Haskell 测试要从 target\release 加载它
$build = Start-Lane 'build'
Write-Host '== 先建 Rust 动态库' -ForegroundColor DarkGray
Wait-Lanes @($build)
Write-Host "== 车道并行:$($laneNames -join ' / ')" -ForegroundColor DarkGray
$lanes = @()
foreach ($name in $laneNames) { $lanes += Start-Lane $name }
Wait-Lanes $lanes
'--- 门禁结果 ---'
$allExit = @($build.Exit)
$failed = 0
foreach ($lane in $lanes) {
$codes = @()
$steps = @(Select-String -LiteralPath $lane.Log -Pattern '^STEP_EXIT (\S+) (-?\d+)$' -ErrorAction SilentlyContinue)
foreach ($m in $steps) {
$codes += "$($m.Matches[0].Groups[1].Value)=$($m.Matches[0].Groups[2].Value)"
}
$allExit += $lane.Exit
if ($lane.Exit -ne 0) { $failed++ }
'车道 {0,-9} exit={1,-3} {2}' -f $lane.Name, $lane.Exit, ($codes -join ' ')
$tail = @(Get-Content -LiteralPath $lane.Log -Encoding UTF8 -ErrorAction SilentlyContinue | Select-Object -Last 5)
foreach ($line in $tail) { " $line" }
}
$elapsed = [int]((Get-Date) - $started).TotalSeconds
"用时: ${elapsed}s"
"STEPS=$($allExit -join ',')"
if ($failed -gt 0 -or $build.Exit -ne 0) { 'GATE_EXIT=1'; exit 1 }
'GATE_EXIT=0'
exit 0
+59
View File
@@ -0,0 +1,59 @@
# 单条车道:按 -Kind 依次跑步骤,每步打印 STEP_EXIT,全零才退出 0
param(
[string]$Root = (Split-Path -Parent $PSScriptRoot),
[Parameter(Mandatory)][ValidateSet('build', 'cargo', 'stack', 'comments')][string]$Kind,
[switch]$NoLint
)
$ErrorActionPreference = 'Continue'
$codes = @()
# 跑一个步骤:切目录、输出原样透传、记录退出码
function Invoke-Step([string]$Label, [string]$Dir, [string[]]$Argv) {
Write-Host "=== $Label ==="
$code = 1
Push-Location -LiteralPath $Dir
try {
$exe = $Argv[0]
$rest = @($Argv | Select-Object -Skip 1)
& $exe @rest
if ($null -ne $global:LASTEXITCODE) { $code = [int]$global:LASTEXITCODE }
}
catch {
Write-Host "--- $Label raised: $_"
$code = 1
}
finally { Pop-Location }
Write-Host "--- $Label exit $code"
"STEP_EXIT $Label $code"
$script:codes += $code
}
$storage = Join-Path $Root 'chusql-core\storage'
$engine = Join-Path $Root 'chusql-core\engine'
switch ($Kind) {
'build' {
Invoke-Step 'cargo-build' $storage @('cargo', 'build', '--release')
}
'cargo' {
Invoke-Step 'cargo-test' $storage @('cargo', 'test', '--release')
if (-not $NoLint) {
Invoke-Step 'cargo-clippy' $storage @('cargo', 'clippy', '--all-targets', '--', '-D', 'warnings')
}
}
'stack' {
Invoke-Step 'engine' $engine @('stack', 'test', 'chusql-core-engine', '--fast')
Invoke-Step 'server' (Join-Path $Root 'chusql-server') @('stack', 'test', 'chusql-server', '--fast')
Invoke-Step 'web' (Join-Path $Root 'chusql-web') @('stack', 'test', 'chusql-web', '--fast')
Invoke-Step 'cli' (Join-Path $Root 'chusql-cli') @('stack', 'test', 'chusql-cli', '--fast')
}
'comments' {
$check = Join-Path $PSScriptRoot 'comment-check.ps1'
$purity = Join-Path $PSScriptRoot 'comment-purity.ps1'
Invoke-Step 'comment-check' $Root @('pwsh', '-NoProfile', '-File', $check, '-Root', $Root)
Invoke-Step 'comment-purity' $Root @('pwsh', '-NoProfile', '-File', $purity, '-Root', $Root)
}
}
$failed = @($codes | Where-Object { $_ -ne 0 })
if ($failed.Count -gt 0) { exit 1 }
exit 0