dsh-plugin-guide 0.3.29 → 0.3.30

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.
@@ -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)`)