From 1f1659fa2fa0510cea2e91964cc1deb34f5b7a51 Mon Sep 17 00:00:00 2001 From: modusensus Date: Sun, 4 Oct 2026 01:01:33 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(release-prep):=20CHANGELOG=20=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D=E8=8A=82=E5=9C=A8=20CRLF=20=E6=A3=80=E5=87=BA?= =?UTF-8?q?=E4=B8=8A=E4=B8=8D=E5=86=8D=E9=9D=99=E9=BB=98=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原实现用 /^(# Changelog\n\n)/ 匹配 CHANGELOG 文件头,而 Windows 检出是 CRLF —— 正则命中 不了,`replace` 退化成空操作,**但脚本照样打印 `✓ … 占位节`**,`git status` 里看不出任何 异常。CI 的 release-prep 工作流跑在 ubuntu(LF)所以从未暴露,只有本机发版会中招——v0.8.13 那次即如此,最后靠人工补的占位节。 改法:规则抽成 dsh-mneme/scripts/changelog-prep.mjs 的纯函数(入口在仓库根、import 即执行, 测不了;与 test-count-sync.mjs 收拢替换规则同一个理由)。 - 行尾 `\r?\n` 两种都吃,插入内容跟随原文件行尾(不混排),带 BOM 也认; - 匹配不上时返回 {ok:false, reason:"header-not-found"} 而不是静默返回原文,入口据此报错 并 exit 1 —— 绝不假打印成功。 6 条回归测试:LF / CRLF / BOM / 幂等 / 回报契约 / detectEol。变异检验:把正则改回只认 LF、 失败时改回「返回原文」、固定用 LF 插入——三条各自让对应用例变红。 --- dsh-mneme/scripts/changelog-prep.mjs | 36 ++++++++++++++++ dsh-mneme/test/changelog-prep.test.js | 60 +++++++++++++++++++++++++++ scripts/release-prep.mjs | 20 ++++++--- 3 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 dsh-mneme/scripts/changelog-prep.mjs create mode 100644 dsh-mneme/test/changelog-prep.test.js diff --git a/dsh-mneme/scripts/changelog-prep.mjs b/dsh-mneme/scripts/changelog-prep.mjs new file mode 100644 index 0000000..6922600 --- /dev/null +++ b/dsh-mneme/scripts/changelog-prep.mjs @@ -0,0 +1,36 @@ +// 发布准备里「在 CHANGELOG 顶部插版本占位节」这一步的单一实现。 +// +// 为什么抽成模块而不是留在入口里:入口 scripts/release-prep.mjs 在仓库根、import 即 +// 执行,测不了;而这一步恰恰踩过坑——原先的实现在 **CRLF 检出**上用 /^(# Changelog\n\n)/ +// 匹配文件头,命中不了于是 replace 退化成 no-op,**脚本却照样打印 ✓**,git status 里 +// 什么也看不见。发版时最怕的就是这种假成功(CI 跑在 ubuntu 上是 LF,所以只有本机会中招)。 +// 抽出来之后:行尾两种都能吃,匹配不上则**如实回报**,由入口报错退出。 +// +// 与 scripts/test-count-sync.mjs 同一个理由:规则收在共用模块里才测得到、也才不会两处各写一份。 + +/** 取出文本的行尾风格:插入的内容跟着它走,避免把 CRLF 文件混成一半 CRLF 一半 LF。 */ +export function detectEol(text) { + return text.includes("\r\n") ? "\r\n" : "\n"; +} + +/** + * 在 `# Changelog` 标题之后插入 `## [version] - date` 占位节(幂等:已含该版本则不改)。 + * + * 返回 `{ok:false, reason}` 而不是静默返回原文——调用方必须能区分「没什么可做」与 + * 「该做但没做成」,后者要报错退出。`reason` 取值:`already-present` / `header-not-found`。 + * + * @param {string} text CHANGELOG 全文 + * @param {string} version 如 "0.8.13" + * @param {string} date 如 "2026-10-03" + * @returns {{ok: true, text: string} | {ok: false, reason: "already-present" | "header-not-found"}} + */ +export function insertReleaseSection(text, version, date) { + if (text.includes(`## [${version}]`)) return { ok: false, reason: "already-present" }; + const eol = detectEol(text); + const head = `## [${version}] - ${date}${eol}${eol}## 🐛 修复${eol}${eol}- (待填)${eol}${eol}`; + // BOM 可选:带 BOM 的检出上 `^#` 同样匹配不到,属于同一类「形状变了就静默失败」。 + // 行尾 `\r?\n` 两种都吃——这就是本模块存在的首要理由。 + const out = text.replace(/^(\uFEFF?# Changelog\r?\n\r?\n)/, `$1${head}`); + if (out === text) return { ok: false, reason: "header-not-found" }; + return { ok: true, text: out }; +} diff --git a/dsh-mneme/test/changelog-prep.test.js b/dsh-mneme/test/changelog-prep.test.js new file mode 100644 index 0000000..3de7df0 --- /dev/null +++ b/dsh-mneme/test/changelog-prep.test.js @@ -0,0 +1,60 @@ +// scripts/changelog-prep.mjs 的回归测试。 +// +// 这一步的价值全在「别假成功」上:发布准备原先在 CRLF 检出上匹配不到 CHANGELOG 文件头, +// replace 退化成 no-op 却照样打印 ✓,git status 里看不出任何异常——CI 跑在 ubuntu(LF), +// 所以只有本机发版会中招(v0.8.13 那次就是这么踩到的,最后靠人工补的占位节)。 +// 下面用 LF / CRLF / BOM 三种输入各钉一遍,并锁住「匹配不上要如实回报」这条契约: +// 调用方靠 reason 决定 exit 1,退回「返回原文」就等于退回假成功。 +import test from "node:test"; +import assert from "node:assert/strict"; + +import { detectEol, insertReleaseSection } from "../scripts/changelog-prep.mjs"; + +const LF_HEAD = "# Changelog\n\n## [0.8.12] - 2026-10-01\n\n## 🐛 修复\n\n- 旧条目\n"; +const CRLF_HEAD = LF_HEAD.replace(/\n/g, "\r\n"); +/** 有没有「裸 LF」(前面不是 CR 的 \n)——混排行尾的判据。 */ +const hasBareLf = (text) => /(^|[^\r])\n/.test(text); + +test("LF 输入:插入占位节,行尾保持 LF,旧内容原样保留", () => { + const r = insertReleaseSection(LF_HEAD, "0.8.13", "2026-10-03"); + assert.equal(r.ok, true); + assert.ok(r.text.startsWith("# Changelog\n\n## [0.8.13] - 2026-10-03"), "插在标题之后"); + assert.ok(r.text.includes("- (待填)"), "带 release.yml verify 要的占位节"); + assert.ok(r.text.includes("## [0.8.12] - 2026-10-01"), "旧内容不动"); + assert.equal(r.text.includes("\r\n"), false, "不该往 LF 文件里混 CRLF"); +}); + +test("CRLF 输入:同样能插(原先这里静默 no-op —— 本模块存在的首要理由)", () => { + const r = insertReleaseSection(CRLF_HEAD, "0.8.13", "2026-10-03"); + assert.equal(r.ok, true, "CRLF 检出上必须也能插入"); + assert.notEqual(r.text, CRLF_HEAD, "绝不返回原文——那正是当初的假成功"); + assert.ok(r.text.includes("## [0.8.13] - 2026-10-03")); + assert.equal(hasBareLf(r.text), false, "插入的内容也要用 CRLF,不混行尾"); +}); + +test("带 BOM 的输入也能插(`^#` 匹配不到属同一类形状漂移)", () => { + const r = insertReleaseSection("\uFEFF" + LF_HEAD, "0.8.13", "2026-10-03"); + assert.equal(r.ok, true); + assert.ok(r.text.includes("## [0.8.13] - 2026-10-03")); +}); + +test("已含该版本:幂等跳过,并如实回报 already-present", () => { + const once = insertReleaseSection(LF_HEAD, "0.8.13", "2026-10-03"); + assert.equal(once.ok, true); + assert.deepEqual(insertReleaseSection(once.text, "0.8.13", "2026-10-03"), { ok: false, reason: "already-present" }); +}); + +test("文件头不匹配:回报 header-not-found,而不是静默返回原文", () => { + // 这条锁的是契约本身。若有人把实现改回「返回原文 + 入口照旧打印 ✓」,它会红。 + assert.deepEqual(insertReleaseSection("# ChangelogX\n\n## [0.8.12]\n", "0.8.13", "2026-10-03"), { + ok: false, + reason: "header-not-found" + }); + assert.deepEqual(insertReleaseSection("", "0.8.13", "2026-10-03"), { ok: false, reason: "header-not-found" }); +}); + +test("detectEol:认两种行尾,无换行时按 LF 处理", () => { + assert.equal(detectEol("a\nb"), "\n"); + assert.equal(detectEol("a\r\nb"), "\r\n"); + assert.equal(detectEol("a"), "\n"); +}); diff --git a/scripts/release-prep.mjs b/scripts/release-prep.mjs index 57274ef..eb09e1d 100644 --- a/scripts/release-prep.mjs +++ b/scripts/release-prep.mjs @@ -6,6 +6,7 @@ import fs from "node:fs"; import { applyTestCount } from "../dsh-mneme/scripts/test-count-sync.mjs"; +import { insertReleaseSection } from "../dsh-mneme/scripts/changelog-prep.mjs"; const version = (process.argv[2] || "").replace(/^v/, ""); const testCount = process.argv[3]; @@ -48,16 +49,23 @@ if (testCount && /^\d+$/.test(testCount)) { } // ── 3. CHANGELOG 顶部空节(满足 release.yml verify 的 ^## [V] 检查)── +// 规则收在 dsh-mneme/scripts/changelog-prep.mjs(可测、行尾两种都吃)。原先这段用 +// /^(# Changelog\n\n)/ 匹配文件头,CRLF 检出上命中不了 → replace 退化成 no-op 却照样 +// 打印 ✓,git status 里看不出任何异常——发版时最怕这种假成功。现在匹配不上直接报错 +// 退出,绝不假装写成功。 { const t = fs.readFileSync(CHANGELOG, "utf8"); - if (!t.includes(`## [${version}]`)) { - // 按东八区取日期(与历史 CHANGELOG 日期惯例一致) - const date = new Date(Date.now() + 8 * 3600 * 1000).toISOString().slice(0, 10); - const head = `## [${version}] - ${date}\n\n## 🐛 修复\n\n- (待填)\n\n`; - fs.writeFileSync(CHANGELOG, t.replace(/^(# Changelog\n\n)/, "$1" + head)); + // 按东八区取日期(与历史 CHANGELOG 日期惯例一致) + const date = new Date(Date.now() + 8 * 3600 * 1000).toISOString().slice(0, 10); + const r = insertReleaseSection(t, version, date); + if (r.ok) { + fs.writeFileSync(CHANGELOG, r.text); console.log(`✓ ${CHANGELOG} 占位节`); - } else { + } else if (r.reason === "already-present") { console.log(`- ${CHANGELOG} 已含 ${version},跳过`); + } else { + console.error(`✗ ${CHANGELOG} 顶部没匹配到 "# Changelog" 标题,未插入占位节(reason=${r.reason})——请人工检查文件形状`); + process.exit(1); } } From 2047801c3a46a34042bc1f08c92147f7b9d113d4 Mon Sep 17 00:00:00 2001 From: modusensus Date: Sun, 4 Oct 2026 01:01:33 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs(changelog):=20=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E5=87=86=E5=A4=87=E8=84=9A=E6=9C=AC=E7=9A=84?= =?UTF-8?q?=20CRLF=20=E5=81=87=E6=88=90=E5=8A=9F=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=EF=BC=9Bbadge:sync=20=E5=88=B7=E5=88=B0=201529?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本批新增 6 条用例,双 README 的测试数随之从 1523 用 npm run badge:sync 刷到 1529(6 处), 不手改。 --- README.md | 6 +++--- dsh-mneme/CHANGELOG.md | 6 ++++++ dsh-mneme/README.md | 6 +++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1028dcb..1212631 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ license CI node - tests + tests coverage Awesome

@@ -183,7 +183,7 @@ dsh web ```bash cd dsh-mneme && npm install -npm test # 1523 个测试 +npm test # 1529 个测试 npm run stress # 三轴线压测 npm run sync # src → lib 同步 ``` @@ -366,7 +366,7 @@ The plugin ships a zero-dependency stdio MCP server (standalone npm package **`m ```bash cd dsh-mneme && npm install -npm test # 1523 tests +npm test # 1529 tests npm run stress # three-axis stress test npm run sync # src → lib sync ``` diff --git a/dsh-mneme/CHANGELOG.md b/dsh-mneme/CHANGELOG.md index f7dc960..dc0ce96 100644 --- a/dsh-mneme/CHANGELOG.md +++ b/dsh-mneme/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +## 🧹 工程 + +- **发布准备脚本在 CRLF 检出上不再假成功(`scripts/release-prep.mjs`)**:该脚本用 `/^(# Changelog\n\n)/` 匹配 CHANGELOG 文件头,而 Windows 检出是 CRLF——正则命中不了,`replace` 退化成空操作,**脚本却照样打印 `✓ … 占位节`**,`git status` 里看不出任何异常(CI 跑在 ubuntu 是 LF,所以只有本机发版会中招,v0.8.13 那次即如此、最后靠人工补的占位节)。规则抽成 `dsh-mneme/scripts/changelog-prep.mjs` 的纯函数:行尾两种都吃、插入内容跟随原文件行尾、带 BOM 也认;匹配不上则如实回报 `header-not-found`,入口**报错退出(exit 1)**而不是假打印成功。配 6 条回归测试(LF / CRLF / BOM / 幂等 / 回报契约 / detectEol)。 + ## [0.8.13] - 2026-10-03 ## 🐛 修复 diff --git a/dsh-mneme/README.md b/dsh-mneme/README.md index 5d97de0..61b53a2 100644 --- a/dsh-mneme/README.md +++ b/dsh-mneme/README.md @@ -5,7 +5,7 @@ [![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme) [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) -[![tests](https://img.shields.io/badge/tests-1523%20passed-success)](https://github.com/slow-stack/mneme) +[![tests](https://img.shields.io/badge/tests-1529%20passed-success)](https://github.com/slow-stack/mneme) [![CI](https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml)](https://github.com/slow-stack/mneme/actions) [![node](https://img.shields.io/badge/node-22%2B-blue)](https://nodejs.org) [![npm downloads](https://img.shields.io/npm/d18m/@modusensus/dsh-mneme.svg?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme) @@ -584,7 +584,7 @@ src/ ├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列) └── index.js # 插件接线 lib/ # src 的同步分发产物(npm run sync;发布前由 root prepack 的 check-sync.js 校验一致性;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖) -test/ # 1523 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验) +test/ # 1529 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验) scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js / benchmark-embed.js / benchmark-rerank.js 基准 · sync-test-badge.mjs 测试徽章 · build-runtime-manifest.mjs 运行时清单 ``` @@ -593,7 +593,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压 ```bash cd dsh-mneme npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试) -npm test # 运行 1523 个测试 +npm test # 运行 1529 个测试 npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM) npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行) ```