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.
@@ -1,194 +1,197 @@
1
- # dsh-plugin-guide 完整性检查器
2
- # 用法: pwsh -File scripts/verify-kit.ps1 -Root <路径>
3
- # 校验范围: 本知识库自有文档(SKILL/README/NOTICE/guide/references 顶层);
4
- # 官方文档副本(references/official-docs)是逐字副本,其内部链接指向 deepseek-harness checkout,不在校验范围。
5
- param([string]$Root = '', [string]$Checkout = '', [switch]$Strict)
6
- if ([string]::IsNullOrEmpty($Root)) { $Root = if ($PSScriptRoot) { Split-Path $PSScriptRoot -Parent } else { (Get-Location).Path } }
7
- $ErrorActionPreference = 'SilentlyContinue'
8
- $issues = New-Object System.Collections.Generic.List[string]
9
- $info = New-Object System.Collections.Generic.List[string]
10
-
11
- # ---- 1) 关键路径 ----
12
- $critical = @(
13
- 'SKILL.md','README.md','README-zh.md','README-es.md','README-pt.md','README-hi.md','LICENSE','NOTICE.md','.gitignore',
14
- 'CONTRIBUTING.md','SECURITY.md','.github/ISSUE_TEMPLATE/bug_report.yml','.github/ISSUE_TEMPLATE/feature_request.yml','.github/PULL_REQUEST_TEMPLATE.md',
15
- 'guide/plugin-dev-guide.md','guide/quick-reference.md','guide/links.md','guide/quick-reference.zh-CN.md','guide/quick-reference.es.md','guide/quick-reference.pt.md','guide/quick-reference.hi.md',
16
- # 2026-10-13: 补齐 gated 文件——这些曾被漏掉,导致陈旧内容能存活到发布
17
- 'guide/version-matrix.md','guide/family-toolchain.md','guide/migration-0.2.md','guide/unfixed-issues.md',
18
- 'guide/release-engineering.md','guide/release-engineering.zh-CN.md',
19
- 'guide/when-to-retire-a-plugin.md','guide/when-to-retire-a-plugin.zh-CN.md',
20
- 'guide/choosing-a-plugin.md','guide/choosing-a-plugin.zh-CN.md',
21
- 'references/sources.md','references/harness-repo.md','references/website-pages.md',
22
- 'references/official-plugin-dev-skill.md','references/dev-resource-landscape.md',
23
- 'src/cli/skill-sections.ts','src/cli/templates.ts',
24
- 'references/upstream-cordis.md','references/cordis-paper-and-community.md',
25
- 'references/community-ecosystem.md','references/community-repo-deep-dive.md',
26
- 'references/official-docs/docs/cordis-primer.md',
27
- 'references/official-docs/docs/cordis-tutorial/index.md',
28
- 'references/official-docs/docs/cordis-tutorial/01-first-plugin.md',
29
- 'references/official-docs/docs/cordis-tutorial/02-lifecycle-and-effects.md',
30
- 'references/official-docs/docs/cordis-tutorial/03-services.md',
31
- 'references/official-docs/docs/cordis-tutorial/04-events.md',
32
- 'references/official-docs/docs/cordis-tutorial/05-config.md',
33
- 'references/official-docs/docs/cordis-tutorial/06-composition-and-hmr.md',
34
- 'references/official-docs/docs/cordis-tutorial/07-into-the-harness.md',
35
- 'references/official-docs/docs/architecture.md',
36
- 'references/official-docs/docs/cookbook/extension-cookbook.md',
37
- 'references/official-docs/docs/cookbook/adding-a-tool.md',
38
- 'references/official-docs/docs/event-producer-consumer.md',
39
- 'references/official-docs/docs/user/develop/basic/index.md',
40
- 'references/official-docs/docs/user/develop/basic/tool.md',
41
- 'references/official-docs/docs/user/develop/basic/config.md',
42
- 'references/official-docs/docs/user/develop/basic/publish.md',
43
- 'references/official-docs/docs/user/develop/framework/service.md',
44
- 'references/official-docs/docs/user/develop/framework/events.md',
45
- 'references/official-docs/docs/user/develop/practice/index.md',
46
- 'references/official-docs/docs/user/develop/practice/llm-adapter.md',
47
- 'references/official-docs/docs/subsystems/session.md',
48
- 'references/official-docs/docs/subsystems/tools.md',
49
- 'references/official-docs/docs/cordis-api/context.md',
50
- 'references/official-docs/AGENTS.md',
51
- 'references/official-docs/SNAPSHOT.md',
52
- 'references/official-docs/packages/AGENTS.md',
53
- 'references/official-docs/packages/README.md',
54
- 'references/official-docs/vendor/README.md',
55
- 'scripts/download-sources.ps1','scripts/download-community-repos.ps1','scripts/gen-topic-snapshot.ps1','scripts/sync-official-docs.ps1','scripts/install-skill.ps1','scripts/verify-kit.ps1','scripts/archive-discussions.ps1','scripts/download-community-articles.ps1'
56
- )
57
- foreach ($c in $critical) { if (-not (Test-Path (Join-Path $Root $c))) { $issues.Add("CRITICAL-MISSING: $c") } }
58
- $info.Add("critical: $($critical.Count) 项, 缺失 $((($issues | Where-Object { $_ -like 'CRITICAL-*' }) | Measure-Object).Count)")
59
-
60
- # ---- 1b) 归档内容断言(防止同名文件错位: 历史事故是 examples/AGENTS.md 被当成仓库根 AGENTS.md) ----
61
- function Assert-FirstLine([string]$rel, [string]$pattern, [string]$expect) {
62
- $p = Join-Path $Root $rel
63
- if (-not (Test-Path $p)) { return }
64
- $first = Get-Content $p -Encoding UTF8 | Where-Object { $_.Trim() -ne '' } | Select-Object -First 1
65
- if ($first -notlike $pattern) { $issues.Add("CONTENT-ASSERT-FAIL: $rel 首行应为 $expect, 实际: $first") }
66
- }
67
- Assert-FirstLine 'references/official-docs/AGENTS.md' '# AGENTS.md' "仓库根 AGENTS.md 标题"
68
- Assert-FirstLine 'references/official-docs/packages/AGENTS.md' '# AGENTS.md*Harness Packages*' "packages/AGENTS.md 标题"
69
-
70
- # ---- 2) 自有文档相对链接解析 ----
71
- # 只检查 markdown 链接目标(读者会点击的);代码块/行内码里的裸词不参与,
72
- # 避免把 name、ctx、- 等片段误判为路径。链接按"相对所在文件目录"解析;
73
- # downloads/ 引用允许,指向 checkout 的逃逸链接(../../、../packages/ 等)跳过。
74
- $scopeFiles = @((Join-Path $Root 'SKILL.md'), (Join-Path $Root 'README.md'), (Join-Path $Root 'NOTICE.md'))
75
- $scopeFiles += Get-ChildItem (Join-Path $Root 'guide') -Filter *.md -File
76
- $scopeFiles += Get-ChildItem (Join-Path $Root 'references') -Filter *.md -File # 仅顶层,不含 official-docs 子树
77
- $missing = 0; $downloadsRefs = 0; $repoRelRefs = 0; $absRefs = 0
78
-
79
- function Is-PathToken([string]$t) {
80
- # 纯 ASCII 相对路径:含斜杠(guide/links.md、../references/x.md)或带扩展名(README.md);
81
- # 裸词、含空格/CJK 的引文片段不参与检查
82
- return $t -match '^(\.\./)?(\./)?(([A-Za-z0-9][A-Za-z0-9_.\-]*/)+[A-Za-z0-9_.\-]+(\.[A-Za-z0-9]+)?|[A-Za-z0-9][A-Za-z0-9_.\-]+\.[A-Za-z0-9]+)(#[A-Za-z0-9_\-/]+)?$'
83
- }
84
- function Test-FileRel([string]$p, [string]$dir) {
85
- $p = $p -replace '^\./',''
86
- $p = $p -replace '[?#].*$',''
87
- if ($p -match '[*]') { $d = Split-Path (Join-Path $dir $p) -Parent; return Test-Path $d }
88
- return Test-Path (Join-Path $dir $p)
89
- }
90
-
91
- foreach ($f in $scopeFiles) {
92
- if (-not (Test-Path $f.FullName)) { continue }
93
- $relFile = $f.FullName.Substring($Root.Length + 1)
94
- $fileDir = Split-Path $f.FullName -Parent
95
- $text = Get-Content $f.FullName -Raw -Encoding UTF8
96
- $tokens = New-Object System.Collections.Generic.HashSet[string]
97
- foreach ($m in [regex]::Matches($text, '\[[^\]]*\]\(([^)\s]+)\)')) {
98
- $t = $m.Groups[1].Value.Trim()
99
- if ($t -match '^https?://' -or $t -match '^[A-Za-z]:[\\/]' -or $t -match '^(<|\$|#)') { continue }
100
- if ($t -match '^[A-Za-z]:[\\/]') { $absRefs++; continue }
101
- if (-not (Is-PathToken $t)) { continue }
102
- [void]$tokens.Add($t)
103
- }
104
- foreach ($t in $tokens) {
105
- if ($t -match '^(\.\./){2,}' -or $t -match '^\.\./packages/|^\.\./\.agents/') { $repoRelRefs++; continue }
106
- if ($t -match '^(\./)?downloads/') { $downloadsRefs++; continue }
107
- if (-not (Test-FileRel $t $fileDir)) { $missing++; $issues.Add("LINK-MISSING: [$relFile] -> $t") }
108
- }
109
- }
110
- $info.Add("link-scan: $($scopeFiles.Count) 个自有文档; 缺失 $missing; downloads 引用 $downloadsRefs(允许); checkout 相对链接 $repoRelRefs(跳过); 绝对路径 $absRefs(信息)")
111
-
112
- # ---- 3) 功能文档中的本机绝对路径(设计内的回退/示例,仅信息) ----
113
- foreach ($f in @('SKILL.md','README.md','guide/plugin-dev-guide.md','guide/quick-reference.md','guide/links.md')) {
114
- $hits = Select-String -Path (Join-Path $Root $f) -Pattern 'D:\\deepseek-harness' -AllMatches
115
- foreach ($h in $hits) { $info.Add("ABS-PATH(设计内回退/示例): [$f] L$($h.LineNumber)") }
116
- }
117
-
118
- # ---- 4) 官方文档副本与 checkout 漂移校验(可选: -Checkout <deepseek-harness 路径>) ----
119
- # 把副本与 ref(origin/master, 回退 HEAD)的 git blob 逐一对比: 与工作树/本地未推送提交无关, 只回答"副本与上游是否一致"。
120
- if ($Checkout -and (Test-Path $Checkout)) {
121
- $kbDocs = Join-Path $Root 'references\official-docs'
122
- $driftCount = 0; $kbExtra = 0; $missingKb = 0; $checkedDrift = 0
123
- $gitOk = (& git -C $Checkout rev-parse --is-inside-work-tree 2>$null) -eq 'true'
124
- if (-not $gitOk) {
125
- $info.Add("checkout-drift: $Checkout 不是 git 仓库, 跳过(使用 scripts/sync-official-docs.ps1 的非 git 模式同步)")
126
- } else {
127
- $ref = if ((& git -C $Checkout rev-parse --verify -q origin/master 2>$null)) { 'origin/master' } else { 'HEAD' }
128
- $map = @{
129
- 'docs' = 'docs';
130
- 'AGENTS.md' = 'AGENTS.md';
131
- 'BENCHMARK.md' = 'BENCHMARK.md';
132
- 'CONTRIBUTING.md' = 'CONTRIBUTING.md';
133
- 'CONTRIBUTING.zh.md' = 'CONTRIBUTING.zh.md';
134
- 'CONTRIBUTING.i18n.yaml' = 'CONTRIBUTING.i18n.yaml';
135
- 'README-zh.md' = 'README-zh.md';
136
- 'README.i18n.yaml' = 'README.i18n.yaml';
137
- 'THIRD_PARTY_NOTICES.md' = 'THIRD_PARTY_NOTICES.md';
138
- 'LICENSE' = 'LICENSE';
139
- 'packages/AGENTS.md' = 'packages\AGENTS.md';
140
- 'packages/README.md' = 'packages\README.md';
141
- 'vendor/README.md' = 'vendor\README.md';
142
- 'website/docs.ts' = 'website-docs.ts'
143
- }
144
- # CLAUDE.md 上游是 symlink(blob 为目标路径文本), 与 KB 普通文件 blob 必然不同——单独比对目标文本内容。
145
- $claudeUpstream = (& git -C $Checkout cat-file -p "${ref}:CLAUDE.md" 2>$null | Select-Object -First 1)
146
- if ($claudeUpstream) {
147
- $claudeKb = Join-Path $kbDocs 'CLAUDE.md'
148
- if (Test-Path $claudeKb) {
149
- $claudeKbContent = (Get-Content $claudeKb -Raw -Encoding UTF8).Trim()
150
- if ($claudeKbContent -ne $claudeUpstream.Trim()) { $issues.Add("CHECKOUT-DRIFT: CLAUDE.md symlink 目标文本与 $ref 不一致, 运行 sync-official-docs.ps1 同步") }
151
- } else { $missingKb++; $issues.Add("CHECKOUT-DRIFT-MISSING-KB: CLAUDE.md -> 副本无此文件, 请运行 sync-official-docs.ps1") }
152
- }
153
- $keys = @($map.Keys)
154
- foreach ($line in (& git -C $Checkout ls-tree -r $ref -- $keys)) {
155
- if (-not $line) { continue }
156
- $i = $line.IndexOf("`t")
157
- if ($i -lt 0) { continue }
158
- $path = $line.Substring($i + 1)
159
- $sha = (($line.Substring(0, $i) -split '\s+')[2])
160
- $checkedDrift++
161
- $prefix = $null; $kbRel = $null
162
- foreach ($k in $keys) { if ($path -eq $k -or $path.StartsWith("$k/")) { $prefix = $k; $kbRel = $map[$k]; break } }
163
- if (-not $kbRel) { continue }
164
- $suffix = if ($path -eq $prefix) { '' } else { $path.Substring($prefix.Length + 1) }
165
- $kbPath = if ($suffix) { Join-Path (Join-Path $kbDocs $kbRel) ($suffix -replace '/','\') } else { Join-Path $kbDocs $kbRel }
166
- if (-not (Test-Path $kbPath)) { $missingKb++; $issues.Add("CHECKOUT-DRIFT-MISSING-KB: $path -> 副本无此文件, 请运行 sync-official-docs.ps1"); continue }
167
- $kbSha = (& git -C $Checkout hash-object $kbPath).Trim()
168
- if ($kbSha -ne $sha) {
169
- $driftCount++
170
- if ($driftCount -le 10) { $issues.Add("CHECKOUT-DRIFT: $path 与 $ref 不一致(副本落后或超前于上游), 运行 sync-official-docs.ps1 同步") }
171
- }
172
- }
173
- # KB 内多出、源 ref 已没有的文件(仅 docs/ 目录映射)
174
- $docsDir = Join-Path $kbDocs 'docs'
175
- if (Test-Path $docsDir) {
176
- $docsRoot = (Resolve-Path $docsDir).Path
177
- $treePaths = New-Object System.Collections.Generic.HashSet[string]
178
- foreach ($line in (& git -C $Checkout ls-tree -r --name-only $ref -- docs)) { if ($line) { [void]$treePaths.Add($line) } }
179
- Get-ChildItem -Recurse -File $docsDir | ForEach-Object {
180
- $rel = 'docs/' + $_.FullName.Substring($docsRoot.Length + 1).Replace('\','/')
181
- if (-not $treePaths.Contains($rel)) { $kbExtra++; $issues.Add("CHECKOUT-DRIFT-KB-EXTRA: 副本有、$ref 无: $rel (同步时会被清理)") }
182
- }
183
- }
184
- $info.Add("checkout-drift: ref=$ref 比较 $checkedDrift 个 blob; 内容漂移 $driftCount, KB 缺失 $missingKb, KB 多余 $kbExtra")
185
- if ($Strict -and ($driftCount -gt 0 -or $missingKb -gt 0 -or $kbExtra -gt 0)) { $issues.Add("CHECKOUT-DRIFT-STRICT: -Strict 下漂移视为失败") }
186
- }
187
- } elseif ($Checkout) {
188
- $issues.Add("CHECKOUT-MISSING: -Checkout 指定的路径不存在: $Checkout")
189
- }
190
-
191
- # ---- 输出 ----
192
- $info | ForEach-Object { Write-Output $_ }
193
- if ($issues.Count -gt 0) { $issues | ForEach-Object { Write-Output $_ }; Write-Output "ISSUES: $($issues.Count)"; exit 1 }
194
- Write-Output "VERIFY-OK"; exit 0
1
+ # dsh-plugin-guide 完整性检查器
2
+ # 用法: pwsh -File scripts/verify-kit.ps1 -Root <路径>
3
+ # 校验范围: 本知识库自有文档(SKILL/README/NOTICE/guide/references 顶层);
4
+ # 官方文档副本(references/official-docs)是逐字副本,其内部链接指向 deepseek-harness checkout,不在校验范围。
5
+ param([string]$Root = '', [string]$Checkout = '', [switch]$Strict)
6
+ if ([string]::IsNullOrEmpty($Root)) { $Root = if ($PSScriptRoot) { Split-Path $PSScriptRoot -Parent } else { (Get-Location).Path } }
7
+ $ErrorActionPreference = 'SilentlyContinue'
8
+ $issues = New-Object System.Collections.Generic.List[string]
9
+ $info = New-Object System.Collections.Generic.List[string]
10
+
11
+ # ---- 1) 关键路径 ----
12
+ $critical = @(
13
+ 'SKILL.md','README.md','README-zh.md','README-es.md','README-pt.md','README-hi.md','LICENSE','NOTICE.md','.gitignore',
14
+ # 2026-10-13: 入口点与行尾策略。index.js/index.d.ts 曾是唯一无测试、也不受门禁保护的出厂代码,
15
+ # CRLF 缺陷就是从那里进的(见 tests/entry.test.ts 顶部注释);.gitattributes 是那类缺陷的根治手段。
16
+ 'index.js','index.d.ts','.gitattributes',
17
+ 'CONTRIBUTING.md','SECURITY.md','.github/ISSUE_TEMPLATE/bug_report.yml','.github/ISSUE_TEMPLATE/feature_request.yml','.github/PULL_REQUEST_TEMPLATE.md',
18
+ 'guide/plugin-dev-guide.md','guide/quick-reference.md','guide/links.md','guide/quick-reference.zh-CN.md','guide/quick-reference.es.md','guide/quick-reference.pt.md','guide/quick-reference.hi.md',
19
+ # 2026-10-13: 补齐 gated 文件——这些曾被漏掉,导致陈旧内容能存活到发布
20
+ 'guide/version-matrix.md','guide/family-toolchain.md','guide/migration-0.2.md','guide/unfixed-issues.md',
21
+ 'guide/release-engineering.md','guide/release-engineering.zh-CN.md',
22
+ 'guide/when-to-retire-a-plugin.md','guide/when-to-retire-a-plugin.zh-CN.md',
23
+ 'guide/choosing-a-plugin.md','guide/choosing-a-plugin.zh-CN.md',
24
+ 'references/sources.md','references/harness-repo.md','references/website-pages.md',
25
+ 'references/official-plugin-dev-skill.md','references/dev-resource-landscape.md',
26
+ 'src/cli/skill-sections.ts','src/cli/templates.ts',
27
+ 'references/upstream-cordis.md','references/cordis-paper-and-community.md',
28
+ 'references/community-ecosystem.md','references/community-repo-deep-dive.md',
29
+ 'references/official-docs/docs/cordis-primer.md',
30
+ 'references/official-docs/docs/cordis-tutorial/index.md',
31
+ 'references/official-docs/docs/cordis-tutorial/01-first-plugin.md',
32
+ 'references/official-docs/docs/cordis-tutorial/02-lifecycle-and-effects.md',
33
+ 'references/official-docs/docs/cordis-tutorial/03-services.md',
34
+ 'references/official-docs/docs/cordis-tutorial/04-events.md',
35
+ 'references/official-docs/docs/cordis-tutorial/05-config.md',
36
+ 'references/official-docs/docs/cordis-tutorial/06-composition-and-hmr.md',
37
+ 'references/official-docs/docs/cordis-tutorial/07-into-the-harness.md',
38
+ 'references/official-docs/docs/architecture.md',
39
+ 'references/official-docs/docs/cookbook/extension-cookbook.md',
40
+ 'references/official-docs/docs/cookbook/adding-a-tool.md',
41
+ 'references/official-docs/docs/event-producer-consumer.md',
42
+ 'references/official-docs/docs/user/develop/basic/index.md',
43
+ 'references/official-docs/docs/user/develop/basic/tool.md',
44
+ 'references/official-docs/docs/user/develop/basic/config.md',
45
+ 'references/official-docs/docs/user/develop/basic/publish.md',
46
+ 'references/official-docs/docs/user/develop/framework/service.md',
47
+ 'references/official-docs/docs/user/develop/framework/events.md',
48
+ 'references/official-docs/docs/user/develop/practice/index.md',
49
+ 'references/official-docs/docs/user/develop/practice/llm-adapter.md',
50
+ 'references/official-docs/docs/subsystems/session.md',
51
+ 'references/official-docs/docs/subsystems/tools.md',
52
+ 'references/official-docs/docs/cordis-api/context.md',
53
+ 'references/official-docs/AGENTS.md',
54
+ 'references/official-docs/SNAPSHOT.md',
55
+ 'references/official-docs/packages/AGENTS.md',
56
+ 'references/official-docs/packages/README.md',
57
+ 'references/official-docs/vendor/README.md',
58
+ 'scripts/download-sources.ps1','scripts/download-community-repos.ps1','scripts/gen-topic-snapshot.ps1','scripts/sync-official-docs.ps1','scripts/install-skill.ps1','scripts/verify-kit.ps1','scripts/archive-discussions.ps1','scripts/download-community-articles.ps1'
59
+ )
60
+ foreach ($c in $critical) { if (-not (Test-Path (Join-Path $Root $c))) { $issues.Add("CRITICAL-MISSING: $c") } }
61
+ $info.Add("critical: $($critical.Count) 项, 缺失 $((($issues | Where-Object { $_ -like 'CRITICAL-*' }) | Measure-Object).Count)")
62
+
63
+ # ---- 1b) 归档内容断言(防止同名文件错位: 历史事故是 examples/AGENTS.md 被当成仓库根 AGENTS.md) ----
64
+ function Assert-FirstLine([string]$rel, [string]$pattern, [string]$expect) {
65
+ $p = Join-Path $Root $rel
66
+ if (-not (Test-Path $p)) { return }
67
+ $first = Get-Content $p -Encoding UTF8 | Where-Object { $_.Trim() -ne '' } | Select-Object -First 1
68
+ if ($first -notlike $pattern) { $issues.Add("CONTENT-ASSERT-FAIL: $rel 首行应为 $expect, 实际: $first") }
69
+ }
70
+ Assert-FirstLine 'references/official-docs/AGENTS.md' '# AGENTS.md' "仓库根 AGENTS.md 标题"
71
+ Assert-FirstLine 'references/official-docs/packages/AGENTS.md' '# AGENTS.md*Harness Packages*' "packages/AGENTS.md 标题"
72
+
73
+ # ---- 2) 自有文档相对链接解析 ----
74
+ # 只检查 markdown 链接目标(读者会点击的);代码块/行内码里的裸词不参与,
75
+ # 避免把 name、ctx、- 等片段误判为路径。链接按"相对所在文件目录"解析;
76
+ # downloads/ 引用允许,指向 checkout 的逃逸链接(../../、../packages/ 等)跳过。
77
+ $scopeFiles = @((Join-Path $Root 'SKILL.md'), (Join-Path $Root 'README.md'), (Join-Path $Root 'NOTICE.md'))
78
+ $scopeFiles += Get-ChildItem (Join-Path $Root 'guide') -Filter *.md -File
79
+ $scopeFiles += Get-ChildItem (Join-Path $Root 'references') -Filter *.md -File # 仅顶层,不含 official-docs 子树
80
+ $missing = 0; $downloadsRefs = 0; $repoRelRefs = 0; $absRefs = 0
81
+
82
+ function Is-PathToken([string]$t) {
83
+ # 纯 ASCII 相对路径:含斜杠(guide/links.md、../references/x.md)或带扩展名(README.md);
84
+ # 裸词、含空格/CJK 的引文片段不参与检查
85
+ return $t -match '^(\.\./)?(\./)?(([A-Za-z0-9][A-Za-z0-9_.\-]*/)+[A-Za-z0-9_.\-]+(\.[A-Za-z0-9]+)?|[A-Za-z0-9][A-Za-z0-9_.\-]+\.[A-Za-z0-9]+)(#[A-Za-z0-9_\-/]+)?$'
86
+ }
87
+ function Test-FileRel([string]$p, [string]$dir) {
88
+ $p = $p -replace '^\./',''
89
+ $p = $p -replace '[?#].*$',''
90
+ if ($p -match '[*]') { $d = Split-Path (Join-Path $dir $p) -Parent; return Test-Path $d }
91
+ return Test-Path (Join-Path $dir $p)
92
+ }
93
+
94
+ foreach ($f in $scopeFiles) {
95
+ if (-not (Test-Path $f.FullName)) { continue }
96
+ $relFile = $f.FullName.Substring($Root.Length + 1)
97
+ $fileDir = Split-Path $f.FullName -Parent
98
+ $text = Get-Content $f.FullName -Raw -Encoding UTF8
99
+ $tokens = New-Object System.Collections.Generic.HashSet[string]
100
+ foreach ($m in [regex]::Matches($text, '\[[^\]]*\]\(([^)\s]+)\)')) {
101
+ $t = $m.Groups[1].Value.Trim()
102
+ if ($t -match '^https?://' -or $t -match '^[A-Za-z]:[\\/]' -or $t -match '^(<|\$|#)') { continue }
103
+ if ($t -match '^[A-Za-z]:[\\/]') { $absRefs++; continue }
104
+ if (-not (Is-PathToken $t)) { continue }
105
+ [void]$tokens.Add($t)
106
+ }
107
+ foreach ($t in $tokens) {
108
+ if ($t -match '^(\.\./){2,}' -or $t -match '^\.\./packages/|^\.\./\.agents/') { $repoRelRefs++; continue }
109
+ if ($t -match '^(\./)?downloads/') { $downloadsRefs++; continue }
110
+ if (-not (Test-FileRel $t $fileDir)) { $missing++; $issues.Add("LINK-MISSING: [$relFile] -> $t") }
111
+ }
112
+ }
113
+ $info.Add("link-scan: $($scopeFiles.Count) 个自有文档; 缺失 $missing; downloads 引用 $downloadsRefs(允许); checkout 相对链接 $repoRelRefs(跳过); 绝对路径 $absRefs(信息)")
114
+
115
+ # ---- 3) 功能文档中的本机绝对路径(设计内的回退/示例,仅信息) ----
116
+ foreach ($f in @('SKILL.md','README.md','guide/plugin-dev-guide.md','guide/quick-reference.md','guide/links.md')) {
117
+ $hits = Select-String -Path (Join-Path $Root $f) -Pattern 'D:\\deepseek-harness' -AllMatches
118
+ foreach ($h in $hits) { $info.Add("ABS-PATH(设计内回退/示例): [$f] L$($h.LineNumber)") }
119
+ }
120
+
121
+ # ---- 4) 官方文档副本与 checkout 漂移校验(可选: -Checkout <deepseek-harness 路径>) ----
122
+ # 把副本与 ref(origin/master, 回退 HEAD)的 git blob 逐一对比: 与工作树/本地未推送提交无关, 只回答"副本与上游是否一致"。
123
+ if ($Checkout -and (Test-Path $Checkout)) {
124
+ $kbDocs = Join-Path $Root 'references\official-docs'
125
+ $driftCount = 0; $kbExtra = 0; $missingKb = 0; $checkedDrift = 0
126
+ $gitOk = (& git -C $Checkout rev-parse --is-inside-work-tree 2>$null) -eq 'true'
127
+ if (-not $gitOk) {
128
+ $info.Add("checkout-drift: $Checkout 不是 git 仓库, 跳过(使用 scripts/sync-official-docs.ps1 的非 git 模式同步)")
129
+ } else {
130
+ $ref = if ((& git -C $Checkout rev-parse --verify -q origin/master 2>$null)) { 'origin/master' } else { 'HEAD' }
131
+ $map = @{
132
+ 'docs' = 'docs';
133
+ 'AGENTS.md' = 'AGENTS.md';
134
+ 'BENCHMARK.md' = 'BENCHMARK.md';
135
+ 'CONTRIBUTING.md' = 'CONTRIBUTING.md';
136
+ 'CONTRIBUTING.zh.md' = 'CONTRIBUTING.zh.md';
137
+ 'CONTRIBUTING.i18n.yaml' = 'CONTRIBUTING.i18n.yaml';
138
+ 'README-zh.md' = 'README-zh.md';
139
+ 'README.i18n.yaml' = 'README.i18n.yaml';
140
+ 'THIRD_PARTY_NOTICES.md' = 'THIRD_PARTY_NOTICES.md';
141
+ 'LICENSE' = 'LICENSE';
142
+ 'packages/AGENTS.md' = 'packages\AGENTS.md';
143
+ 'packages/README.md' = 'packages\README.md';
144
+ 'vendor/README.md' = 'vendor\README.md';
145
+ 'website/docs.ts' = 'website-docs.ts'
146
+ }
147
+ # CLAUDE.md 上游是 symlink(blob 为目标路径文本), 与 KB 普通文件 blob 必然不同——单独比对目标文本内容。
148
+ $claudeUpstream = (& git -C $Checkout cat-file -p "${ref}:CLAUDE.md" 2>$null | Select-Object -First 1)
149
+ if ($claudeUpstream) {
150
+ $claudeKb = Join-Path $kbDocs 'CLAUDE.md'
151
+ if (Test-Path $claudeKb) {
152
+ $claudeKbContent = (Get-Content $claudeKb -Raw -Encoding UTF8).Trim()
153
+ if ($claudeKbContent -ne $claudeUpstream.Trim()) { $issues.Add("CHECKOUT-DRIFT: CLAUDE.md symlink 目标文本与 $ref 不一致, 运行 sync-official-docs.ps1 同步") }
154
+ } else { $missingKb++; $issues.Add("CHECKOUT-DRIFT-MISSING-KB: CLAUDE.md -> 副本无此文件, 请运行 sync-official-docs.ps1") }
155
+ }
156
+ $keys = @($map.Keys)
157
+ foreach ($line in (& git -C $Checkout ls-tree -r $ref -- $keys)) {
158
+ if (-not $line) { continue }
159
+ $i = $line.IndexOf("`t")
160
+ if ($i -lt 0) { continue }
161
+ $path = $line.Substring($i + 1)
162
+ $sha = (($line.Substring(0, $i) -split '\s+')[2])
163
+ $checkedDrift++
164
+ $prefix = $null; $kbRel = $null
165
+ foreach ($k in $keys) { if ($path -eq $k -or $path.StartsWith("$k/")) { $prefix = $k; $kbRel = $map[$k]; break } }
166
+ if (-not $kbRel) { continue }
167
+ $suffix = if ($path -eq $prefix) { '' } else { $path.Substring($prefix.Length + 1) }
168
+ $kbPath = if ($suffix) { Join-Path (Join-Path $kbDocs $kbRel) ($suffix -replace '/','\') } else { Join-Path $kbDocs $kbRel }
169
+ if (-not (Test-Path $kbPath)) { $missingKb++; $issues.Add("CHECKOUT-DRIFT-MISSING-KB: $path -> 副本无此文件, 请运行 sync-official-docs.ps1"); continue }
170
+ $kbSha = (& git -C $Checkout hash-object $kbPath).Trim()
171
+ if ($kbSha -ne $sha) {
172
+ $driftCount++
173
+ if ($driftCount -le 10) { $issues.Add("CHECKOUT-DRIFT: $path 与 $ref 不一致(副本落后或超前于上游), 运行 sync-official-docs.ps1 同步") }
174
+ }
175
+ }
176
+ # KB 内多出、源 ref 已没有的文件(仅 docs/ 目录映射)
177
+ $docsDir = Join-Path $kbDocs 'docs'
178
+ if (Test-Path $docsDir) {
179
+ $docsRoot = (Resolve-Path $docsDir).Path
180
+ $treePaths = New-Object System.Collections.Generic.HashSet[string]
181
+ foreach ($line in (& git -C $Checkout ls-tree -r --name-only $ref -- docs)) { if ($line) { [void]$treePaths.Add($line) } }
182
+ Get-ChildItem -Recurse -File $docsDir | ForEach-Object {
183
+ $rel = 'docs/' + $_.FullName.Substring($docsRoot.Length + 1).Replace('\','/')
184
+ if (-not $treePaths.Contains($rel)) { $kbExtra++; $issues.Add("CHECKOUT-DRIFT-KB-EXTRA: 副本有、$ref 无: $rel (同步时会被清理)") }
185
+ }
186
+ }
187
+ $info.Add("checkout-drift: ref=$ref 比较 $checkedDrift 个 blob; 内容漂移 $driftCount, KB 缺失 $missingKb, KB 多余 $kbExtra")
188
+ if ($Strict -and ($driftCount -gt 0 -or $missingKb -gt 0 -or $kbExtra -gt 0)) { $issues.Add("CHECKOUT-DRIFT-STRICT: -Strict 下漂移视为失败") }
189
+ }
190
+ } elseif ($Checkout) {
191
+ $issues.Add("CHECKOUT-MISSING: -Checkout 指定的路径不存在: $Checkout")
192
+ }
193
+
194
+ # ---- 输出 ----
195
+ $info | ForEach-Object { Write-Output $_ }
196
+ if ($issues.Count -gt 0) { $issues | ForEach-Object { Write-Output $_ }; Write-Output "ISSUES: $($issues.Count)"; exit 1 }
197
+ Write-Output "VERIFY-OK"; exit 0