Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3E63DD?style=flat-square" alt="license"></a>
<a href="https://github.com/slow-stack/mneme/actions"><img src="https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml?style=flat-square&label=CI" alt="CI"></a>
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-22%2B-3E63DD?style=flat-square&logo=nodedotjs&logoColor=white" alt="node"></a>
<a href="https://github.com/slow-stack/mneme"><img src="https://img.shields.io/badge/tests-1523%20passed-3E63DD?style=flat-square" alt="tests"></a>
<a href="https://github.com/slow-stack/mneme"><img src="https://img.shields.io/badge/tests-1529%20passed-3E63DD?style=flat-square" alt="tests"></a>
<a href="https://codecov.io/gh/slow-stack/mneme"><img src="https://img.shields.io/codecov/c/github/slow-stack/mneme/main?style=flat-square" alt="coverage"></a>
<a href="https://github.com/awesome-dsh-plugin/awesome-dsh-plugin"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="Awesome"></a>
</p>
Expand Down Expand Up @@ -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 同步
```
Expand Down Expand Up @@ -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
```
Expand Down
6 changes: 6 additions & 0 deletions dsh-mneme/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

## 🐛 修复
Expand Down
6 changes: 3 additions & 3 deletions dsh-mneme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 运行时清单
```

Expand All @@ -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 钩子自动执行)
```
Expand Down
36 changes: 36 additions & 0 deletions dsh-mneme/scripts/changelog-prep.mjs
Original file line number Diff line number Diff line change
@@ -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 };
}
60 changes: 60 additions & 0 deletions dsh-mneme/test/changelog-prep.test.js
Original file line number Diff line number Diff line change
@@ -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");
});
20 changes: 14 additions & 6 deletions scripts/release-prep.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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);
}
}

Expand Down
Loading