dsh-plugin-guide 0.3.29 → 0.3.31
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/README-es.md +28 -1
- package/README-hi.md +28 -1
- package/README-pt.md +28 -1
- package/README-zh.md +26 -1
- package/README.md +42 -12
- package/SKILL.md +21 -6
- package/dist/dsh-plugin-dev.js +95 -29
- package/guide/plugin-dev-guide.md +98 -2
- package/guide/quick-reference.es.md +2 -0
- package/guide/quick-reference.hi.md +2 -0
- package/guide/quick-reference.pt.md +2 -0
- package/index.d.ts +32 -0
- package/index.js +24 -9
- package/package.json +17 -9
- package/references/community-repo-deep-dive.md +90 -125
- package/references/harness-repo.md +19 -0
- package/references/mirror-scope-decision.md +72 -0
- package/references/sources.md +1 -0
- package/references/website-pages.md +5 -0
- package/scripts/archive-discussions.ps1 +162 -162
- package/scripts/check-docs-drift.ps1 +55 -55
- package/scripts/check-ps1-bom.mjs +68 -0
- package/scripts/check-readme-parity.mjs +149 -0
- package/scripts/check-readme-sync.mjs +14 -9
- package/scripts/download-community-articles.ps1 +188 -188
- package/scripts/download-community-repos.ps1 +232 -232
- package/scripts/download-sources.ps1 +195 -195
- package/scripts/gen-topic-snapshot.ps1 +91 -91
- package/scripts/install-skill.ps1 +63 -63
- package/scripts/sync-official-docs.ps1 +138 -138
- package/scripts/verify-artifacts.mjs +26 -0
- package/scripts/verify-kit.ps1 +197 -194
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
# dsh-plugin-guide 官方 GitHub Discussions 归档脚本(可复跑,幂等)
|
|
2
|
-
# 输出: downloads/github/harness/discussions/
|
|
3
|
-
# list.json 全部讨论(title/body/category/answers/labels 等,正文即文档)
|
|
4
|
-
# comments-<n>.json 精选线程的全部评论(选择规则见下)
|
|
5
|
-
# _selection.tsv 精选线程清单
|
|
6
|
-
# README.md 分类统计 + 精选清单
|
|
7
|
-
# 说明: GitHub REST API;未提供 Token 时走匿名配额(60/h,可能不够),建议
|
|
8
|
-
# $env:GH_TOKEN 或 -Token <token>(经典 PAT,只需 public_repo 读权限)。
|
|
9
|
-
# 选择规则(与 08-14 首版覆盖度一致): Announcements 全部; Q&A/Ideas 评论>=1;
|
|
10
|
-
# General/Show and tell 评论>=3 且标题不含拉群/招聘/广告词。
|
|
11
|
-
# 传输用 Invoke-WebRequest(而非 curl.exe 数组参数): PS 5.1 下 curl 数组参数
|
|
12
|
-
# 的引号拆分会把 -H 头打碎,导致静默拿到错误对象——本脚本已规避该坑。
|
|
13
|
-
# 参数: -OutDir <dir> 覆盖输出目录; -SkipComments 只刷新 list.json 不拉评论。
|
|
14
|
-
param(
|
|
15
|
-
[string]$OutDir = '',
|
|
16
|
-
[string]$Repo = 'deepseek-ai/deepseek-harness',
|
|
17
|
-
[string]$Token = $env:GH_TOKEN,
|
|
18
|
-
[switch]$SkipComments
|
|
19
|
-
)
|
|
20
|
-
$ErrorActionPreference = 'Stop'
|
|
21
|
-
$ProgressPreference = 'SilentlyContinue'
|
|
22
|
-
if (-not $OutDir) { $OutDir = Join-Path (Split-Path $PSScriptRoot -Parent) 'downloads\github\harness\discussions' }
|
|
23
|
-
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
|
24
|
-
|
|
25
|
-
$headers = @{ 'User-Agent' = 'dsh-plugin-guide-research'; 'Accept' = 'application/vnd.github+json' }
|
|
26
|
-
if ($Token) { $headers['Authorization'] = "Bearer $Token" }
|
|
27
|
-
|
|
28
|
-
function Get-Api([string]$url) {
|
|
29
|
-
# 返回对象数组;失败/限流时抛错(带状态码),由调用方决定重试或中止。
|
|
30
|
-
try {
|
|
31
|
-
$resp = Invoke-WebRequest -Uri $url -Headers $headers -TimeoutSec 60 -UseBasicParsing
|
|
32
|
-
} catch {
|
|
33
|
-
$code = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }
|
|
34
|
-
throw "HTTP $code : $url"
|
|
35
|
-
}
|
|
36
|
-
$obj = $null
|
|
37
|
-
try { $obj = $resp.Content | ConvertFrom-Json } catch { throw "JSON parse failed: $url" }
|
|
38
|
-
if ($obj -is [System.Management.Automation.PSCustomObject] -and $obj.PSObject.Properties.Name -contains 'message') {
|
|
39
|
-
throw "API message '$($obj.message)' : $url"
|
|
40
|
-
}
|
|
41
|
-
return @($obj)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
# ---- 1) 全量 list(按创建序分页;REST 的 direction 参数对 discussions 无效,只能从头翻到尾) ----
|
|
45
|
-
$prevCount = 0
|
|
46
|
-
if (Test-Path (Join-Path $OutDir 'list.json')) {
|
|
47
|
-
try { $prev = @((Get-Content (Join-Path $OutDir 'list.json') -Raw -Encoding UTF8 | ConvertFrom-Json)); $prevCount = $prev.Count } catch { $prevCount = 0 }
|
|
48
|
-
}
|
|
49
|
-
$all = @()
|
|
50
|
-
$page = 1
|
|
51
|
-
# REST /discussions caps at 5000 rows (50 pages x 100). The cap is explicit so a
|
|
52
|
-
# silent truncation cannot be mistaken for a complete archive: the run reports
|
|
53
|
-
# when it stops at the ceiling, and the README records the ceiling alongside the
|
|
54
|
-
# GraphQL total_count (which is larger).
|
|
55
|
-
$pageCap = 50
|
|
56
|
-
while ($page -le $pageCap) {
|
|
57
|
-
$items = Get-Api "https://api.github.com/repos/$Repo/discussions?per_page=100&page=$page&sort=created"
|
|
58
|
-
if ($items.Count -eq 0) { Write-Output "page ${page}: 空页,列表结束"; break }
|
|
59
|
-
$all += $items
|
|
60
|
-
Write-Output ("page {0}: +{1} (累计 {2})" -f $page, $items.Count, $all.Count)
|
|
61
|
-
if ($items.Count -lt 100) { break }
|
|
62
|
-
$page++
|
|
63
|
-
Start-Sleep -Milliseconds 500
|
|
64
|
-
}
|
|
65
|
-
if ($all.Count -ge ($pageCap * 100)) {
|
|
66
|
-
Write-Output "WARN: 列表在第 $pageCap 页触顶(REST 5000 条上限)——归档为前 5000 条,非全量;更全的历史需按 GraphQL UPDATED_AT 分页补抓"
|
|
67
|
-
}
|
|
68
|
-
if ($all.Count -eq 0) { throw 'discussions 列表抓取失败(0 条)' }
|
|
69
|
-
if ($prevCount -gt 0 -and $all.Count -lt [Math]::Floor($prevCount * 0.9)) {
|
|
70
|
-
throw "列表疑似缩水: 上次 $prevCount 条, 本次仅 $all.Count 条 —— 拒绝覆盖归档,请检查网络/配额后重跑"
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
# 按 number 去重(分页边界可能重复)
|
|
74
|
-
$uniq = @{}
|
|
75
|
-
foreach ($d in $all) { $uniq[[int]$d.number] = $d }
|
|
76
|
-
$discs = @($uniq.Values | Sort-Object { [int]$_.number })
|
|
77
|
-
$listPath = Join-Path $OutDir 'list.json'
|
|
78
|
-
$discs | ConvertTo-Json -Depth 6 | Set-Content -Path $listPath -Encoding UTF8
|
|
79
|
-
Write-Output "list.json: $($discs.Count) 条 -> $listPath"
|
|
80
|
-
|
|
81
|
-
# ---- 2) 精选线程 + 评论 ----
|
|
82
|
-
$sel = @()
|
|
83
|
-
foreach ($d in $discs) {
|
|
84
|
-
$cat = $d.category.name
|
|
85
|
-
$c = [int]$d.comments
|
|
86
|
-
$t = [string]$d.title
|
|
87
|
-
$pick = $false
|
|
88
|
-
if ($cat -eq 'Announcements') { $pick = $true }
|
|
89
|
-
elseif ($cat -eq 'Q&A' -and $c -ge 1) { $pick = $true }
|
|
90
|
-
elseif ($cat -eq 'Ideas' -and $c -ge 1) { $pick = $true }
|
|
91
|
-
elseif (($cat -eq 'General' -or $cat -eq 'Show and tell') -and $c -ge 3) {
|
|
92
|
-
# 非社群拉群帖: 标题含拉群/扫码/加群/招聘/广告类词的不收
|
|
93
|
-
if ($t -notmatch '群|扫码|加群|招聘|内推|广告|投简历|纳新') { $pick = $true }
|
|
94
|
-
}
|
|
95
|
-
if ($pick) { $sel += $d }
|
|
96
|
-
}
|
|
97
|
-
$sel = @($sel | Sort-Object { [int]$_.number })
|
|
98
|
-
Write-Output "精选线程: $($sel.Count) 条"
|
|
99
|
-
|
|
100
|
-
if (-not $SkipComments) {
|
|
101
|
-
$n = 0
|
|
102
|
-
foreach ($d in $sel) {
|
|
103
|
-
$num = [int]$d.number
|
|
104
|
-
$comments = @()
|
|
105
|
-
$cp = 1
|
|
106
|
-
while ($cp -le 20) {
|
|
107
|
-
$ci = Get-Api "https://api.github.com/repos/$Repo/discussions/$num/comments?per_page=100&page=$cp"
|
|
108
|
-
if ($ci.Count -eq 0) { break }
|
|
109
|
-
$comments += $ci
|
|
110
|
-
if ($ci.Count -lt 100) { break }
|
|
111
|
-
$cp++
|
|
112
|
-
Start-Sleep -Milliseconds 300
|
|
113
|
-
}
|
|
114
|
-
$comments | ConvertTo-Json -Depth 6 | Set-Content -Path (Join-Path $OutDir "comments-$num.json") -Encoding UTF8
|
|
115
|
-
$n++
|
|
116
|
-
if ($n % 20 -eq 0) { Write-Output "comments: $n / $($sel.Count)" }
|
|
117
|
-
Start-Sleep -Milliseconds 200
|
|
118
|
-
}
|
|
119
|
-
Write-Output "comments: 完成 $n 个线程"
|
|
120
|
-
# 清掉列表里已不存在的旧 comments 文件
|
|
121
|
-
$keep = New-Object System.Collections.Generic.HashSet[string]
|
|
122
|
-
foreach ($d in $sel) { [void]$keep.Add("comments-$([int]$d.number).json") }
|
|
123
|
-
Get-ChildItem $OutDir -Filter 'comments-*.json' | Where-Object { -not $keep.Contains($_.Name) } | Remove-Item -Force
|
|
124
|
-
} else {
|
|
125
|
-
Write-Output 'comments: 跳过(-SkipComments)'
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
# ---- 3) _selection.tsv 与 README.md ----
|
|
129
|
-
$selLines = @('number' + "`t" + 'category' + "`t" + 'comments' + "`t" + 'title')
|
|
130
|
-
foreach ($d in $sel) { $selLines += ("{0}`t{1}`t{2}`t{3}" -f $d.number, $d.category.name, $d.comments, ($d.title -replace "`t",' ')) }
|
|
131
|
-
Set-Content -Path (Join-Path $OutDir '_selection.tsv') -Value $selLines -Encoding UTF8
|
|
132
|
-
|
|
133
|
-
$cats = @{}
|
|
134
|
-
foreach ($d in $discs) { $k = [string]$d.category.name; if (-not $cats.ContainsKey($k)) { $cats[$k] = 0 }; $cats[$k]++ }
|
|
135
|
-
$utc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
|
136
|
-
$L = @()
|
|
137
|
-
$L += '# 官方 GitHub Discussions 归档(自动生成,见抓取时间)'
|
|
138
|
-
$L += ''
|
|
139
|
-
$L += '> 来源:https://github.com/deepseek-ai/deepseek-harness/discussions (官方仓库 Issues 关闭,Discussions 为反馈主渠道)。'
|
|
140
|
-
$L += '> 抓取:GitHub REST API(list 含每条正文;comments-<n>.json 为精选线程的全部评论)。'
|
|
141
|
-
$L += "> 更新:提供 ``GH_TOKEN`` 后可由 ``scripts/archive-discussions.ps1`` 幂等刷新;抓取时间 $utc (UTC)。"
|
|
142
|
-
$L += ''
|
|
143
|
-
$L += '## 文件'
|
|
144
|
-
$L += ''
|
|
145
|
-
$L += "- list.json — 全部 $($discs.Count) 条讨论(含 title/body/category/answers/labels 等字段,正文即模型可读文档)。"
|
|
146
|
-
$L += "- comments-<n>.json — 精选 $($sel.Count) 条线程的评论(选择规则:Announcements/Q&A/Ideas 评论>=1;General/Show and tell 评论>=3 且非社群拉群帖)。"
|
|
147
|
-
$L += '- _selection.tsv — 精选线程清单(编号/分类/评论数/标题)。'
|
|
148
|
-
$L += ''
|
|
149
|
-
$L += '## 分类统计'
|
|
150
|
-
$L += ''
|
|
151
|
-
foreach ($k in @($cats.Keys | Sort-Object)) { $L += "- $k:$($cats[$k]) 条" }
|
|
152
|
-
$L += ''
|
|
153
|
-
$L += '## 有归档评论的精选线程'
|
|
154
|
-
$L += ''
|
|
155
|
-
$L += '| # | 分类 | 评论 | 标题 |'
|
|
156
|
-
$L += '|---|---|---|---|'
|
|
157
|
-
foreach ($d in $sel) {
|
|
158
|
-
$title = ($d.title -replace '\|','\|' -replace "[\r\n]",' ')
|
|
159
|
-
$L += "| [#$($d.number)](https://github.com/deepseek-ai/deepseek-harness/discussions/$($d.number)) | $($d.category.name) | $($d.comments) | $title |"
|
|
160
|
-
}
|
|
161
|
-
Set-Content -Path (Join-Path $OutDir 'README.md') -Value ($L -join "`r`n") -Encoding UTF8
|
|
162
|
-
Write-Output "DONE: list=$($discs.Count) selected=$($sel.Count) -> $OutDir"
|
|
1
|
+
# dsh-plugin-guide 官方 GitHub Discussions 归档脚本(可复跑,幂等)
|
|
2
|
+
# 输出: downloads/github/harness/discussions/
|
|
3
|
+
# list.json 全部讨论(title/body/category/answers/labels 等,正文即文档)
|
|
4
|
+
# comments-<n>.json 精选线程的全部评论(选择规则见下)
|
|
5
|
+
# _selection.tsv 精选线程清单
|
|
6
|
+
# README.md 分类统计 + 精选清单
|
|
7
|
+
# 说明: GitHub REST API;未提供 Token 时走匿名配额(60/h,可能不够),建议
|
|
8
|
+
# $env:GH_TOKEN 或 -Token <token>(经典 PAT,只需 public_repo 读权限)。
|
|
9
|
+
# 选择规则(与 08-14 首版覆盖度一致): Announcements 全部; Q&A/Ideas 评论>=1;
|
|
10
|
+
# General/Show and tell 评论>=3 且标题不含拉群/招聘/广告词。
|
|
11
|
+
# 传输用 Invoke-WebRequest(而非 curl.exe 数组参数): PS 5.1 下 curl 数组参数
|
|
12
|
+
# 的引号拆分会把 -H 头打碎,导致静默拿到错误对象——本脚本已规避该坑。
|
|
13
|
+
# 参数: -OutDir <dir> 覆盖输出目录; -SkipComments 只刷新 list.json 不拉评论。
|
|
14
|
+
param(
|
|
15
|
+
[string]$OutDir = '',
|
|
16
|
+
[string]$Repo = 'deepseek-ai/deepseek-harness',
|
|
17
|
+
[string]$Token = $env:GH_TOKEN,
|
|
18
|
+
[switch]$SkipComments
|
|
19
|
+
)
|
|
20
|
+
$ErrorActionPreference = 'Stop'
|
|
21
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
22
|
+
if (-not $OutDir) { $OutDir = Join-Path (Split-Path $PSScriptRoot -Parent) 'downloads\github\harness\discussions' }
|
|
23
|
+
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
|
24
|
+
|
|
25
|
+
$headers = @{ 'User-Agent' = 'dsh-plugin-guide-research'; 'Accept' = 'application/vnd.github+json' }
|
|
26
|
+
if ($Token) { $headers['Authorization'] = "Bearer $Token" }
|
|
27
|
+
|
|
28
|
+
function Get-Api([string]$url) {
|
|
29
|
+
# 返回对象数组;失败/限流时抛错(带状态码),由调用方决定重试或中止。
|
|
30
|
+
try {
|
|
31
|
+
$resp = Invoke-WebRequest -Uri $url -Headers $headers -TimeoutSec 60 -UseBasicParsing
|
|
32
|
+
} catch {
|
|
33
|
+
$code = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 }
|
|
34
|
+
throw "HTTP $code : $url"
|
|
35
|
+
}
|
|
36
|
+
$obj = $null
|
|
37
|
+
try { $obj = $resp.Content | ConvertFrom-Json } catch { throw "JSON parse failed: $url" }
|
|
38
|
+
if ($obj -is [System.Management.Automation.PSCustomObject] -and $obj.PSObject.Properties.Name -contains 'message') {
|
|
39
|
+
throw "API message '$($obj.message)' : $url"
|
|
40
|
+
}
|
|
41
|
+
return @($obj)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# ---- 1) 全量 list(按创建序分页;REST 的 direction 参数对 discussions 无效,只能从头翻到尾) ----
|
|
45
|
+
$prevCount = 0
|
|
46
|
+
if (Test-Path (Join-Path $OutDir 'list.json')) {
|
|
47
|
+
try { $prev = @((Get-Content (Join-Path $OutDir 'list.json') -Raw -Encoding UTF8 | ConvertFrom-Json)); $prevCount = $prev.Count } catch { $prevCount = 0 }
|
|
48
|
+
}
|
|
49
|
+
$all = @()
|
|
50
|
+
$page = 1
|
|
51
|
+
# REST /discussions caps at 5000 rows (50 pages x 100). The cap is explicit so a
|
|
52
|
+
# silent truncation cannot be mistaken for a complete archive: the run reports
|
|
53
|
+
# when it stops at the ceiling, and the README records the ceiling alongside the
|
|
54
|
+
# GraphQL total_count (which is larger).
|
|
55
|
+
$pageCap = 50
|
|
56
|
+
while ($page -le $pageCap) {
|
|
57
|
+
$items = Get-Api "https://api.github.com/repos/$Repo/discussions?per_page=100&page=$page&sort=created"
|
|
58
|
+
if ($items.Count -eq 0) { Write-Output "page ${page}: 空页,列表结束"; break }
|
|
59
|
+
$all += $items
|
|
60
|
+
Write-Output ("page {0}: +{1} (累计 {2})" -f $page, $items.Count, $all.Count)
|
|
61
|
+
if ($items.Count -lt 100) { break }
|
|
62
|
+
$page++
|
|
63
|
+
Start-Sleep -Milliseconds 500
|
|
64
|
+
}
|
|
65
|
+
if ($all.Count -ge ($pageCap * 100)) {
|
|
66
|
+
Write-Output "WARN: 列表在第 $pageCap 页触顶(REST 5000 条上限)——归档为前 5000 条,非全量;更全的历史需按 GraphQL UPDATED_AT 分页补抓"
|
|
67
|
+
}
|
|
68
|
+
if ($all.Count -eq 0) { throw 'discussions 列表抓取失败(0 条)' }
|
|
69
|
+
if ($prevCount -gt 0 -and $all.Count -lt [Math]::Floor($prevCount * 0.9)) {
|
|
70
|
+
throw "列表疑似缩水: 上次 $prevCount 条, 本次仅 $all.Count 条 —— 拒绝覆盖归档,请检查网络/配额后重跑"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# 按 number 去重(分页边界可能重复)
|
|
74
|
+
$uniq = @{}
|
|
75
|
+
foreach ($d in $all) { $uniq[[int]$d.number] = $d }
|
|
76
|
+
$discs = @($uniq.Values | Sort-Object { [int]$_.number })
|
|
77
|
+
$listPath = Join-Path $OutDir 'list.json'
|
|
78
|
+
$discs | ConvertTo-Json -Depth 6 | Set-Content -Path $listPath -Encoding UTF8
|
|
79
|
+
Write-Output "list.json: $($discs.Count) 条 -> $listPath"
|
|
80
|
+
|
|
81
|
+
# ---- 2) 精选线程 + 评论 ----
|
|
82
|
+
$sel = @()
|
|
83
|
+
foreach ($d in $discs) {
|
|
84
|
+
$cat = $d.category.name
|
|
85
|
+
$c = [int]$d.comments
|
|
86
|
+
$t = [string]$d.title
|
|
87
|
+
$pick = $false
|
|
88
|
+
if ($cat -eq 'Announcements') { $pick = $true }
|
|
89
|
+
elseif ($cat -eq 'Q&A' -and $c -ge 1) { $pick = $true }
|
|
90
|
+
elseif ($cat -eq 'Ideas' -and $c -ge 1) { $pick = $true }
|
|
91
|
+
elseif (($cat -eq 'General' -or $cat -eq 'Show and tell') -and $c -ge 3) {
|
|
92
|
+
# 非社群拉群帖: 标题含拉群/扫码/加群/招聘/广告类词的不收
|
|
93
|
+
if ($t -notmatch '群|扫码|加群|招聘|内推|广告|投简历|纳新') { $pick = $true }
|
|
94
|
+
}
|
|
95
|
+
if ($pick) { $sel += $d }
|
|
96
|
+
}
|
|
97
|
+
$sel = @($sel | Sort-Object { [int]$_.number })
|
|
98
|
+
Write-Output "精选线程: $($sel.Count) 条"
|
|
99
|
+
|
|
100
|
+
if (-not $SkipComments) {
|
|
101
|
+
$n = 0
|
|
102
|
+
foreach ($d in $sel) {
|
|
103
|
+
$num = [int]$d.number
|
|
104
|
+
$comments = @()
|
|
105
|
+
$cp = 1
|
|
106
|
+
while ($cp -le 20) {
|
|
107
|
+
$ci = Get-Api "https://api.github.com/repos/$Repo/discussions/$num/comments?per_page=100&page=$cp"
|
|
108
|
+
if ($ci.Count -eq 0) { break }
|
|
109
|
+
$comments += $ci
|
|
110
|
+
if ($ci.Count -lt 100) { break }
|
|
111
|
+
$cp++
|
|
112
|
+
Start-Sleep -Milliseconds 300
|
|
113
|
+
}
|
|
114
|
+
$comments | ConvertTo-Json -Depth 6 | Set-Content -Path (Join-Path $OutDir "comments-$num.json") -Encoding UTF8
|
|
115
|
+
$n++
|
|
116
|
+
if ($n % 20 -eq 0) { Write-Output "comments: $n / $($sel.Count)" }
|
|
117
|
+
Start-Sleep -Milliseconds 200
|
|
118
|
+
}
|
|
119
|
+
Write-Output "comments: 完成 $n 个线程"
|
|
120
|
+
# 清掉列表里已不存在的旧 comments 文件
|
|
121
|
+
$keep = New-Object System.Collections.Generic.HashSet[string]
|
|
122
|
+
foreach ($d in $sel) { [void]$keep.Add("comments-$([int]$d.number).json") }
|
|
123
|
+
Get-ChildItem $OutDir -Filter 'comments-*.json' | Where-Object { -not $keep.Contains($_.Name) } | Remove-Item -Force
|
|
124
|
+
} else {
|
|
125
|
+
Write-Output 'comments: 跳过(-SkipComments)'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# ---- 3) _selection.tsv 与 README.md ----
|
|
129
|
+
$selLines = @('number' + "`t" + 'category' + "`t" + 'comments' + "`t" + 'title')
|
|
130
|
+
foreach ($d in $sel) { $selLines += ("{0}`t{1}`t{2}`t{3}" -f $d.number, $d.category.name, $d.comments, ($d.title -replace "`t",' ')) }
|
|
131
|
+
Set-Content -Path (Join-Path $OutDir '_selection.tsv') -Value $selLines -Encoding UTF8
|
|
132
|
+
|
|
133
|
+
$cats = @{}
|
|
134
|
+
foreach ($d in $discs) { $k = [string]$d.category.name; if (-not $cats.ContainsKey($k)) { $cats[$k] = 0 }; $cats[$k]++ }
|
|
135
|
+
$utc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
|
136
|
+
$L = @()
|
|
137
|
+
$L += '# 官方 GitHub Discussions 归档(自动生成,见抓取时间)'
|
|
138
|
+
$L += ''
|
|
139
|
+
$L += '> 来源:https://github.com/deepseek-ai/deepseek-harness/discussions (官方仓库 Issues 关闭,Discussions 为反馈主渠道)。'
|
|
140
|
+
$L += '> 抓取:GitHub REST API(list 含每条正文;comments-<n>.json 为精选线程的全部评论)。'
|
|
141
|
+
$L += "> 更新:提供 ``GH_TOKEN`` 后可由 ``scripts/archive-discussions.ps1`` 幂等刷新;抓取时间 $utc (UTC)。"
|
|
142
|
+
$L += ''
|
|
143
|
+
$L += '## 文件'
|
|
144
|
+
$L += ''
|
|
145
|
+
$L += "- list.json — 全部 $($discs.Count) 条讨论(含 title/body/category/answers/labels 等字段,正文即模型可读文档)。"
|
|
146
|
+
$L += "- comments-<n>.json — 精选 $($sel.Count) 条线程的评论(选择规则:Announcements/Q&A/Ideas 评论>=1;General/Show and tell 评论>=3 且非社群拉群帖)。"
|
|
147
|
+
$L += '- _selection.tsv — 精选线程清单(编号/分类/评论数/标题)。'
|
|
148
|
+
$L += ''
|
|
149
|
+
$L += '## 分类统计'
|
|
150
|
+
$L += ''
|
|
151
|
+
foreach ($k in @($cats.Keys | Sort-Object)) { $L += "- $k:$($cats[$k]) 条" }
|
|
152
|
+
$L += ''
|
|
153
|
+
$L += '## 有归档评论的精选线程'
|
|
154
|
+
$L += ''
|
|
155
|
+
$L += '| # | 分类 | 评论 | 标题 |'
|
|
156
|
+
$L += '|---|---|---|---|'
|
|
157
|
+
foreach ($d in $sel) {
|
|
158
|
+
$title = ($d.title -replace '\|','\|' -replace "[\r\n]",' ')
|
|
159
|
+
$L += "| [#$($d.number)](https://github.com/deepseek-ai/deepseek-harness/discussions/$($d.number)) | $($d.category.name) | $($d.comments) | $title |"
|
|
160
|
+
}
|
|
161
|
+
Set-Content -Path (Join-Path $OutDir 'README.md') -Value ($L -join "`r`n") -Encoding UTF8
|
|
162
|
+
Write-Output "DONE: list=$($discs.Count) selected=$($sel.Count) -> $OutDir"
|
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
# dsh-plugin-guide official-docs freshness probe.
|
|
2
|
-
# Purpose: detect drift between the KB mirror (references/official-docs) and the
|
|
3
|
-
# upstream deepseek-harness repository WITHOUT a local checkout. It compares the
|
|
4
|
-
# upstream branch tip SHA (git ls-remote) against the pinned "Source commit" SHA
|
|
5
|
-
# recorded in references/official-docs/SNAPSHOT.md — the SHA-locked authority the
|
|
6
|
-
# mirror was synced from. It never edits the mirror; drift is only reported.
|
|
7
|
-
# Usage: pwsh -File scripts/check-docs-drift.ps1 [-Repo <url>] [-Branch <name>] [-Snapshot <path>] [-FailOnDrift]
|
|
8
|
-
# Exit codes: 0 = FRESH, 1 = DRIFT (with -FailOnDrift), 2 = probe could not run (snapshot/parse/fetch failure).
|
|
9
|
-
# In CI it also writes `drift=yes|no|error` to $env:GITHUB_OUTPUT when that variable is set.
|
|
10
|
-
param(
|
|
11
|
-
[string]$Repo = 'https://github.com/deepseek-ai/deepseek-harness.git',
|
|
12
|
-
[string]$Branch = 'master',
|
|
13
|
-
[string]$Snapshot = 'references/official-docs/SNAPSHOT.md',
|
|
14
|
-
[switch]$FailOnDrift
|
|
15
|
-
)
|
|
16
|
-
$ErrorActionPreference = 'Stop'
|
|
17
|
-
$kit = Split-Path $PSScriptRoot -Parent
|
|
18
|
-
$snapshotPath = Join-Path $kit $Snapshot
|
|
19
|
-
|
|
20
|
-
# ---- 1) Read the pinned source commit SHA from SNAPSHOT.md ----
|
|
21
|
-
$pinned = $null
|
|
22
|
-
if (-not (Test-Path $snapshotPath)) { Write-Error "snapshot not found: $snapshotPath"; exit 2 }
|
|
23
|
-
foreach ($line in Get-Content $snapshotPath -Encoding UTF8) {
|
|
24
|
-
if ($line -match '^\|\s*Source commit\s*\|\s*`([0-9a-f]{40})`\s*\|') {
|
|
25
|
-
$pinned = $Matches[1]
|
|
26
|
-
break
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (-not $pinned) { Write-Error "cannot parse the pinned 'Source commit' SHA from $Snapshot"; exit 2 }
|
|
30
|
-
|
|
31
|
-
# ---- 2) Fetch the upstream branch tip SHA (no clone required) ----
|
|
32
|
-
$remoteLine = (& git ls-remote $Repo "refs/heads/$Branch" 2>$null | Select-Object -First 1)
|
|
33
|
-
if (-not $remoteLine -or $remoteLine -notmatch '^([0-9a-f]{40})\s') {
|
|
34
|
-
$message = "cannot reach upstream $Repo (branch $Branch)"
|
|
35
|
-
Write-Output "ERROR: $message"
|
|
36
|
-
if ($env:GITHUB_OUTPUT) { Add-Content -Path $env:GITHUB_OUTPUT -Value 'drift=error' }
|
|
37
|
-
exit 2
|
|
38
|
-
}
|
|
39
|
-
$upstream = $Matches[1]
|
|
40
|
-
|
|
41
|
-
# ---- 3) Compare and report ----
|
|
42
|
-
$fresh = ($upstream -eq $pinned)
|
|
43
|
-
Write-Output "pinned (SNAPSHOT.md): $pinned"
|
|
44
|
-
Write-Output "upstream ($Branch): $upstream"
|
|
45
|
-
if ($fresh) {
|
|
46
|
-
Write-Output 'FRESH: references/official-docs matches the upstream branch tip'
|
|
47
|
-
} else {
|
|
48
|
-
Write-Output "DRIFT: references/official-docs is pinned to $pinned but upstream is at $upstream"
|
|
49
|
-
Write-Output 'Action: run pwsh -File scripts/sync-official-docs.ps1 to refresh the mirror (do not hand-edit the mirror).'
|
|
50
|
-
}
|
|
51
|
-
if ($env:GITHUB_OUTPUT) {
|
|
52
|
-
Add-Content -Path $env:GITHUB_OUTPUT -Value ($(if ($fresh) { 'drift=no' } else { 'drift=yes' }))
|
|
53
|
-
}
|
|
54
|
-
if (-not $fresh -and $FailOnDrift) { exit 1 }
|
|
55
|
-
exit 0
|
|
1
|
+
# dsh-plugin-guide official-docs freshness probe.
|
|
2
|
+
# Purpose: detect drift between the KB mirror (references/official-docs) and the
|
|
3
|
+
# upstream deepseek-harness repository WITHOUT a local checkout. It compares the
|
|
4
|
+
# upstream branch tip SHA (git ls-remote) against the pinned "Source commit" SHA
|
|
5
|
+
# recorded in references/official-docs/SNAPSHOT.md — the SHA-locked authority the
|
|
6
|
+
# mirror was synced from. It never edits the mirror; drift is only reported.
|
|
7
|
+
# Usage: pwsh -File scripts/check-docs-drift.ps1 [-Repo <url>] [-Branch <name>] [-Snapshot <path>] [-FailOnDrift]
|
|
8
|
+
# Exit codes: 0 = FRESH, 1 = DRIFT (with -FailOnDrift), 2 = probe could not run (snapshot/parse/fetch failure).
|
|
9
|
+
# In CI it also writes `drift=yes|no|error` to $env:GITHUB_OUTPUT when that variable is set.
|
|
10
|
+
param(
|
|
11
|
+
[string]$Repo = 'https://github.com/deepseek-ai/deepseek-harness.git',
|
|
12
|
+
[string]$Branch = 'master',
|
|
13
|
+
[string]$Snapshot = 'references/official-docs/SNAPSHOT.md',
|
|
14
|
+
[switch]$FailOnDrift
|
|
15
|
+
)
|
|
16
|
+
$ErrorActionPreference = 'Stop'
|
|
17
|
+
$kit = Split-Path $PSScriptRoot -Parent
|
|
18
|
+
$snapshotPath = Join-Path $kit $Snapshot
|
|
19
|
+
|
|
20
|
+
# ---- 1) Read the pinned source commit SHA from SNAPSHOT.md ----
|
|
21
|
+
$pinned = $null
|
|
22
|
+
if (-not (Test-Path $snapshotPath)) { Write-Error "snapshot not found: $snapshotPath"; exit 2 }
|
|
23
|
+
foreach ($line in Get-Content $snapshotPath -Encoding UTF8) {
|
|
24
|
+
if ($line -match '^\|\s*Source commit\s*\|\s*`([0-9a-f]{40})`\s*\|') {
|
|
25
|
+
$pinned = $Matches[1]
|
|
26
|
+
break
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (-not $pinned) { Write-Error "cannot parse the pinned 'Source commit' SHA from $Snapshot"; exit 2 }
|
|
30
|
+
|
|
31
|
+
# ---- 2) Fetch the upstream branch tip SHA (no clone required) ----
|
|
32
|
+
$remoteLine = (& git ls-remote $Repo "refs/heads/$Branch" 2>$null | Select-Object -First 1)
|
|
33
|
+
if (-not $remoteLine -or $remoteLine -notmatch '^([0-9a-f]{40})\s') {
|
|
34
|
+
$message = "cannot reach upstream $Repo (branch $Branch)"
|
|
35
|
+
Write-Output "ERROR: $message"
|
|
36
|
+
if ($env:GITHUB_OUTPUT) { Add-Content -Path $env:GITHUB_OUTPUT -Value 'drift=error' }
|
|
37
|
+
exit 2
|
|
38
|
+
}
|
|
39
|
+
$upstream = $Matches[1]
|
|
40
|
+
|
|
41
|
+
# ---- 3) Compare and report ----
|
|
42
|
+
$fresh = ($upstream -eq $pinned)
|
|
43
|
+
Write-Output "pinned (SNAPSHOT.md): $pinned"
|
|
44
|
+
Write-Output "upstream ($Branch): $upstream"
|
|
45
|
+
if ($fresh) {
|
|
46
|
+
Write-Output 'FRESH: references/official-docs matches the upstream branch tip'
|
|
47
|
+
} else {
|
|
48
|
+
Write-Output "DRIFT: references/official-docs is pinned to $pinned but upstream is at $upstream"
|
|
49
|
+
Write-Output 'Action: run pwsh -File scripts/sync-official-docs.ps1 to refresh the mirror (do not hand-edit the mirror).'
|
|
50
|
+
}
|
|
51
|
+
if ($env:GITHUB_OUTPUT) {
|
|
52
|
+
Add-Content -Path $env:GITHUB_OUTPUT -Value ($(if ($fresh) { 'drift=no' } else { 'drift=yes' }))
|
|
53
|
+
}
|
|
54
|
+
if (-not $fresh -and $FailOnDrift) { exit 1 }
|
|
55
|
+
exit 0
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Assert every PowerShell script under scripts/ keeps its UTF-8 BOM.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: PowerShell 5.1 (Windows PowerShell - the only shell on a
|
|
4
|
+
// machine without pwsh) decodes a BOM-less script as ANSI/CP936. `verify-kit.ps1`
|
|
5
|
+
// carries ~1.6 KB of Chinese comments and messages, so the mis-decode corrupts
|
|
6
|
+
// the source and the parser fails with "Missing ')' in method call". That is the
|
|
7
|
+
// repository's own recorded convention (`references/sources.md`), and on
|
|
8
|
+
// 2026-10-13 `verify-kit.ps1` was found to be the only one of nine scripts
|
|
9
|
+
// missing its BOM - and therefore unusable on PowerShell 5.1, which was
|
|
10
|
+
// invisible to CI because CI runs it under `pwsh`, where a missing BOM is
|
|
11
|
+
// harmless.
|
|
12
|
+
//
|
|
13
|
+
// Deliberately implemented in Node, not PowerShell: it must run on the Linux CI
|
|
14
|
+
// runner and on a machine whose only PowerShell is the 5.1 that cannot read the
|
|
15
|
+
// file in the first place.
|
|
16
|
+
|
|
17
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
|
|
20
|
+
const dir = join(process.cwd(), 'scripts')
|
|
21
|
+
const BOM = [0xef, 0xbb, 0xbf]
|
|
22
|
+
|
|
23
|
+
/** True when the buffer starts with a UTF-8 BOM. */
|
|
24
|
+
function hasBom(buf) {
|
|
25
|
+
return buf.length >= 3 && buf[0] === BOM[0] && buf[1] === BOM[1] && buf[2] === BOM[2]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Count bytes above ASCII, i.e. how much a mis-decode could corrupt. */
|
|
29
|
+
function nonAsciiBytes(buf) {
|
|
30
|
+
let n = 0
|
|
31
|
+
for (const b of buf) if (b > 127) n++
|
|
32
|
+
return n
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const scripts = readdirSync(dir)
|
|
36
|
+
.filter((f) => f.toLowerCase().endsWith('.ps1'))
|
|
37
|
+
.sort()
|
|
38
|
+
|
|
39
|
+
const problems = []
|
|
40
|
+
const rows = []
|
|
41
|
+
|
|
42
|
+
for (const file of scripts) {
|
|
43
|
+
const buf = readFileSync(join(dir, file))
|
|
44
|
+
const bom = hasBom(buf)
|
|
45
|
+
const nonAscii = nonAsciiBytes(buf)
|
|
46
|
+
rows.push({ file, bom, nonAscii, bytes: buf.length })
|
|
47
|
+
// A BOM matters most when the file contains non-ASCII text; for a pure-ASCII
|
|
48
|
+
// script a missing BOM is harmless, so report it without failing.
|
|
49
|
+
if (!bom && nonAscii > 0) {
|
|
50
|
+
problems.push(
|
|
51
|
+
`${file}: ${nonAscii} non-ASCII bytes but no UTF-8 BOM - PowerShell 5.1 will mis-decode this script`,
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const r of rows) {
|
|
57
|
+
const flag = !r.bom && r.nonAscii > 0 ? 'FAIL' : r.bom ? 'ok ' : 'warn'
|
|
58
|
+
console.log(`${flag} ${r.file.padEnd(34)} bom=${String(r.bom).padEnd(5)} nonAscii=${String(r.nonAscii).padEnd(5)} bytes=${r.bytes}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (problems.length > 0) {
|
|
62
|
+
console.error('\nps1-bom: FAIL')
|
|
63
|
+
for (const p of problems) console.error(' ' + p)
|
|
64
|
+
console.error('\nFix: prepend EF BB BF to the file (keep the content byte-identical otherwise).')
|
|
65
|
+
process.exit(1)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(`\nps1-bom OK (${scripts.length} scripts; every non-ASCII one carries a BOM)`)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Content-parity gate for the five READMEs and the five quick references.
|
|
2
|
+
//
|
|
3
|
+
// `check-readme-sync.mjs` only compares `## ` headings, so four translation
|
|
4
|
+
// families drifted past it unnoticed: an audit on 2026-10-13 found the es/pt/hi
|
|
5
|
+
// READMEs missing whole entries of "What you get" (including the flagship
|
|
6
|
+
// version-adaptivity document), the quick references still claiming "215 pages"
|
|
7
|
+
// against an actual 429, and a "re-verified on 2026-10-06" date attached to a
|
|
8
|
+
// commit dated 2026-10-09.
|
|
9
|
+
//
|
|
10
|
+
// This gate compares STRUCTURE at the block level — the number of list items and
|
|
11
|
+
// table rows inside each H2 section — instead of only the H2 titles. It cannot
|
|
12
|
+
// judge translation quality; it catches the case that actually happens, a
|
|
13
|
+
// section that gained or lost items in one language only.
|
|
14
|
+
//
|
|
15
|
+
// Usage: node scripts/check-readme-parity.mjs [--json] [--max-drift N]
|
|
16
|
+
|
|
17
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
|
|
20
|
+
const root = process.cwd()
|
|
21
|
+
const json = process.argv.includes('--json')
|
|
22
|
+
const maxArg = process.argv.indexOf('--max-drift')
|
|
23
|
+
const maxDrift = maxArg >= 0 ? Number(process.argv[maxArg + 1] ?? 0) : 0
|
|
24
|
+
|
|
25
|
+
/** The English source and its translations, for each family. */
|
|
26
|
+
const FAMILIES = [
|
|
27
|
+
{
|
|
28
|
+
name: 'README',
|
|
29
|
+
base: 'README.md',
|
|
30
|
+
variants: ['README-zh.md', 'README-es.md', 'README-pt.md', 'README-hi.md'],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'quick-reference',
|
|
34
|
+
base: 'guide/quick-reference.md',
|
|
35
|
+
variants: [
|
|
36
|
+
'guide/quick-reference.zh-CN.md',
|
|
37
|
+
'guide/quick-reference.es.md',
|
|
38
|
+
'guide/quick-reference.pt.md',
|
|
39
|
+
'guide/quick-reference.hi.md',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Split a markdown document into H2 sections and count countable blocks.
|
|
46
|
+
*
|
|
47
|
+
* Sections are keyed by ORDINAL, not by title: the quick references legitimately
|
|
48
|
+
* translate their headings, so matching on English titles produced 26 false
|
|
49
|
+
* "missing section" reports per language. What must stay identical across a
|
|
50
|
+
* translation family is the SEQUENCE and the SIZE of the sections, because the
|
|
51
|
+
* failure mode this gate exists for is a section that gained or lost content in
|
|
52
|
+
* one language only.
|
|
53
|
+
*
|
|
54
|
+
* @param text - raw markdown.
|
|
55
|
+
* @returns ordered sections with their countable blocks.
|
|
56
|
+
*/
|
|
57
|
+
function sectionProfile(text) {
|
|
58
|
+
const lines = text.split(/\r?\n/)
|
|
59
|
+
const out = []
|
|
60
|
+
let current = { index: 0, title: '(preamble)', bullets: 0, tableRows: 0, codeBlocks: 0 }
|
|
61
|
+
out.push(current)
|
|
62
|
+
let inFence = false
|
|
63
|
+
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
if (/^\s*```/.test(line)) {
|
|
66
|
+
inFence = !inFence
|
|
67
|
+
if (inFence) current.codeBlocks++
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
if (inFence) continue
|
|
71
|
+
const h2 = /^##\s+(.*)$/.exec(line)
|
|
72
|
+
if (h2) {
|
|
73
|
+
current = { index: out.length, title: h2[1].trim(), bullets: 0, tableRows: 0, codeBlocks: 0 }
|
|
74
|
+
out.push(current)
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if (/^\s*[-*]\s+\S/.test(line)) current.bullets++
|
|
78
|
+
// A table row, excluding the separator row.
|
|
79
|
+
if (/^\s*\|/.test(line) && !/^\s*[\s:|-]+\|\s*$/.test(line)) current.tableRows++
|
|
80
|
+
}
|
|
81
|
+
return out
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Compare one variant against the English source, by section ordinal. */
|
|
85
|
+
function compare(basePath, variantPath) {
|
|
86
|
+
const base = sectionProfile(readFileSync(join(root, basePath), 'utf8'))
|
|
87
|
+
const variant = sectionProfile(readFileSync(join(root, variantPath), 'utf8'))
|
|
88
|
+
const drift = []
|
|
89
|
+
|
|
90
|
+
if (base.length !== variant.length) {
|
|
91
|
+
drift.push({
|
|
92
|
+
kind: 'section-count',
|
|
93
|
+
detail: `${base.length} sections in base vs ${variant.length} in variant`,
|
|
94
|
+
base: null,
|
|
95
|
+
variant: null,
|
|
96
|
+
section: null,
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const n = Math.min(base.length, variant.length)
|
|
101
|
+
for (let i = 0; i < n; i++) {
|
|
102
|
+
for (const key of ['bullets', 'tableRows', 'codeBlocks']) {
|
|
103
|
+
if (base[i][key] !== variant[i][key]) {
|
|
104
|
+
drift.push({
|
|
105
|
+
section: `#${i} ${base[i].title}`,
|
|
106
|
+
kind: key,
|
|
107
|
+
base: base[i][key],
|
|
108
|
+
variant: variant[i][key],
|
|
109
|
+
detail: `base="${base[i].title}" variant="${variant[i].title}"`,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return drift
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let total = 0
|
|
118
|
+
const report = []
|
|
119
|
+
for (const family of FAMILIES) {
|
|
120
|
+
if (!existsSync(join(root, family.base))) continue
|
|
121
|
+
for (const v of family.variants) {
|
|
122
|
+
if (!existsSync(join(root, v))) continue
|
|
123
|
+
const drift = compare(family.base, v)
|
|
124
|
+
total += drift.length
|
|
125
|
+
report.push({ family: family.name, variant: v, drift })
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (json) {
|
|
130
|
+
console.log(JSON.stringify({ total, report }, null, 2))
|
|
131
|
+
} else {
|
|
132
|
+
for (const r of report) {
|
|
133
|
+
if (r.drift.length === 0) {
|
|
134
|
+
console.log(`ok ${r.family.padEnd(16)} ${r.variant}`)
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
console.log(`DRIFT ${r.family.padEnd(15)} ${r.variant} (${r.drift.length} block(s))`)
|
|
138
|
+
for (const d of r.drift.slice(0, 12)) {
|
|
139
|
+
if (d.kind === 'section-count') console.log(` ${d.detail}`)
|
|
140
|
+
else if (d.section === null) console.log(` ${d.kind}: ${d.detail ?? ''}`)
|
|
141
|
+
else console.log(` ${d.kind.padEnd(11)} ${d.base} vs ${d.variant} in "${d.section}"`)
|
|
142
|
+
}
|
|
143
|
+
if (r.drift.length > 12) console.log(` ... and ${r.drift.length - 12} more`)
|
|
144
|
+
}
|
|
145
|
+
console.log(`\nblock-level drift entries: ${total}`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The gate is opt-in strict: run with --max-drift N to allow a known budget.
|
|
149
|
+
process.exit(total > maxDrift ? 1 : 0)
|