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-2680%20passed-3E63DD?style=flat-square" alt="tests"></a>
<a href="https://github.com/slow-stack/mneme"><img src="https://img.shields.io/badge/tests-2681%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 @@ -272,7 +272,7 @@ dsh-mneme-serve # 默认 ~/.dsh/memory + 127.0.0.1:8790

```bash
cd dsh-mneme && npm install
npm test # 2680 个测试
npm test # 2681 个测试
npm run stress # 三轴线压测
npm run sync # src → lib 同步
```
Expand Down Expand Up @@ -554,7 +554,7 @@ Community projects that wire mneme into more platforms — not part of this repo

```bash
cd dsh-mneme && npm install
npm test # 2680 tests
npm test # 2681 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
Expand Up @@ -2,6 +2,12 @@

## [Unreleased]

- **修一个打开记忆详情就崩掉整个宿主的崩溃(#553)**:0.10.0 里点开侧边栏任意一条记忆的详情抽屉,渲染进程立刻抛 `ReferenceError: provenance is not defined`。崩的是 renderer,整个宿主跟着重启,进行中的会话被打断,100% 复现。

根因是两个隔了三千多行的组件:`MemoryDrawer` 的归因 effect 与渲染块在读 `provenance` / `setProvenance`,而那句 `useState` 落在了同文件**上一个**组件 `EntityPanel` 里(`74ba401` 加归因区块时放错了位置)。`EntityPanel` 里那份是纯死代码——整个函数体除声明外对 `provenance` 零引用。渲染先于 effect,所以第一帧就炸;就算渲染放过,effect 也会在 `setProvenance` 上炸。声明挪进 `MemoryDrawer`、死代码删掉,并写明这个 state 为什么必须住在读它的那个组件里。

这个形状躲得过眼睛(隔三千行、同名 state 看着完全正常)、语法检查(`provenance` 是合法标识符,只是不在作用域里)和原有用例(只读源码字符串,从不执行组件),所以补的不是「锁一个名字」的用例,而是按组件扫「调了 `setXxx(` 却没有声明」——锁的是这个结构。扫描器要认的几种合法情形都放行了:`setTimeout`/`setInterval` 这类全局、把 setter 当形参往下传的嵌套函数、模块级同名 setter(`openLibrary` 调 `setOverlayOpen`,那是模块级 pub/sub,不是 React state)。变异自证:把声明原样搬回 `EntityPanel`,用例当场打红 `MemoryDrawer calls setProvenance without declaring it`。

- **图谱的锚定层补上另外两路起点,被动确认有了能查的地方(#24 收尾)**:issue #24 的四块设计里,块 1 的「精确名 / Tag / 向量三路起点」当初只接了精确名一路——`anchoring.js` 本身早就支持多路(`anchorSeeds` 累加、`HOP_QUOTA` 分层都在),缺的是 `service.js` 的调用点只喂了 `normalizePath(..., { path: "exact" })` 一路,而通路身份在合并那一步就丢了(`normalizePath` 的返回值里没有 `path`,`anchorSeeds` 里 `p.path ?? "?"` 把每一路都记成 `"?"`)。块 4 的「例外管理」同样只有自动确认、没有可见面——面板里 `passiveConfirm` 一次都不出现。

**Tag 路走全等匹配,不走子串**:`store.findEntitiesByTags` 把查询按标点和空白切成词、滤掉单字(中文单字作 tag 没有区分度),再要求 `tags` 数组里**某个 tag 与词完全相等**。子串匹配会让 `ai` 命中 `mail`、让 `Java` 命中 `JavaScript`,而这条路的作用是「用户提到了某个主题词」,模糊命中的代价是把整类无关记忆拉进图轴。排序按命中记忆数降序、名称升序——同分时要有确定性顺序,否则同一查询两次跑出来的种子次序不同。
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-2680%20passed-success)](https://github.com/slow-stack/mneme)
[![tests](https://img.shields.io/badge/tests-2681%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 @@ -721,7 +721,7 @@ src/
├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列)
└── index.js # 插件接线
lib/ # src 的同步分发产物(npm run sync,prepack 钩子在发布时自动跑;一致性由 scripts/check-sync.js 在 CI 与 release.yml 校验——它只查不同步;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖)
test/ # 2680 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
test/ # 2681 个 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 @@ -730,7 +730,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
```bash
cd dsh-mneme
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
npm test # 运行 2680 个测试
npm test # 运行 2681 个测试
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
```
Expand Down
10 changes: 7 additions & 3 deletions dsh-mneme/lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2831,9 +2831,6 @@ window.__ModuleLoader__.load({
// ④ 关系图谱 the same ego graph the old graph-only tab drew
function EntityPanel({ t, focusEntity, onJumpMemory }) {
const [entities, setEntities] = useState(null);
// #512:这条记忆的归因——窗口级(哪一段原文蒸出来的)与行级(哪一轮铸造的)。
// null = 还没读到(或读失败),两者要分得开,不能被当成「没有归因」。
const [provenance, setProvenance] = useState(null); // null = still loading
const [railQuery, setRailQuery] = useState("");
const [entityName, setEntityName] = useState(focusEntity || "");
const [depth, setDepth] = useState(1);
Expand Down Expand Up @@ -6615,6 +6612,13 @@ window.__ModuleLoader__.load({
const [saveError, setSaveError] = useState(false);
const [copied, setCopied] = useState(false);
const [entities, setEntities] = useState(null);
// #512:这条记忆的归因——窗口级(哪一段原文蒸出来的)与行级(哪一轮铸造的)。
// null = 还没读到(或读失败),两者要分得开,不能被当成「没有归因」。
// 这个 state 必须住在**读它的那个组件**里:加归因区块时它曾被误放进
// EntityPanel(同文件的上一个组件),于是 MemoryDrawer 的 effect 与渲染块
// 引用到一个不存在的绑定,一开抽屉就 ReferenceError 把整个 renderer 带走
// (issue #553)。同名 state 落在隔壁组件里,静态扫描与 review 都不容易看出来。
const [provenance, setProvenance] = useState(null); // null = still loading
// 整包退出的两段:先干跑清点(点按钮那一发),再确认物删。撤是**物理删除**不是归档
// (归档行仍占 share_external_id 的 UNIQUE 位,撤了会导不回来,理由在
// src/share-import.js 的 deleteSharePack),所以这里比删除那条路多一站清点。
Expand Down
73 changes: 73 additions & 0 deletions dsh-mneme/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2703,3 +2703,76 @@ test("entity graph surfaces the passive-confirmation review surface (issue #24)"
assert.ok(clientSource.includes("if (boostedOpen) loadBoosted();"),
"the review surface must not fetch while collapsed");
});

// issue #553:`MemoryDrawer` 用了 `provenance` / `setProvenance`,而那句 useState 却落在
// 同文件的上一个组件 `EntityPanel` 里——于是打开任何一条记忆详情都会在渲染期抛
// `ReferenceError: provenance is not defined`,整个 renderer 被带走(DSH 直接重启)。
//
// 这个形状不是笔误级别的,它躲得过三样东西:
// · 眼睛——两个组件隔了三千多行,同名的 useState 看起来完全正常;
// · 语法检查——`provenance` 是合法标识符,只是不在作用域里;
// · 现有用例——它们读的是源码字符串,从不执行组件。
// 所以这里不锁某一个名字,而是把**每个组件里「调了 setter 却没有声明」**这件事整体扫一遍,
// 才对得起「别再有人把 state 放进隔壁组件」这个意图。
test("every client component declares the setters it calls (issue #553)", () => {
// 浏览器/宿主全局,不是 React state,不该被要求声明
const GLOBALS = new Set([
"setTimeout", "setInterval", "setImmediate", "setAnimationFrame",
"setState" // 子组件把 setter 当参数传下去时,形参名会命中,由 params 分支放行
]);

const lines = clientSource.split("\n");
// 组件 = 缩进 4 空格的 function 声明(本文件所有 UI 组件都是这个形状)。
// 排除自身名字就叫 `setXxx` 的——那是普通的 setter 函数定义,不是组件
// (`openLibrary` / `setOverlayOpen` 就是一对模块级函数,后者调用自身是递归)。
const comps = [];
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(/^ function ([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*\{/);
if (!m || /^set[A-Z]/.test(m[1])) continue;
let end = lines.length;
for (let j = i + 1; j < lines.length; j++) {
if (/^ function /.test(lines[j]) || /^\s{0,3}(const|let|export|class) /.test(lines[j])) {
end = j;
break;
}
}
comps.push({ name: m[1], params: m[2], start: i + 1, text: lines.slice(i, end).join("\n") });
}
assert.ok(comps.length > 50, `expected the component scan to find many components, got ${comps.length}`);

const offenders = [];
// 模块作用域里已经存在同名绑定时不算越界(`openLibrary` 调 `setOverlayOpen` 就是这类:
// 后者是模块级的普通函数,不是 React setter,而且函数声明会提升,先调后声明也成立)。
// 整个 bundle 包在 `__ModuleLoader__.load({...})` 里,所以「模块顶层」= 缩进正好 4 空格。
const moduleScope = new Set();
for (const l of lines) {
if (!/^ \S/.test(l)) continue;
const f = l.match(/^ function\s+([A-Za-z_$][\w$]*)/);
if (f) moduleScope.add(f[1]);
const c = l.match(/^ (?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/);
if (c) moduleScope.add(c[1]);
}

for (const c of comps) {
const declared = new Set();
// const [x, setX] = ... / const setX = ...
for (const m of c.text.matchAll(/const\s*\[[^\]]*?,\s*(set[A-Za-z_$][\w$]*)\s*\]\s*=/g)) declared.add(m[1]);
for (const m of c.text.matchAll(/const\s+(set[A-Za-z_$][\w$]*)\s*=/g)) declared.add(m[1]);
// 形参:组件自身的、以及体内嵌套函数/箭头函数的形参——
// setter 经常这样往下传(`routeSelects(providerKey, modelKey, testState, setTestState, …)`)。
for (const m of c.params.matchAll(/(set[A-Za-z_$][\w$]*)/g)) declared.add(m[1]);
for (const m of c.text.matchAll(/\(([^()]*)\)\s*(?:=>|\{)/g)) {
for (const p of m[1].matchAll(/set[A-Za-z_$][\w$]*/g)) declared.add(p[0]);
}
// 调用点:setXxx( ——排除 obj.setXxx(
for (const m of c.text.matchAll(/(?<![.\w$])(set[A-Z][A-Za-z0-9_$]*)\s*\(/g)) {
const name = m[1];
if (GLOBALS.has(name) || declared.has(name) || moduleScope.has(name)) continue;
offenders.push(`${c.name} (line ${c.start}) calls ${name} without declaring it`);
}
}

assert.deepEqual(offenders, [],
"a setter must be declared in the component that calls it — a state hook placed in the "
+ "neighbouring component compiles fine but throws ReferenceError at render time (#553)");
});
Loading