@modusensus/dsh-mneme 0.10.1 → 0.11.0

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.
Files changed (75) hide show
  1. package/README.md +10 -3
  2. package/bin/dsh-mneme-choro.mjs +254 -0
  3. package/docs/CONFIGURATION.md +8 -1
  4. package/lib/api-standalone.js +17 -21
  5. package/lib/api.js +247 -11
  6. package/lib/bootstrap-http.js +105 -0
  7. package/lib/bootstrap.js +5 -1
  8. package/lib/choro-command.js +104 -0
  9. package/lib/client.js +1028 -102
  10. package/lib/config-keys.js +144 -0
  11. package/lib/config.js +28 -0
  12. package/lib/index.js +50 -4
  13. package/lib/inject.js +15 -3
  14. package/lib/lang.js +54 -0
  15. package/lib/quality-filter.js +48 -12
  16. package/lib/service.js +3 -0
  17. package/lib/settings.js +8 -1
  18. package/lib/store.js +502 -0
  19. package/lib/tools.js +127 -1
  20. package/lib/wiki/inject.js +164 -0
  21. package/lib/wiki/partition.js +200 -0
  22. package/lib/wiki/plan.js +214 -0
  23. package/lib/wiki/queries.js +220 -0
  24. package/lib/wiki/render.js +187 -0
  25. package/lib/wiki/scan.js +371 -0
  26. package/lib/wiki/t0.js +463 -0
  27. package/lib/wiki/t1.js +496 -0
  28. package/lib/write-admission.js +5 -3
  29. package/package.json +4 -2
  30. package/src/api-standalone.js +17 -21
  31. package/src/api.js +247 -11
  32. package/src/bootstrap-http.js +105 -0
  33. package/src/bootstrap.js +5 -1
  34. package/src/choro-command.js +104 -0
  35. package/src/config-keys.js +144 -0
  36. package/src/config.js +28 -0
  37. package/src/index.js +50 -4
  38. package/src/inject.js +15 -3
  39. package/src/lang.js +54 -0
  40. package/src/quality-filter.js +48 -12
  41. package/src/service.js +3 -0
  42. package/src/settings.js +8 -1
  43. package/src/store.js +502 -0
  44. package/src/tools.js +127 -1
  45. package/src/wiki/inject.js +164 -0
  46. package/src/wiki/partition.js +200 -0
  47. package/src/wiki/plan.js +214 -0
  48. package/src/wiki/queries.js +220 -0
  49. package/src/wiki/render.js +187 -0
  50. package/src/wiki/scan.js +371 -0
  51. package/src/wiki/t0.js +463 -0
  52. package/src/wiki/t1.js +496 -0
  53. package/src/write-admission.js +5 -3
  54. package/test/api-routes-gaps.test.js +131 -1
  55. package/test/api.test.js +7 -1
  56. package/test/bootstrap.test.js +8 -0
  57. package/test/choro-command.test.js +218 -0
  58. package/test/client.test.js +655 -19
  59. package/test/config-keys-routes.test.js +233 -0
  60. package/test/contrast.test.js +14 -7
  61. package/test/inject.test.js +51 -0
  62. package/test/llm-audit.test.js +29 -1
  63. package/test/quality-backfill.test.js +290 -0
  64. package/test/quality-filter.test.js +113 -4
  65. package/test/service.test.js +1 -1
  66. package/test/wiki-api.test.js +153 -0
  67. package/test/wiki-inject.test.js +265 -0
  68. package/test/wiki-partition.test.js +112 -0
  69. package/test/wiki-plan.test.js +80 -0
  70. package/test/wiki-queries.test.js +187 -0
  71. package/test/wiki-scan.test.js +208 -0
  72. package/test/wiki-store.test.js +190 -0
  73. package/test/wiki-t0.test.js +380 -0
  74. package/test/wiki-t1.test.js +235 -0
  75. package/test/wiki-tools.test.js +121 -0
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
6
6
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
7
7
  [![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
8
- [![tests](https://img.shields.io/badge/tests-2681%20passed-success)](https://github.com/slow-stack/mneme)
8
+ [![tests](https://img.shields.io/badge/tests-2808%20passed-success)](https://github.com/slow-stack/mneme)
9
9
  [![CI](https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml)](https://github.com/slow-stack/mneme/actions)
10
10
  [![node](https://img.shields.io/badge/node-22%2B-blue)](https://nodejs.org)
11
11
  [![npm downloads](https://img.shields.io/npm/d18m/@modusensus/dsh-mneme.svg?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
@@ -247,6 +247,12 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
247
247
 
248
248
  > 只读端点,与 list/search/semantic 一样在设置 `apiToken` 后仍保持开放。
249
249
 
250
+ 沉淀曲线(面板状态页「8 周沉淀」图的数据源):
251
+
252
+ - `GET /api/dsh-mneme/semantic/consolidation-stats?days=` — 近 N 天(缺省 56,钳 1–365)按天返回三条序列:`added`(新建记忆,排除外来导入行)、`merged`(当天被巩固掉的**去重记录数**,取自 `receipt_chain` 里 `kind∈(merge,update)` 且 `verdict='live'`)、`archived`(当天进入归档面的条数),另附三个窗口合计。
253
+
254
+ `merged` 刻意按去重 `record_id` 数而不是行数:同一条记录被多轮巩固重复记账是常态(活库实测 2412 行只对应 1796 个记录),按行数会把「又记了一遍」算成「又沉淀了一条」。新增与合并背离才是这张图要看的东西——2026-10-25–10-31 那段新增照旧、合并归零,只看新增或只看总量都发现不了。
255
+
250
256
  ### 记忆库主区视图与记忆图谱 🕸️(v0.5.0)
251
257
 
252
258
  记忆功能从侧边栏抽屉收进**主内容区全宽 tab**(conversation.view 插槽,与「对话 / Trajectory」并列),侧边栏「记忆」入口点击后直接激活该 tab。页面顶部子 tab 行切换三个子视图:
@@ -286,6 +292,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
286
292
  | **v0.9.5**(2026-10-30) | 记忆能安全地递给别人,也能整包收回来:**分享包 `mneme.share/v1`**(`dsh-mneme share` / `receive` / `undo-share`,面板「⋯」里同一批三件事)只含活跃行,scope 与 home 前缀不进包,命中密钥 / PII 的行整条不进;出包先给报告再给文件,超 3 MiB 水位回 413、按 `--type` / `--tag` 分批;入包一条不合格整包拒、干跑为默认、按 `share_external_id` 幂等,整包退出走物删。「本机巩固不许改写别人导入的原文」由单一谓词 `isImportedRow` 守在汇流口与九个行池上。同版给记忆来源加了读出口 `GET /memory/provenance`(#421,@z2Ace0107 实现;409 = 用户关了、503 = 装配坏了),修掉同标题重复入库(#505)、扫描谎报「没东西可扫」(#515)、五处未围栏的巩固 prompt(#501)与 dream 合并护栏整类误拦(#521),面板新增设置页分组目录(#518) |
287
293
  | **v0.10.0**(2026-11-02) | 归因与闸门——给已经发生的事补上它自己的身份:**蒸馏有了运行身份**(窗口级 `distill_runs` 记 `ok`/`degraded`/`noop`/`failed`/`aborted` 与 `audit_id`,行级 `memory_mints` 记「这条记忆出生自哪一轮巩固」;历史行留空**不猜**,失败轮也真记一行),此前归因只到「会话区间」——那是输入的坐标不是运行的身份;**溯源补上反方向** `GET /memory/provenance?sessionId=`(两个主键互斥,`limit` 越界 400 不静默截断,撞上限带 `truncated: true`);**会话级无痕闸门** `sessionGateEnabled`(指定会话与子代理不进不出,其余会话照常;子代理判据只认宿主委派标记,`parentSession` 不是判据——本机 641 个会话头实测会误判 3 个 fork 主会话);**防谄媚补到写入面**(蒸馏与总览加「立场纪律」四条与「叙述纪律」,排在与时间换算的指令之前)。另修 `differentiate` 正反馈闭环(注记改落 `differentiated_pairs` 账本、正文一个字节都改不了,实测单条曾堆 19 段)、sleep 冲突相位的输入侧预算与续排(分批后不再隔夜才轮到)、面板「最近巩固」区分 organize 回执(#495)、面板首次有移动端与 481–519px 中间带断点(#542) |
288
294
  | **v0.10.1**(2026-11-03) | 打开记忆详情不再崩掉整个宿主:0.10.0 里点开侧边栏任意一条记忆的详情抽屉,渲染进程立刻抛 `ReferenceError: provenance is not defined`,整个宿主跟着重启、进行中的会话被打断,100% 复现(#553)。根因是两个隔了三千多行的组件——`MemoryDrawer` 的归因 effect 与渲染块在读 `provenance` / `setProvenance`,而那句 `useState` 落在了同文件**上一个**组件 `EntityPanel` 里,在那里是纯死代码(整个函数体除声明外对 `provenance` 零引用)。这个形状躲得过眼睛、语法检查与原有用例,所以补的不是「锁一个名字」的用例,而是按组件扫「调了 `setXxx(` 却没有声明」。同版带上 v0.10.0 之后合入的图谱锚定层收尾(#24):Tag 与向量两路起点接线(`normalizePath` 补回通路身份、`findEntitiesByTags` 走全等匹配并按 500 分块、反查改 UNION 三源以覆盖只看关系不建 attr 行的记忆、向量路复用同一份 query 向量并滤掉 0 分种子),以及被动确认的复核出口(`GET /semantic/graph/boosted-relations` 按「合法抬升量 > 0」列出、ego 边补 `weight`/`source`、面板默认收起的审计视角) |
295
+ | **v0.11.0**(2026-11-05) | 仓库地志(chorography)落地并接到消费面:读一个 JS/TS 仓库,把文件树与 import 图编译成模块划分与 `.chorography/` 下的 Markdown 地志页——T0 骨架全程零模型零网络、`plan.yaml` 正面控制、`CHORO:HUMAN` 保留区跨重生成逐字节存活;T1 逐模块一次 headless 会话补架构/约定/技术栈/构建四段语义正文并落可核验的 claims(三重上限、编造锚点整轮拒)。消费面三张脸:面板第二座位 `dsh-mneme-wiki`、三个只读工具、`/choro/*` 读路由,外加每轮开局一小块标题级提纲注入(`chorographyInject` = light/full/off)与只读 `/choro` 命令(过期只提示、绝不自动重跑)。`chorographyEnabled` opt-in 默认关。wiki 行与记忆行物理隔离,永不进巩固/质量过滤任何行池。另修 #573:宽态侧栏的记忆入口落在「更多」行下面、被展开的菜单盖住 |
289
296
 
290
297
  | **v0.8.x** | 作用域隔离(agent / workspace 双维 + opt-in 硬过滤)、冲突队列人工裁决、生态化(stdio MCP server / 图召回轴 / 冷启动 / 注入截断) |
291
298
 
@@ -722,7 +729,7 @@ src/
722
729
  ├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列)
723
730
  └── index.js # 插件接线
724
731
  lib/ # src 的同步分发产物(npm run sync,prepack 钩子在发布时自动跑;一致性由 scripts/check-sync.js 在 CI 与 release.yml 校验——它只查不同步;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖)
725
- test/ # 2681 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
732
+ test/ # 2808 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
726
733
  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 运行时清单
727
734
  ```
728
735
 
@@ -731,7 +738,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
731
738
  ```bash
732
739
  cd dsh-mneme
733
740
  npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
734
- npm test # 运行 2681 个测试
741
+ npm test # 运行 2808 个测试
735
742
  npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
736
743
  npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
737
744
  ```
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+ // bin/dsh-mneme-choro.mjs —— chorography(仓库地志)T0 确定性骨架的命令行入口。
3
+ //
4
+ // 为什么独立成 bin 而不是 cli.mjs 的子命令:CONTRIBUTING「The CLI is dependency-free
5
+ // by contract」禁止给 bin/cli.mjs 加 import,而 T0 要挂 lib/ 的扫描/划分/渲染与
6
+ // store(写 choro_* 表);命名循 dsh-mneme-serve / dsh-mneme-mcp 先例。
7
+ //
8
+ // 为什么缺省 dry-run、--apply 才写:会动仓库磁盘的东西先给数字再执行(reclaim 同款
9
+ // 纪律)。dry-run 零写入,receipt 里有「会写哪些文件、各多大」。
10
+ //
11
+ // T1(--t1)沿用同一条纪律,且以「花不花钱」再加一道:dry-run 只列候选模块与跳过
12
+ // 原因,--apply 才真开 headless 会话(吃宿主模型额度)。
13
+ //
14
+ // stdout 纪律:一次成功/dry-run 的 receipt 走 stdout(人读或 --json 机器读),错误
15
+ // 与拒绝走 stderr;exit 0 = dry-run 或成功,exit 1 = 拒绝/失败。
16
+ import fs, { readFileSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { join, resolve, dirname } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { createStore } from "../lib/store.js";
21
+ import { runChoroT0, ChoroRefusal } from "../lib/wiki/t0.js";
22
+ import { createT1, DEFAULT_T1_MODEL, T1Refusal } from "../lib/wiki/t1.js";
23
+
24
+ const BIN_NAME = "dsh-mneme-choro";
25
+ const PKG = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"));
26
+
27
+ const USAGE = `${BIN_NAME} — 仓库地志(chorography):T0 确定性骨架 + T1 语义生成轮
28
+
29
+ 用法:
30
+ ${BIN_NAME} <repo-path> [选项]
31
+
32
+ 选项:
33
+ --apply 实际写入 <repo>/.chorography/ 并落库(缺省 dry-run,只报数字)
34
+ --t1 T1 语义生成轮:逐模块一轮 headless 会话补四段语义正文 + claims。
35
+ 缺省 dry-run 只列候选;--apply 才开会话(花宿主模型额度)
36
+ --module <slug> 只跑一个模块(T1)
37
+ --model <p/m> T1 模型路由(缺省 xdpool/deepseek-v4.1-flash)
38
+ --timeout <秒> T1 单模块墙钟上限(缺省 240s)
39
+ --retry-failed 把 failed 页拉进来重试(T1;成功后计数清零)
40
+ --dsh-exe <路径> DeepSeek Harness 主程序定位(缺省走 DSH_EXE / 标准安装位)
41
+ --db <path> memory.db 路径(缺省 ~/.dsh/memory/memory.db,前导 ~ 会展开)
42
+ --json receipt 用机器可读 JSON 输出
43
+ -h, --help 本帮助
44
+ -V, --version 版本
45
+
46
+ T0 是确定性骨架(零模型、可离线):文件树 + import 图 → 模块划分 → 骨架页。
47
+ T1 用 headless 会话补语义段落与溯源 claims(逐模块、三重上限、失败不推进)。
48
+
49
+ 页面里 CHORO:HUMAN 标记块的内容在重生成时逐字节保留;块外手改不保留。
50
+ `;
51
+
52
+ function parseArgv(argv) {
53
+ const positionals = [];
54
+ const flags = {};
55
+ for (let i = 0; i < argv.length; i++) {
56
+ const arg = argv[i];
57
+ if (arg === "--apply") flags.apply = true;
58
+ else if (arg === "--json") flags.json = true;
59
+ else if (arg === "--t1") flags.t1 = true;
60
+ else if (arg === "--retry-failed") flags.retryFailed = true;
61
+ else if (arg === "--module" || arg === "--model" || arg === "--dsh-exe") {
62
+ const next = argv[i + 1];
63
+ if (next === undefined || next.startsWith("--")) {
64
+ throw new Error(`${arg} 需要一个参数`);
65
+ }
66
+ flags[arg.slice(2)] = next;
67
+ i++;
68
+ } else if (arg === "--timeout") {
69
+ const next = argv[i + 1];
70
+ if (next === undefined || next.startsWith("--")) {
71
+ throw new Error("--timeout 需要一个秒数");
72
+ }
73
+ // 现算一次:NaN 会一路混进 spawn 的 timeoutMs,表现成「上限不生效」而不是报错。
74
+ const secs = Number(next);
75
+ if (!Number.isFinite(secs) || secs <= 0) throw new Error(`--timeout 需要正数秒:${next}`);
76
+ flags.timeout = secs;
77
+ i++;
78
+ } else if (arg === "--db") {
79
+ const next = argv[i + 1];
80
+ if (next === undefined || next.startsWith("--")) {
81
+ throw new Error("--db 需要一个路径参数");
82
+ }
83
+ flags.db = next;
84
+ i++;
85
+ } else if (arg === "-h" || arg === "--help") flags.help = true;
86
+ else if (arg === "-V" || arg === "--version") flags.version = true;
87
+ else if (arg.startsWith("--")) throw new Error(`未知选项: ${arg}`);
88
+ else positionals.push(arg);
89
+ }
90
+ return { positionals, flags };
91
+ }
92
+
93
+ // 前导 ~ 展开,语义与 src/serve.js 同款(~ 只在开头、后面跟 / 或 \ 或结尾)。
94
+ function expandHome(path) {
95
+ const home = homedir();
96
+ return String(path).replace(/^~(?=$|[\\/])/, home);
97
+ }
98
+
99
+ function defaultDbPath() {
100
+ return expandHome("~/.dsh/memory/memory.db");
101
+ }
102
+
103
+ async function main() {
104
+ const argv = process.argv.slice(2);
105
+ let parsed;
106
+ try {
107
+ parsed = parseArgv(argv);
108
+ } catch (err) {
109
+ console.error(String(err?.message ?? err));
110
+ process.exit(1);
111
+ }
112
+ const { positionals, flags } = parsed;
113
+ if (flags.version) {
114
+ console.log(PKG.version);
115
+ return;
116
+ }
117
+ if (flags.help) {
118
+ console.log(USAGE.trimEnd());
119
+ return;
120
+ }
121
+ if (positionals.length === 0) {
122
+ console.error(USAGE.trimEnd());
123
+ console.error(`\n缺仓库路径:${BIN_NAME} <repo-path> [--apply]`);
124
+ process.exit(1);
125
+ }
126
+ if (positionals.length > 1) {
127
+ console.error(`多余的位置参数:${positionals.slice(1).join(" ")}(仓库路径只有一个,含空格请加引号)`);
128
+ process.exit(1);
129
+ }
130
+
131
+ const repoPath = resolve(positionals[0]);
132
+ const dbPath = resolve(expandHome(flags.db ?? defaultDbPath()));
133
+
134
+ let store;
135
+ try {
136
+ store = createStore(dbPath);
137
+ } catch (err) {
138
+ console.error(`打开 memory.db 失败(${dbPath}):${String(err?.message ?? err)}`);
139
+ process.exit(1);
140
+ }
141
+
142
+ try {
143
+ if (flags.t1) {
144
+ // T1 档(M2):缺省 dry-run 只列候选模块与章程预览(零会话零写入);
145
+ // --apply 才真开会话花钱。三重上限与降级表在 createT1 里。
146
+ const t1 = createT1({ store });
147
+ const receipt = await t1.runT1({
148
+ repoRoot: repoPath,
149
+ model: flags.model ?? DEFAULT_T1_MODEL,
150
+ moduleSlug: flags.module ?? null,
151
+ retryFailed: flags.retryFailed === true,
152
+ dryRun: flags.apply !== true,
153
+ dshExe: flags["dsh-exe"] ?? null,
154
+ limits: flags.timeout ? { timeoutMs: flags.timeout * 1000 } : null,
155
+ });
156
+ console.log(flags.json ? JSON.stringify(receipt, null, 2) : renderT1Receipt(receipt, { apply: flags.apply === true }));
157
+ return;
158
+ }
159
+ const receipt = runChoroT0({ repoRoot: repoPath, store, apply: flags.apply === true });
160
+ console.log(flags.json ? JSON.stringify(receipt, null, 2) : renderReceipt(receipt));
161
+ } catch (err) {
162
+ if (err instanceof ChoroRefusal || err instanceof T1Refusal) {
163
+ console.error(`拒绝运行(${err.code}):${err.message}`);
164
+ } else {
165
+ console.error(`${BIN_NAME} 失败:${err?.stack ?? String(err)}`);
166
+ }
167
+ process.exitCode = 1;
168
+ } finally {
169
+ try { store.close(); } catch { /* WAL 关闭异常不遮蔽主结果 */ }
170
+ }
171
+ if (process.exitCode) process.exit(process.exitCode);
172
+ }
173
+
174
+ function renderT1Receipt(r, { apply }) {
175
+ const lines = [];
176
+ const head = r.dryRun ? "T1 dry-run(未开任何会话;确认后加 --apply)" : "T1 已跑";
177
+ lines.push(`${head} · ${r.repoName} · 模型 ${r.model}`);
178
+ if (r.t1Unavailable) {
179
+ lines.push(`T1 整档不可用(环境级,页状态未动):${r.t1Unavailable}`);
180
+ lines.push("候选模块仍在下面,环境修好后重跑即可。");
181
+ lines.push("");
182
+ }
183
+ if (r.dryRun) {
184
+ lines.push(`将富化的模块(${r.candidates.length}):`);
185
+ for (const c of r.candidates) lines.push(` ${c.slug.padEnd(24)} ${String(c.fileCount).padStart(4)} 文件 · ${c.status}`);
186
+ lines.push(`跳过(${r.skipped.length}):`);
187
+ for (const s of r.skipped) lines.push(` ${s.slug} — ${s.reason}`);
188
+ lines.push(`三重上限:${r.limits.timeoutMs}ms 墙钟 / ${r.limits.maxToolCalls} 次工具 / ${r.limits.maxOutputBytes}B 输出`);
189
+ lines.push("");
190
+ lines.push("每个模块一轮 headless 会话,会花宿主模型额度;确认后加 --apply。");
191
+ } else {
192
+ lines.push(`富化 ${r.enriched.length} · 失败 ${r.failed.length} · 跳过 ${r.skipped.length}${r.t1Unavailable ? " · 整档中止(见上)" : ""}`);
193
+ for (const e of r.enriched) lines.push(` ✓ ${e.slug}(${e.sections.join("/")} · claims ×${e.claims})`);
194
+ for (const f of r.failed) lines.push(` ✗ ${f.slug} — ${f.reason}${f.marked ? "(连续失败达阈值,标记 failed)" : ""}`);
195
+ for (const s of r.skipped) lines.push(` · ${s.slug} — ${s.reason}`);
196
+ }
197
+ for (const w of r.warnings ?? []) lines.push(`提示:${w}`);
198
+ return lines.join("\n");
199
+ }
200
+
201
+ function renderReceipt(r) {
202
+ const lines = [];
203
+ const head = r.dryRun ? "dry-run(未写任何东西;确认后加 --apply)" : "已写入";
204
+ lines.push(`${head} · ${r.repoName} @ ${r.repoPath}`);
205
+ lines.push(`git 基线 ${r.gitBaseline} · 计划 ${r.planPath ?? "(无 plan.yaml,走默认启发式)"}`);
206
+ lines.push(
207
+ `扫描 ${r.fileCount} 个文件 → ${r.moduleCount} 个模块 → ${r.pageCount} 页` +
208
+ (r.skippedByWhitelist > 0 ? `(白名单挡掉 ${r.skippedByWhitelist} 页)` : "") +
209
+ ` · generatedAt ${r.generatedAt}${r.reusedGeneratedAt ? "(沿用上次)" : ""}`
210
+ );
211
+ if (r.skippedFiles.length > 0) {
212
+ lines.push(`跳过 ${r.skippedFiles.length} 个读不了的文件:`);
213
+ for (const s of r.skippedFiles.slice(0, 5)) lines.push(` · ${s.file} — ${s.reason}`);
214
+ if (r.skippedFiles.length > 5) lines.push(` … 共 ${r.skippedFiles.length} 个`);
215
+ }
216
+ if (r.unresolvedCount > 0) lines.push(`${r.unresolvedCount} 条相对引用未解析(别名/动态路径),未计入依赖图。`);
217
+ lines.push("");
218
+ lines.push("模块划分:");
219
+ for (const m of r.moduleSummary) {
220
+ lines.push(` ${m.slug.padEnd(28)} ${String(m.fileCount).padStart(4)} 文件 · ${m.internalEdges} 边 · ${m.externalDeps} 外部依赖 · ${m.title}`);
221
+ }
222
+ lines.push("");
223
+ if (r.dryRun) {
224
+ lines.push("将写入:");
225
+ for (const w of r.wouldWrite) lines.push(` ${w.path}(${w.bytes} B)`);
226
+ lines.push(`会保留的 CHORO:HUMAN 保留区:${r.humanBlocksSeen} 块`);
227
+ lines.push("");
228
+ lines.push("确认无误后加 --apply 执行。");
229
+ } else {
230
+ lines.push(`写入 ${r.bytes} B;保留区 ${r.humanBlocksPreserved} 块逐字节未动;choro_* 表已整仓替换。`);
231
+ }
232
+ for (const w of r.warnings) lines.push(`提示:${w}`);
233
+ return lines.join("\n");
234
+ }
235
+
236
+ // 直接运行才进 main(import 只取 parseArgv 做单测时必须安静):npm 全局安装走
237
+ // node_modules/.bin 的符号链接,argv[1] 是链接路径而非真身,所以 realpath 两侧再比。
238
+ const invokedDirectly = (() => {
239
+ if (process.argv[1] == null) return false;
240
+ try {
241
+ return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
242
+ } catch {
243
+ return false;
244
+ }
245
+ })();
246
+
247
+ if (invokedDirectly) {
248
+ main().catch((err) => {
249
+ console.error(err?.stack ?? String(err));
250
+ process.exit(1);
251
+ });
252
+ }
253
+
254
+ export { parseArgv };
@@ -12,6 +12,11 @@
12
12
  - 行为开关(布尔键)默认关的即 opt-in:不开 = 行为与该键存在之前逐字节一致。
13
13
  - 绝大多数行为开关与部分阈值/路由键在 `settings.js` 的白名单里,可在面板「功能开关」
14
14
  逐项启停(线上回滚开关);未入白名单的键(路径、词典、枚举外的杂项)只能改 bundle 配置。
15
+ - **「面板能改 / 只能改文件」不用在这里逐行标注**:面板「设置 → 连接 → 配置键覆盖面」
16
+ 那张只读卡会实时把两者交/差出来(`GET /api/dsh-mneme/config-keys`,只回键名与计数、
17
+ 不回任何值)。清单由 `src/config-keys.js` 从 schema 与白名单现算,改哪边都不会漏。
18
+ 那里还会单独列出「白名单登记了、但 schema 没给默认值」的键——这类键在面板上有输入框,
19
+ 留空时却不会进「当前生效值」。
15
20
 
16
21
  ## 全局与存储
17
22
 
@@ -162,7 +167,7 @@
162
167
  | `injectTimePrefix` | `false` | 对话开始注入一次当前时间(#34,v0.7.11 误删后随 #333 恢复) | opt-in;格式与键名与 v0.7.2 一致,存量 feature_flags 恢复即生效 |
163
168
  | `personaEnabled` | `false` | 人格段:用户自写的一小段常驻注入文本 | opt-in;只是轻量常驻文本、不引入重型后台路径,故不进 `LIGHT_MODE_OFF` |
164
169
  | `personaStrictAddressing` | `false` | 人格寻址口径。关(历史行为):会话身份没建卡、或该卡被显式关掉时,落 `default` 卡 | 开:这两种情况都不注入人格,`default` 只兜「解析不到会话身份」的无名会话。用途是宿主里跑着自带身份的外部 bot 时,别让某个人格无条件进它每一帧。默认关 = 升级零变化 |
165
- | `profileExtractEnabled` **light** | `false` | 画像提取草稿:`POST /api/dsh-mneme/profile/extract` 从活跃记忆反向写一段「助手眼中的用户」草稿(面板「用户画像」卡上的按钮) | opt-in;一次调用一轮 LLM + 读上百行记忆,故 `LIGHT_MODE_OFF` 里有它。**只出草稿**:它不写 `user_settings`,采用草稿是面板里另一次显式保存——画像正文必须是用户手写的原文。空库直接报 `no-input`、模型没按格式回报 `unparseable`,两者都不回空草稿 |
170
+ | `profileExtractEnabled` **light** | `false` | 画像提取草稿:`POST /api/dsh-mneme/profile/extract` 从活跃记忆反向写一段「助手眼中的用户」草稿(面板「Agent 眼中的你」卡上的按钮) | opt-in;一次调用一轮 LLM + 读上百行记忆,故 `LIGHT_MODE_OFF` 里有它。**只出草稿副本**:它不写 `user_settings`,面板也只把结果当副本展示、可一键复制(2026-11-04 改口径,此前「采用为画像」会直接覆盖手写画像且无撤销)——画像正文必须是用户自己手写的原文。空库直接报 `no-input`、模型没按格式回报 `unparseable`,两者都不回空草稿 |
166
171
 
167
172
  ## 巩固(autoDream)
168
173
 
@@ -310,6 +315,8 @@
310
315
  | `disableMemorySearch` | `false` | 对模型隐藏 `memory_search` 工具(v0.8.5) | 慢/轻量模型的工具往返节流;隐藏即不可调 |
311
316
  | `disableMemoryArchive` | `false` | 对模型隐藏 `memory_archive` 工具 | 同上 |
312
317
  | `organizeEnabled` | `false` | 对模型开放 `memory_organize` 工具(#231:先 dryRun 出比对报告,agent 判断后 apply 落库) | 与上面两个**反向**:那两个藏默认在场的工具,本键开默认不在场的能力。关 = 工具根本不注册(模型看不到就不会调)。dryRun→apply 是两步协议,弱模型容易漏 `run_id` 或跳过报告,所以 `LIGHT_MODE_OFF` 里有它;apply 只能引用一次真实报告、且一份报告只许落地一次 |
318
+ | `chorographyEnabled` | `false` | chorography(仓库地志,spec 见 docs/specs/)暴露闸:对模型开放三个只读工具(`choro_search` / `choro_read_page` / `choro_module_map`)+ 面板「仓库地志」浏览页 + 每轮的开局提纲注入 + `/choro` 命令 | 与 `organizeEnabled` 同款反向语义:关 = 工具根本不注册、面板页空态、注入块不渲染、命令不注册,行为与旧版逐字节一致。本键是「是否启用这个子系统」,不是「每轮付多少」——所以它仍**不进 `LIGHT_MODE_OFF`**,轻量档要压的是下面那条 `chorographyInject`。检索是关键词子串(M1);生成走 `dsh-mneme-choro` CLI(T0 确定性骨架 + `--t1` headless 语义富化) |
319
+ | `chorographyInject` | `"light"` | 开局注入档位:`light` 只列文件数最多的 6 个模块(约 8 行)、`full` 列全部模块、`off` 不注入 | 只注模块名 / 文件数 / 页状态 / 页相对路径,**不含正文**(正文由模型 `choro_read_page` 取);注入块折进既有 memory context,不新开 context(spec §7「合并成一次拼装」)。`lightMode: true` 时在消费侧压成 `off`(见 `wiki/inject.js` 的 `resolveInjectTier`)——它**不进 `LIGHT_MODE_OFF` 表**,因为那张表对平铺键一律赋 `false`,枚举值塞进去会与 schema 不符 |
313
320
  | `actionPipelineEnabled` | `false` | M.O. 只读视图的门控:做法图谱 + 对话行结算卡 | opt-in beta;只是纯 UI 入口、不引入重型后台路径,故不进 `LIGHT_MODE_OFF`。写账另有独立的执行记账键(`/mo-run`),看图 ≠ 写账 |
314
321
  | `actionRunEnabled` | `false` | M.O. 执行记账:`/mo-run` 开一次执行窗口,窗口内命令执行才可能被记成一条 run | opt-in beta;挂在热路径(每个会话事件都过一次监听),故 `LIGHT_MODE_OFF` 里有它 |
315
322
  | `actionNudgeEnabled` | `false` | M.O. 自动沉淀:每轮结束(`turn/end`)后挖一次「这个项目重复走的动作」,结果落成 `memoryDir/proposals/<项目键>/` 里的提议,等人确认 | opt-in beta;三个 M.O. 键里最重的一条——挂 `session/event` 且要**同步解压 zstd 会话日志**(本机 118 份实测 16.2s,单次预算 `budgetMs`=4s 兜底),故 `LIGHT_MODE_OFF` 里有它。**永不自动落草稿**:`materialize` 恒 false,进仓库必须是人的显式动作 |
@@ -18,6 +18,7 @@ import { randomBytes, timingSafeEqual } from "node:crypto";
18
18
  import { homedir } from "node:os";
19
19
  import { TYPES } from "./store.js";
20
20
  import { bootstrapFromDirectory } from "./bootstrap.js";
21
+ import { parseBootstrapJson, readBootstrapDir, bootstrapReply } from "./bootstrap-http.js";
21
22
  import { PACKAGE_VERSION } from "./version-check.js";
22
23
  import { parseReclaimJson, readReclaimFields, reclaimReply, RECLAIM_UNAVAILABLE } from "./reclaim-http.js";
23
24
  import { parseShareBody, shareReply, parseReceiveBody, receiveReply, parseUndoBody, undoReply } from "./share-http.js";
@@ -763,34 +764,29 @@ export function createStandaloneApi({ service, store, config = {}, logger, setti
763
764
  // --- POST /bootstrap: cold-start memories from repo files (#220) -------
764
765
  // 显式收 dir(插件没有工作区根目录概念,不猜路径);确定性解析零 LLM;
765
766
  // 幂等骑 saveWithDedupe 的 (type, title, scope) 去重 + _overwrite 刷新。
767
+ //
768
+ // 解析、校验、答复形状全走 bootstrap-http.js:#386 第 6 条给宿主内路由补了
769
+ // 第二张脸,按 #411 的纪律,两张皮只许剩鉴权与路径不同。这里刻意**不**再
770
+ // 自己 JSON.parse 一遍——真要写第二份,test/reclaim-http.test.js 那类静态锁
771
+ // 会转红,而漂移在两边都只表现为「某个错误码不一样了」。
766
772
  if (req.method === "POST" && pathname === "/bootstrap") {
767
773
  void readBody(req, res).then((text) => {
768
774
  if (text === null) return;
769
- let body;
770
- try {
771
- body = JSON.parse(text || "{}");
772
- } catch {
773
- sendJson(res, 400, { error: "invalid-json" });
774
- return;
775
- }
776
- if (body === null || typeof body !== "object" || Array.isArray(body)) {
777
- sendJson(res, 400, { error: "invalid-body" });
775
+ const parsed = parseBootstrapJson(text);
776
+ if (!parsed.ok) {
777
+ sendJson(res, 400, { error: parsed.error });
778
778
  return;
779
779
  }
780
- if (typeof body.dir !== "string" || !body.dir.trim()) {
781
- sendJson(res, 400, { error: "missing-dir" });
780
+ const dirResult = readBootstrapDir(parsed.body);
781
+ if (!dirResult.ok) {
782
+ sendJson(res, 400, { error: dirResult.error });
782
783
  return;
783
784
  }
784
- bootstrapFromDirectory({ service, dir: body.dir, logger })
785
- .then((summary) => sendJson(res, 200, summary))
786
- .catch((error) => {
787
- if (error?.code) {
788
- sendJson(res, 400, { error: error.code });
789
- return;
790
- }
791
- logger?.warn?.(`[dsh-mneme] standalone API bootstrap failed: ${String(error)}`);
792
- sendJson(res, 500, { error: "internal" });
793
- });
785
+ bootstrapReply({
786
+ bootstrapper: ({ dir }) => bootstrapFromDirectory({ service, dir, logger }),
787
+ dir: dirResult.dir,
788
+ warn: (msg) => logger?.warn?.(msg)
789
+ }).then(({ status, body }) => sendJson(res, status, body));
794
790
  });
795
791
  return;
796
792
  }