dsh-gokifu-compact 1.11.8 → 1.11.9
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/CHANGELOG.md +48 -1
- package/README.md +2 -2
- package/package.json +1 -1
- package/scripts/release-lib.mjs +65 -0
- package/scripts/release.mjs +51 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 1.11.9 — 2026-10-31
|
|
4
|
+
|
|
5
|
+
### 发布脚本新增门禁:CHANGELOG 写的「N 个测试文件」必须等于事实
|
|
6
|
+
|
|
7
|
+
**这条门禁上线当天就抓到了真实漂移**:CHANGELOG 从 1.11.5 起一直写着同一个旧数字(**37**),
|
|
8
|
+
实际早就是 **42** —— 连续几个版本没人对过账(它不影响构建,所以从来不会自己叫)。
|
|
9
|
+
|
|
10
|
+
**新增(`scripts/release-lib.mjs`,纯逻辑、可单测)**
|
|
11
|
+
|
|
12
|
+
| 导出 | 行为 |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `declaredTestFiles(section)` | 读「N 个测试文件」;**没写返回 `null`**;容错 `**42** 个测试文件` / `(42 个测试文件;493 + 7)` / `42个测试文件` |
|
|
15
|
+
| `checkTestFileCount(section, actual)` | **三态**:一致放行 / 不一致给出 `{declared, actual, reason}` / 无声明放行 |
|
|
16
|
+
|
|
17
|
+
**`scripts/release.mjs` 体检 ②b**:当前版本那一节的数字 ≠ `test/*.test.mjs` 实际数 ⇒ **红,`--yes` 也不动手**;
|
|
18
|
+
**没写这句不报错**(允许不声明)。红文案同时给出两个数与「怎么数」(含历史版本数法):
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
· CHANGELOG 写的「N 个测试文件」与实际 42 个不符 —— 改了测试文件就必须同步这句(或删掉它,脚本不会因此报错)。
|
|
22
|
+
实际值怎么数:ls test/*.test.mjs | wc -l(看历史版本:git ls-tree -r --name-only <tag>^{commit} test/ | grep -c '\.test\.mjs$')
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 顺带修掉两个「只在预演期才撞到」的发布脚本缺陷
|
|
26
|
+
|
|
27
|
+
写这条门禁时跑 dry-run,脚本**直接崩了**,顺藤摸出两个真缺陷(都不影响发版那一刻,但会让「已发版本上再跑一次预演」变成堆栈):
|
|
28
|
+
|
|
29
|
+
1. **tag 已存在时 dry-run 崩**:`git rev-parse -q --verify refs/tags/x^{commit}` —— Windows 上 `execSync` 走 **cmd.exe**,
|
|
30
|
+
`^` 是转义字符 ⇒ 被吃成 `{commit}`,命令失败抛异常(手动在 Git Bash 里跑同一命令是好的,极难从表面看出)。
|
|
31
|
+
改用 `git rev-list -n1 refs/tags/<tag>`(无 shell 元字符,annotated / lightweight 都剥到 commit)+ try/catch。
|
|
32
|
+
2. **「tag 指向别的 commit」误报**:给 `ls-remote` 传**精确 pattern** 时**不会**输出 `^{}` 剥皮行 ⇒ 拿到的是 tag 对象 sha(≠ commit);
|
|
33
|
+
而且判据原本是「≠ HEAD」,但「发过之后 main 又往前走了」是**正常态**。改为列全部 tag 再在 JS 里过滤剥皮行,
|
|
34
|
+
判据改成「**既不是 HEAD 也不是 HEAD 的祖先**」(`git merge-base --is-ancestor`)才报。
|
|
35
|
+
|
|
36
|
+
### 验证
|
|
37
|
+
|
|
38
|
+
- 全量:**498 passed / 0 skipped / 0 failed**(**42 个测试文件**;493 + 5 条 `release-hardening`)
|
|
39
|
+
- `release-hardening` **15/0** · `verify-install` **16/16**
|
|
40
|
+
- `npm pack --dry-run --json`:**71 文件**(`test/` 不进包,数量不变)
|
|
41
|
+
- 三态端到端(临时克隆、版本 9.9.9):声明 37 / 实际 42 ⇒ **exit 1**;**无声明** ⇒ **exit 0**(附跳过说明);声明 42 ⇒ **exit 0**
|
|
42
|
+
- 仓库记录更正:1.11.8 节的「37」已改为「42」并在原处注明「发布时误写;npm 上已发内容无法改」
|
|
43
|
+
|
|
44
|
+
**未验证**
|
|
45
|
+
|
|
46
|
+
- 门禁只校验**当前 `package.json` 版本那一节**;历史各节不追溯。
|
|
47
|
+
- 没把「仓库自洽」做成单测:若在测试套件里断言「当前版本的数字 == 实际数」,每加一个测试文件都会红到下一版
|
|
48
|
+
(已发布那一节是冻结的)⇒ 这条只在**发布时**才对得起来。
|
|
49
|
+
|
|
3
50
|
## 1.11.8 — 2026-10-31
|
|
4
51
|
|
|
5
52
|
### sidecar 现在能**回灌**:归档掉的悬空边可以放回图里
|
|
@@ -34,7 +81,7 @@
|
|
|
34
81
|
|
|
35
82
|
### 验证
|
|
36
83
|
|
|
37
|
-
- 全量:**493 passed / 0 skipped / 0 failed
|
|
84
|
+
- 全量:**493 passed / 0 skipped / 0 failed**(**42** 个测试文件 —— 发布时误写 37,此处已按实际更正;npm 上 1.11.8 的 CHANGELOG 仍是 37,无法改)
|
|
38
85
|
- `migrate` **21/0** · `rooms` 15/0 · `verify-install` **16/16**
|
|
39
86
|
- `npm pack --dry-run --json`:**71 文件**(数量不变;只改 `src/` `scripts/` `test/`)
|
|
40
87
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-GOkifu-compact
|
|
2
2
|
|
|
3
|
-
> 当前版本:**`1.11.
|
|
3
|
+
> 当前版本:**`1.11.9`** · npm [`dsh-gokifu-compact`](https://www.npmjs.com/package/dsh-gokifu-compact) · [GitHub Releases](https://github.com/MYCF711/dsh-GOkifu-compact/releases) · 更新记录见 [CHANGELOG.md](CHANGELOG.md)
|
|
4
4
|
|
|
5
5
|
**DSH 的棋谱式上下文压缩后端 —— 把「历史 → 一段摘要」换成「历史 → 可复盘的棋谱 DAG + 每步只有 351 tokens 的热清单」。**
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
热清单 351 tokens (2000 边合成夹具 · L5 降级档 · 2026-10-30/10-31 两次实测同值;旧版全量 28,800)
|
|
9
9
|
失败保留 8 / 8 (历史口径 †;官方 8 段模板 4.67 / 8)
|
|
10
10
|
清单开销 未复现/待测 (原载 0.2% = 历史单会话 v1.3.1 后实测;无复跑命令、无对照组;源 evidence/真实计费核对.md)
|
|
11
|
-
验证
|
|
11
|
+
验证 498 项单测(0 skipped) · 真实包 8/8 † · 真实进程 fold 3~6/6 †
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
GO = 围棋,kifu = 棋谱,compact = 压缩。一局棋下完,棋谱上不只有落子顺序,还有每一手的评语。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gokifu-compact",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.9",
|
|
4
4
|
"description": "DSH context-compression backend with a go game record (kifu) DAG: node = position, edge = attempted move + result. Incremental fold (each span summarized once), failure memory that survives compaction, transposition detection (chess-engine style), and a tiny hot ledger rebuilt every step.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
package/scripts/release-lib.mjs
CHANGED
|
@@ -101,3 +101,68 @@ export function runTransaction(steps, opts = {}) {
|
|
|
101
101
|
}
|
|
102
102
|
return { ok: true, done, failed: null, error: null, aborted: [] }
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 从 CHANGELOG 某节里读出**所有**「N 个测试文件」的声明。
|
|
107
|
+
* 支持这些写法(本仓历史都用过):
|
|
108
|
+
* `(37 个测试文件)` · `(37 个测试文件;481 + 5)` · `37 个测试文件` · `**42** 个测试文件`
|
|
109
|
+
* @returns {number[]} 没写就是空数组(不是错误)
|
|
110
|
+
*/
|
|
111
|
+
export function declaredTestFilesAll(section) {
|
|
112
|
+
const text = String(section ?? "")
|
|
113
|
+
const re = /(\d+)\s*\*{0,2}\s*个测试文件/g
|
|
114
|
+
const out = []
|
|
115
|
+
let m
|
|
116
|
+
while ((m = re.exec(text)) !== null) out.push(Number(m[1]))
|
|
117
|
+
return out
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 从 CHANGELOG 某节里读出「N 个测试文件」的**声明**(第一个)。
|
|
122
|
+
* @returns {number|null} 没写就返回 null(不是错误)
|
|
123
|
+
*/
|
|
124
|
+
export function declaredTestFiles(section) {
|
|
125
|
+
const all = declaredTestFilesAll(section)
|
|
126
|
+
return all.length > 0 ? all[0] : null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 校验「N 个测试文件」的声明与实际发现的 `test/*.test.mjs` 数量是否一致。
|
|
131
|
+
*
|
|
132
|
+
* **三态**(task-160 的契约):
|
|
133
|
+
* · 一致 ⇒ `{ ok: true, declared: N }`
|
|
134
|
+
* · 不一致 ⇒ `{ ok: false, declared: N, reason }` —— 发布必须被拦住
|
|
135
|
+
* · 没有声明 ⇒ `{ ok: true, declared: null }` —— 不报错(允许不写这个数)
|
|
136
|
+
*
|
|
137
|
+
* ⚠ 一节里可能出现**多个**数字(例如正文引用了旧版报错原文)。那种情况按「**全部都要等**」判:
|
|
138
|
+
* 只要出现一个不等于实际的数字就红,并把所有值列出来 —— 否则这段文档自己就是一锅粥,
|
|
139
|
+
* 门禁被自己的举例骗过(1.11.9 起草时真实发生过)。
|
|
140
|
+
*
|
|
141
|
+
* 为什么要有这条:本仓 CHANGELOG 长期写「N 个测试文件」,而 N 从 1.11.5 起就停在 37、
|
|
142
|
+
* 实际已经 42 —— 数字漂了 5 个版本没人发现。用户会拿这个数判断覆盖面。
|
|
143
|
+
*
|
|
144
|
+
* @param {string} section 当前版本那一节的正文
|
|
145
|
+
* @param {number} actual 实际 `test/*.test.mjs` 文件数
|
|
146
|
+
*/
|
|
147
|
+
export function checkTestFileCount(section, actual) {
|
|
148
|
+
const all = declaredTestFilesAll(section)
|
|
149
|
+
if (all.length === 0) return { ok: true, declared: null, actual, declaredAll: [] }
|
|
150
|
+
const declared = all[0]
|
|
151
|
+
const bad = all.filter((n) => n !== actual)
|
|
152
|
+
if (bad.length === 0) return { ok: true, declared, actual, declaredAll: all }
|
|
153
|
+
const distinct = [...new Set(all)]
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
declared,
|
|
157
|
+
actual,
|
|
158
|
+
declaredAll: all,
|
|
159
|
+
reason:
|
|
160
|
+
distinct.length > 1
|
|
161
|
+
? `CHANGELOG 这一节里出现了多个不一致的「N 个测试文件」:${distinct.join(" / ")}(实际 ${actual} 个)—— ` +
|
|
162
|
+
`一处写对、另一处写错同样会误导;引用旧数字时请用占位符(如「N 个测试文件」),别写真实旧值`
|
|
163
|
+
: `CHANGELOG 写的「${declared} 个测试文件」与实际 ${actual} 个不符 —— ` +
|
|
164
|
+
`改了测试文件就必须同步这句(或删掉它,脚本不会因此报错)。` +
|
|
165
|
+
`实际值怎么数:ls test/*.test.mjs | wc -l(看历史版本:` +
|
|
166
|
+
`git ls-tree -r --name-only <tag>^{commit} test/ | grep -c '\\.test\\.mjs$')`,
|
|
167
|
+
}
|
|
168
|
+
}
|
package/scripts/release.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
//
|
|
13
13
|
// 体检项(任一不过就退出码 1,且 --yes 也不动手):
|
|
14
14
|
// ① package.json 有 version ② CHANGELOG 有这一版那一段
|
|
15
|
+
// ②b 「N 个测试文件」的声明 == test/*.test.mjs 实际数(**没写这句不报错**)
|
|
15
16
|
// ③ 工作区干净 ④ 当前在默认分支
|
|
16
17
|
// ⑤ remote 是合法 GitHub URL(拒 git+https 之类) ⑥ 远端可达(ls-remote 必须成功)
|
|
17
18
|
// ⑦ 不落后远端(fetch 必须成功,失败=红,不再吞成 note) ⑧ tag 还没被占
|
|
@@ -24,9 +25,9 @@
|
|
|
24
25
|
// GITHUB_TOKEN / GH_TOKEN 环境变量。没有也能跑 —— 它会打印手工建 Release 的命令。
|
|
25
26
|
|
|
26
27
|
import { execSync, execFileSync } from "node:child_process"
|
|
27
|
-
import { readFileSync, existsSync } from "node:fs"
|
|
28
|
+
import { readFileSync, readdirSync, existsSync } from "node:fs"
|
|
28
29
|
import { join } from "node:path"
|
|
29
|
-
import { classifyRemote, runTransaction } from "./release-lib.mjs"
|
|
30
|
+
import { classifyRemote, runTransaction, checkTestFileCount } from "./release-lib.mjs"
|
|
30
31
|
|
|
31
32
|
const ROOT = process.cwd()
|
|
32
33
|
const argv = new Set(process.argv.slice(2))
|
|
@@ -94,6 +95,20 @@ function changelogSection(v) {
|
|
|
94
95
|
const section = version ? changelogSection(version) : ""
|
|
95
96
|
if (version && !section) bad(`CHANGELOG.md 里没有「## ${version}」那一段 —— 先写更新内容再发版`)
|
|
96
97
|
|
|
98
|
+
// ── 「N 个测试文件」的声明必须等于事实 ────────────────────────────────
|
|
99
|
+
// 这条是 task-160 加的:本仓 CHANGELOG 连续 5 个版本写「37 个测试文件」,实际早就是 42
|
|
100
|
+
// —— 数字漂了没人发现,而用户会拿它判断覆盖面。**没写这句不报错**(允许不写)。
|
|
101
|
+
const testDir = join(ROOT, "test")
|
|
102
|
+
const actualTestFiles = existsSync(testDir)
|
|
103
|
+
? readdirSync(testDir).filter((f) => f.endsWith(".test.mjs")).length
|
|
104
|
+
: 0
|
|
105
|
+
const testFileCheck = checkTestFileCount(section, actualTestFiles)
|
|
106
|
+
if (testFileCheck.declared === null) {
|
|
107
|
+
note(`CHANGELOG 没写「N 个测试文件」⇒ 跳过该数字校验(实际 ${actualTestFiles} 个)`)
|
|
108
|
+
} else if (!testFileCheck.ok) {
|
|
109
|
+
bad(testFileCheck.reason)
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
/**
|
|
98
113
|
* 去掉 `> 起因:…` 那段 —— 它写给维护者,不进 Release 正文。
|
|
99
114
|
* (UPDATING 四:一条一行说的是改了什么,`> 起因` 说的是为什么。用户感知不到后者。)
|
|
@@ -183,22 +198,45 @@ let tagCommitLocal = ""
|
|
|
183
198
|
let tagCommitRemote = ""
|
|
184
199
|
if (gh) {
|
|
185
200
|
tagExistsLocal = shOk(`git rev-parse -q --verify refs/tags/${tag}`)
|
|
186
|
-
|
|
201
|
+
// ⚠ 不能用 `refs/tags/x^{commit}`:Windows 上 execSync 走 cmd.exe,`^` 是转义字符 ⇒
|
|
202
|
+
// 会被吃成 `refs/tags/x{commit}`,命令失败、脚本崩(2026-10-31 实测:tag 已存在时跑预演直接抛异常)。
|
|
203
|
+
// `rev-list -n1` 对 annotated / lightweight tag 都会剥到 commit,且没有 shell 元字符。
|
|
204
|
+
if (tagExistsLocal) {
|
|
205
|
+
try {
|
|
206
|
+
tagCommitLocal = sh(`git rev-list -n1 refs/tags/${tag}`)
|
|
207
|
+
} catch {
|
|
208
|
+
tagCommitLocal = ""
|
|
209
|
+
}
|
|
210
|
+
}
|
|
187
211
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const deref =
|
|
193
|
-
const plain =
|
|
194
|
-
|
|
212
|
+
// ⚠ 不能给 ls-remote 传 `refs/tags/x` 这种精确 pattern:那样**不会**输出 `^{}` 剥皮行,
|
|
213
|
+
// 拿到的只是 tag 对象 sha(annotated tag 时 ≠ commit)。列全部 tag 再由 JS 过滤。
|
|
214
|
+
const out = sh(`git ls-remote --tags origin`)
|
|
215
|
+
const lines = out.split("\n").filter(Boolean)
|
|
216
|
+
const deref = lines.find((l) => l.endsWith(`refs/tags/${tag}^{}`))
|
|
217
|
+
const plain = lines.find((l) => l.endsWith(`refs/tags/${tag}`))
|
|
218
|
+
tagExistsRemote = Boolean(plain || deref)
|
|
219
|
+
// annotated tag:deref 行的 sha 才是 commit;lightweight tag:只有 plain 行且它本身就是 commit。
|
|
220
|
+
tagCommitRemote = ((deref ?? plain ?? "").split(/\s+/)[0] ?? "").trim()
|
|
195
221
|
} catch {}
|
|
196
222
|
const headSha = shOk("git rev-parse HEAD") ? sh("git rev-parse HEAD") : ""
|
|
197
|
-
|
|
223
|
+
/** tag 指向的 commit 是不是 HEAD 本身或 HEAD 的祖先(= 正常「发过了,之后又提交了几次」) */
|
|
224
|
+
const tagIsBehindHead = (sha) => {
|
|
225
|
+
if (!sha) return false
|
|
226
|
+
if (!headSha) return false
|
|
227
|
+
if (sha === headSha || headSha.startsWith(sha)) return true
|
|
228
|
+
try {
|
|
229
|
+
sh(`git merge-base --is-ancestor ${sha} HEAD`)
|
|
230
|
+
return true
|
|
231
|
+
} catch {
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
}
|
|
198
235
|
if (tagExistsLocal || tagExistsRemote) {
|
|
199
|
-
|
|
236
|
+
const remoteSha = tagCommitRemote || tagCommitLocal
|
|
237
|
+
if (tagExistsRemote && remoteSha && !tagIsBehindHead(remoteSha)) {
|
|
200
238
|
bad(
|
|
201
|
-
`远端 tag ${tag} 已存在,但指向 ${
|
|
239
|
+
`远端 tag ${tag} 已存在,但指向 ${remoteSha.slice(0, 12)},既不是 HEAD 也不是 HEAD 的祖先 —— ` +
|
|
202
240
|
`可能是上次发布中断时由 Release API 自动建的轻量 tag。脚本**不会自动 force**。` +
|
|
203
241
|
`确认这个 tag 发错之后,手工修:git push --force origin refs/tags/${tag}:refs/tags/${tag}`,
|
|
204
242
|
)
|