@kira905/dsh-memory-toolkit 0.0.0-stage → 0.1.1
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 +42 -0
- package/LICENSE +661 -0
- package/README.en.md +110 -0
- package/README.md +102 -2
- package/dsh-memory-local/FORK-BASE.json +24 -0
- package/dsh-memory-local/README.md +113 -0
- package/dsh-memory-local/build.mjs +101 -0
- package/dsh-memory-local/cordis.patch.yml +20 -0
- package/dsh-memory-local/deploy.mjs +306 -0
- package/dsh-memory-local/entity-status.mjs +75 -0
- package/dsh-memory-local/lib/bridge.mjs +108 -0
- package/dsh-memory-local/lib/host-adapter.js +1171 -0
- package/dsh-memory-local/lib/index.d.ts +128 -0
- package/dsh-memory-local/lib/index.js +944 -0
- package/dsh-memory-local/lib/injector.mjs +400 -0
- package/dsh-memory-local/lib/memory-read.mjs +116 -0
- package/dsh-memory-local/lib/module-gate.mjs +68 -0
- package/dsh-memory-local/lib/pointer.mjs +152 -0
- package/dsh-memory-local/lib/prod-gate.mjs +121 -0
- package/dsh-memory-local/lib/recall-log.mjs +118 -0
- package/dsh-memory-local/lib/recall.mjs +192 -0
- package/dsh-memory-local/lib/schema-migrate.mjs +365 -0
- package/dsh-memory-local/lib/user-message.mjs +32 -0
- package/dsh-memory-local/lib/vectors-read.mjs +110 -0
- package/dsh-memory-local/package.json +51 -0
- package/dsh-memory-local/restore.mjs +259 -0
- package/dsh-memory-local/rollback-injector-fix.mjs +166 -0
- package/dsh-memory-local/src/bridge.mjs +108 -0
- package/dsh-memory-local/src/host-adapter.js +1171 -0
- package/dsh-memory-local/src/index.d.ts +128 -0
- package/dsh-memory-local/src/index.js +944 -0
- package/dsh-memory-local/src/injector.mjs +400 -0
- package/dsh-memory-local/src/memory-read.mjs +116 -0
- package/dsh-memory-local/src/module-gate.mjs +68 -0
- package/dsh-memory-local/src/pointer.mjs +152 -0
- package/dsh-memory-local/src/recall-log.mjs +118 -0
- package/dsh-memory-local/src/recall.mjs +192 -0
- package/dsh-memory-local/src/schema-migrate.mjs +365 -0
- package/dsh-memory-local/src/user-message.mjs +32 -0
- package/dsh-memory-local/src/vectors-read.mjs +110 -0
- package/dsh-memory-local/test/fixtures-s2b.mjs +216 -0
- package/dsh-memory-local/test/probe-entity.mjs +170 -0
- package/dsh-memory-local/test/regression.mjs +270 -0
- package/dsh-memory-local/test/s2b.test.mjs +568 -0
- package/dsh-memory-local/test/schema-migrate.test.mjs +334 -0
- package/dsh-memory-local/test/test-adapter-converge.mjs +127 -0
- package/dsh-memory-local/test/visible-injections.test.mjs +239 -0
- package/dsh-memory-local/verify-after-restart.mjs +181 -0
- package/lib/deploy-guard.mjs +391 -0
- package/lib/guard-http.mjs +93 -0
- package/lib/pitfall-form-map.mjs +130 -0
- package/lib/pitfall-score.mjs +185 -0
- package/lib/session-file-name.mjs +160 -0
- package/lib/zstd-frames.mjs +78 -0
- package/memory-db-tools/README.md +68 -0
- package/memory-db-tools/_test/test-memory-scope.mjs +302 -0
- package/memory-db-tools/audit-analyze.mjs +352 -0
- package/memory-db-tools/audit-dump-ids.mjs +245 -0
- package/memory-db-tools/audit-export.mjs +270 -0
- package/memory-db-tools/audit-hit-probe.mjs +227 -0
- package/memory-db-tools/audit-ledger-probe.mjs +236 -0
- package/memory-db-tools/audit-persona-overlap.mjs +277 -0
- package/memory-db-tools/audit-struct-probe.mjs +213 -0
- package/memory-db-tools/audit-verify-unchanged.mjs +276 -0
- package/memory-db-tools/check-memory-db.mjs +8 -0
- package/memory-db-tools/dump-memory-db.mjs +12 -0
- package/memory-db-tools/lib/memory-scope.mjs +446 -0
- package/memory-db-tools/list-memory-summary.mjs +477 -0
- package/memory-db-tools/memory-identity-check.mjs +101 -0
- package/memory-db-tools/memory-schema-migrate.mjs +290 -0
- package/memory-db-tools/memory-scope-fixture.mjs +197 -0
- package/memory-db-tools/read-memories.mjs +26 -0
- package/memory-db-tools/read-memory-db.mjs +20 -0
- package/memory-db-tools/refine-memories.mjs +247 -0
- package/memory-db-tools/show-mem-times.mjs +22 -0
- package/memory-engine/README.md +70 -0
- package/memory-engine/bench/bench.mjs +221 -0
- package/memory-engine/cli/distill-rollback.mjs +86 -0
- package/memory-engine/cli/distill.mjs +156 -0
- package/memory-engine/cli/doc-index-rebuild.mjs +155 -0
- package/memory-engine/cli/doc-index.mjs +48 -0
- package/memory-engine/cli/domain-tags.mjs +251 -0
- package/memory-engine/cli/pitfall-hook.mjs +91 -0
- package/memory-engine/cli/recall.mjs +132 -0
- package/memory-engine/cli/selfcheck.mjs +57 -0
- package/memory-engine/cli/serve-start.mjs +247 -0
- package/memory-engine/cli/sweep.mjs +58 -0
- package/memory-engine/cli/sync.mjs +69 -0
- package/memory-engine/lib/cli-util.mjs +197 -0
- package/memory-engine/lib/config.mjs +135 -0
- package/memory-engine/lib/dirty-ledger.mjs +174 -0
- package/memory-engine/lib/distill-rollback.mjs +178 -0
- package/memory-engine/lib/distill.mjs +898 -0
- package/memory-engine/lib/doc-index-store.mjs +325 -0
- package/memory-engine/lib/doc-index.mjs +529 -0
- package/memory-engine/lib/doc-scan.mjs +271 -0
- package/memory-engine/lib/doc-summarize.mjs +305 -0
- package/memory-engine/lib/domain-tags.mjs +429 -0
- package/memory-engine/lib/embedder.mjs +242 -0
- package/memory-engine/lib/hashes.mjs +12 -0
- package/memory-engine/lib/memory-source.mjs +127 -0
- package/memory-engine/lib/pitfall-source.mjs +170 -0
- package/memory-engine/lib/policy-source.mjs +371 -0
- package/memory-engine/lib/recall.mjs +331 -0
- package/memory-engine/lib/selfcheck.mjs +131 -0
- package/memory-engine/lib/sweep.mjs +159 -0
- package/memory-engine/lib/sync.mjs +180 -0
- package/memory-engine/lib/tick-hook.mjs +218 -0
- package/memory-engine/lib/vectors-store.mjs +292 -0
- package/memory-engine/package-lock.json +1086 -0
- package/memory-engine/package.json +22 -0
- package/memory-engine/serve.mjs +171 -0
- package/memory-engine/test/01-store.test.mjs +127 -0
- package/memory-engine/test/02-sync.test.mjs +196 -0
- package/memory-engine/test/03-sweep.test.mjs +145 -0
- package/memory-engine/test/04-recall.test.mjs +214 -0
- package/memory-engine/test/05-selfcheck.test.mjs +175 -0
- package/memory-engine/test/06-doc-index.test.mjs +928 -0
- package/memory-engine/test/07-domain-tags.test.mjs +298 -0
- package/memory-engine/test/08-distill.test.mjs +686 -0
- package/memory-engine/test/09-pitfall-source.test.mjs +365 -0
- package/memory-engine/test/10-tick-hook.test.mjs +252 -0
- package/memory-engine/test/11-policy-source.test.mjs +224 -0
- package/memory-engine/test/_tickhook-worker.mjs +56 -0
- package/memory-engine/test/fixtures.mjs +205 -0
- package/memory-engine/test/harness.mjs +73 -0
- package/memory-engine/test/run.mjs +35 -0
- package/memory-engine/tools/bridge-smoke.mjs +80 -0
- package/memory-engine/tools/fetch-model.mjs +128 -0
- package/memory-engine/tools/smoke-recall.mjs +76 -0
- package/package.json +53 -4
- package/persona-memory-audit.mjs +217 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// tools/memory-db-tools/lib/memory-scope.mjs
|
|
2
|
+
//
|
|
3
|
+
// ⑤ 服务面隐私包 · §3.3 S-3「字段名定稿 + 身份绑定」的**唯一实现口径**(施工卡 ⑤-3 交付)。
|
|
4
|
+
//
|
|
5
|
+
// 口径出处(逐条对齐,改这里必须同步改方案档):
|
|
6
|
+
// · docs/服务面隐私包-方案-20260917.md §3.3(1 唯一权威 / 3 身份绑定 / 4 检索隔离 / 5 与 P2 接口)
|
|
7
|
+
// · docs/P2-记忆引擎升级-方案-20260916.md §3.8(两列 DDL / PRAGMA table_info 幂等 / visibility 索引)
|
|
8
|
+
// · docs/小秘书-家人档设计增补-2026-09-17.md §3.3(身份表 secretary-users.json 结构)
|
|
9
|
+
//
|
|
10
|
+
// 本模块是**纯逻辑 + 只读文件读取**:不开数据库连接(调用方把 DatabaseSync 实例传进来),
|
|
11
|
+
// 不写任何文件(除显式调用的 vacuumInto),不做时间/网络副作用。
|
|
12
|
+
// 这样它既能被 CLI 用,也能被将来的记忆引擎(P2 fork 版启动自检)用,还能被单测注入夹具。
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import crypto from "node:crypto";
|
|
17
|
+
|
|
18
|
+
/** 记忆表名(唯一)。 */
|
|
19
|
+
export const MEMORIES_TABLE = "memories";
|
|
20
|
+
|
|
21
|
+
/** 计量单位:加列前的既有列(实测 2026-09-20 · 生产库 6 列)。 */
|
|
22
|
+
export const LEGACY_COLUMNS = Object.freeze(["id", "text", "tags", "pinned", "created_at", "updated_at"]);
|
|
23
|
+
|
|
24
|
+
/** visibility 取值域(§3.3-1)。 */
|
|
25
|
+
export const VISIBILITY_VALUES = Object.freeze(["private_owner", "family", "public"]);
|
|
26
|
+
|
|
27
|
+
/** visibility 默认值(§3.3-1 / 家人档 §3.4「默认 private_owner」)。 */
|
|
28
|
+
export const DEFAULT_VISIBILITY = "private_owner";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* owner_user 缺省回落常量。
|
|
32
|
+
* ⚠️ §3.3-3:它**不是**硬编码人名,而是"本机 owner 的 userid"的**回落值** —— 正常取值来自身份表
|
|
33
|
+
* 里 role==='owner' 的那条;取不到身份表才回落这里,并在自检里**报一行**(见 resolveDefaultOwner)。
|
|
34
|
+
*/
|
|
35
|
+
export const FALLBACK_OWNER_USER = "LiJianXiao";
|
|
36
|
+
|
|
37
|
+
/** 身份表相对 ~ 的路径(家人档 §3.3:`.dsh/secretary-users.json`,不进任何仓)。 */
|
|
38
|
+
export const IDENTITY_TABLE_REL = Object.freeze([".dsh", "secretary-users.json"]);
|
|
39
|
+
|
|
40
|
+
/** 身份表 CLI(家人档 §3.3 规定;W1 线未落 ⇒ 本模块必须能优雅降级到直接读 JSON)。 */
|
|
41
|
+
export const IDENTITY_CLI_REL = Object.freeze(["tools", "dsh-butler", "secretary-user.mjs"]);
|
|
42
|
+
|
|
43
|
+
/** 两列定义(§3.3-1 唯一权威 / §3.8 表)。`defaultKind: 'owner'` = 默认值运行时从身份表解析。 */
|
|
44
|
+
export const NEW_COLUMNS = Object.freeze([
|
|
45
|
+
Object.freeze({
|
|
46
|
+
name: "owner_user",
|
|
47
|
+
type: "TEXT",
|
|
48
|
+
notNull: true,
|
|
49
|
+
defaultKind: "owner",
|
|
50
|
+
doc: "谁的(取值域 = 宿主账号体系的 user id)",
|
|
51
|
+
}),
|
|
52
|
+
Object.freeze({
|
|
53
|
+
name: "visibility",
|
|
54
|
+
type: "TEXT",
|
|
55
|
+
notNull: true,
|
|
56
|
+
defaultKind: "literal",
|
|
57
|
+
defaultLiteral: DEFAULT_VISIBILITY,
|
|
58
|
+
doc: "private_owner | family | public",
|
|
59
|
+
}),
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/** visibility 索引(§3.8 幂等做法 · 家人侧过滤用)。 */
|
|
63
|
+
export const VISIBILITY_INDEX = Object.freeze({
|
|
64
|
+
name: "memories_visibility",
|
|
65
|
+
sql: "CREATE INDEX IF NOT EXISTS memories_visibility ON memories (visibility, updated_at DESC)",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** 被裁决作废的旧字段名(§3.3-2:`source_user` 是 `owner_user` 的误写)。 */
|
|
69
|
+
export const DEPRECATED_FIELD = "source_user";
|
|
70
|
+
|
|
71
|
+
/** 生产记忆库绝对路径(唯一口径;`<USERPROFILE>/.dsh/memory/memory.db`)。 */
|
|
72
|
+
export function productionDbPath(env = process.env) {
|
|
73
|
+
return path.join(env.USERPROFILE || env.HOME || "", ".dsh", "memory", "memory.db");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 主目录(`--home` 缺省 = 用户主目录)。 */
|
|
77
|
+
export function dshHome(env = process.env) {
|
|
78
|
+
return path.join(env.USERPROFILE || env.HOME || "", ".dsh");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 判定两个路径是否同一个真实文件(realpath 双方再比 —— junction 场景的既有教训)。 */
|
|
82
|
+
export function sameRealPath(a, b) {
|
|
83
|
+
const r = (p) => {
|
|
84
|
+
try {
|
|
85
|
+
return fs.realpathSync.native(p).toLowerCase();
|
|
86
|
+
} catch {
|
|
87
|
+
try {
|
|
88
|
+
return path.resolve(p).toLowerCase();
|
|
89
|
+
} catch {
|
|
90
|
+
return String(p).toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
return r(a) === r(b);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 是否是生产库路径(用于"打生产必须显式 --allow-production"这道闸)。 */
|
|
98
|
+
export function isProductionDb(p, env = process.env) {
|
|
99
|
+
return sameRealPath(p, productionDbPath(env));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------- 身份表(§3.3-3)
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 从身份表文档里取 userid 列表 + owner —— **形态自适应**(2026-10-11 卡【翻译官·批4·家人档地基】新增)。
|
|
106
|
+
*
|
|
107
|
+
* 为什么必须做(**静默失真**,不是"兼容性优化"):本档 §3.3 早期设计把表写成**平面 map**
|
|
108
|
+
* (`{"<userid>": {...}}`,本文件 `parseIdentityTable` 的权威形态),而
|
|
109
|
+
* 新形态(`{version, default, users}`)是:
|
|
110
|
+
* `{version, default:"deny", users:{<userid>:{...}}}`。新表一落盘,只认平面 map 的读方会把**整表读成空**
|
|
111
|
+
* ⇒ `resolveDefaultOwner` 回落常量、`ownerBindingDiff` 把全部 owner 判成"未登记" ⇒ **判据从绿变红却不报错**。
|
|
112
|
+
* ⇒ 三种形态(平面 map / `{users:{…}}` / `[{userid,…}]`,外加 `{users:[…]}`)**统一在这里认**。
|
|
113
|
+
*
|
|
114
|
+
* 返回 `{ ok, why, shape, userids, owner }`;`ok:false` = 认不出(调用方按"不可用"处理,不判过)。
|
|
115
|
+
*/
|
|
116
|
+
export function useridsFromIdentityDoc(doc) {
|
|
117
|
+
const out = { ok: false, why: null, shape: 'unknown', userids: [], owner: null };
|
|
118
|
+
const META = new Set(['version', 'schemaVersion', 'default', 'product', 'updatedAt', 'note', '$schema']);
|
|
119
|
+
const push = (uid, rec, seen) => {
|
|
120
|
+
// 兼容"值就是 userid 字符串"的简写形态(`{"<userid>": "角色"}` / `["uid",…]`)—— 与 fork 实现同解
|
|
121
|
+
if (typeof rec === 'string' && rec.trim() && !rec.startsWith('_')) {
|
|
122
|
+
const v = rec.trim();
|
|
123
|
+
if (!META.has(v) && !seen.has(v)) { seen.add(v); out.userids.push(v); }
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const u = String(uid ?? '').trim();
|
|
127
|
+
// `_` 前缀 = 注释键约定(三种形态一视同仁;`$schema` 同理)—— 2026-10-11 卡【家人档地基】补齐本处遗漏
|
|
128
|
+
if (!u || u.startsWith('_') || u === '$schema' || seen.has(u)) return;
|
|
129
|
+
seen.add(u);
|
|
130
|
+
out.userids.push(u);
|
|
131
|
+
if (out.owner === null && rec && typeof rec === 'object' && rec.role === 'owner') out.owner = u;
|
|
132
|
+
};
|
|
133
|
+
if (arrayOfRecords(doc)) {
|
|
134
|
+
out.shape = 'array';
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
for (const r of doc) push(r?.userid ?? r?.user ?? r?.id, r, seen);
|
|
137
|
+
out.ok = true;
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
// ⚠️ **数组但元素不是记录**(`[1,2]`)⇒ 判"认不出",**绝不回落平面 map** ——
|
|
141
|
+
// 回落会把 `[1,2]` 解析成 userid `"0"`/`"1"`(数组下标),把"不可用(不判过)"悄悄变成
|
|
142
|
+
// "认出了一个不存在的用户"。2026-10-11 本卡自查抓到(单测 `身份表:数组根 ⇒ ok=false` 抓住的)。
|
|
143
|
+
if (Array.isArray(doc)) { out.why = '根节点是数组,但元素不是记录(不是 userid 列表)'; return out; }
|
|
144
|
+
if (!doc || typeof doc !== 'object') { out.why = '根节点不是对象/数组'; return out; }
|
|
145
|
+
const inner = doc.users ?? doc.members ?? null;
|
|
146
|
+
if (inner && typeof inner === 'object' && !arrayOfRecords(inner)) {
|
|
147
|
+
out.shape = 'object.users-map';
|
|
148
|
+
const seen = new Set();
|
|
149
|
+
for (const [uid, rec] of Object.entries(inner)) { if (uid.startsWith('_')) continue; push(uid, rec, seen); }
|
|
150
|
+
out.ok = true;
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
if (arrayOfRecords(inner)) {
|
|
154
|
+
out.shape = 'object.users-array';
|
|
155
|
+
const seen = new Set();
|
|
156
|
+
for (const r of inner) push(r?.userid ?? r?.user ?? r?.id, r, seen);
|
|
157
|
+
out.ok = true;
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
// 平面 map:`{"<userid>": {...}}`(`_` 前缀 = 注释键,元数据键跳过)
|
|
161
|
+
out.shape = 'flat-map';
|
|
162
|
+
const seen = new Set();
|
|
163
|
+
for (const [uid, rec] of Object.entries(doc)) {
|
|
164
|
+
if (uid.startsWith('_') || META.has(uid)) continue;
|
|
165
|
+
if (typeof rec === 'string') { push(rec, {}, seen); continue; }
|
|
166
|
+
push(uid, rec, seen);
|
|
167
|
+
} out.ok = true;
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 是不是"记录数组"(`[{userid,…}]` / `["uid",…]`)。
|
|
173
|
+
* ⚠️ **不是"只要是数组就算"**:`[1,2]` 这类(元素既非对象也非字符串)必须**不认**——
|
|
174
|
+
* 旧实现把"根节点是数组"一律拒(`ok:false`),若这里放宽成"元素随便",
|
|
175
|
+
* 就会把 `[1,2]` 从"认不出"悄悄变成"认出来但是空的"(判据由 `exit 2 不可用` 变 `差集=全部 owner`)⇒ 那是**误判**。
|
|
176
|
+
*/
|
|
177
|
+
function arrayOfRecords(v) {
|
|
178
|
+
return Array.isArray(v) && v.every((x) => typeof x === 'string' || (x !== null && typeof x === 'object' && !Array.isArray(x)));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 解析身份表 JSON(**三形态自适应**:平面 map / `{users:{…}}` / `[{userid,…}]`)。
|
|
183
|
+
* 不抛异常;返回 `{ ok, userids, owner, error }`。
|
|
184
|
+
*/
|
|
185
|
+
export function parseIdentityTable(text) {
|
|
186
|
+
let doc;
|
|
187
|
+
try {
|
|
188
|
+
doc = JSON.parse(String(text));
|
|
189
|
+
} catch (e) {
|
|
190
|
+
return { ok: false, userids: [], owner: null, error: `身份表 JSON 解析失败:${e.message}` };
|
|
191
|
+
}
|
|
192
|
+
const r = useridsFromIdentityDoc(doc);
|
|
193
|
+
if (!r.ok) return { ok: false, userids: [], owner: null, error: `身份表形态认不出(${r.why ?? r.shape})` };
|
|
194
|
+
return { ok: true, userids: r.userids, owner: r.owner, shape: r.shape, error: null };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 只读身份表文件。返回 `{ ok, path, exists, userids, owner, error }`。 */
|
|
198
|
+
export function readIdentityTable(homeOrFile, env = process.env) {
|
|
199
|
+
const file = homeOrFile || path.join(env.USERPROFILE || env.HOME || "", ...IDENTITY_TABLE_REL);
|
|
200
|
+
const isFile = /\.json$/i.test(file);
|
|
201
|
+
const p = isFile ? file : path.join(file, ...IDENTITY_TABLE_REL);
|
|
202
|
+
if (!fs.existsSync(p)) {
|
|
203
|
+
return { ok: false, path: p, exists: false, userids: [], owner: null, error: "身份表不存在(W1 线未落)" };
|
|
204
|
+
}
|
|
205
|
+
let text;
|
|
206
|
+
try {
|
|
207
|
+
text = fs.readFileSync(p, "utf8");
|
|
208
|
+
} catch (e) {
|
|
209
|
+
return { ok: false, path: p, exists: true, userids: [], owner: null, error: `身份表读不到:${e.message}` };
|
|
210
|
+
}
|
|
211
|
+
return { ok: true, path: p, exists: true, ...parseIdentityTable(text) };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 解析 `owner_user` 的默认值(§3.3-3 的取值优先级):
|
|
216
|
+
* ① 身份表里 `role === 'owner'` 的那条 ② 缺省回落常量(并在启动自检里**报一行**)。
|
|
217
|
+
* 返回 `{ ownerUser, source: 'identity-table' | 'fallback', notes:[…] }`。
|
|
218
|
+
*/
|
|
219
|
+
export function resolveDefaultOwner(identity) {
|
|
220
|
+
const notes = [];
|
|
221
|
+
if (identity && identity.ok && identity.owner) {
|
|
222
|
+
return { ownerUser: identity.owner, source: "identity-table", notes };
|
|
223
|
+
}
|
|
224
|
+
if (identity && identity.exists && identity.ok && !identity.owner) {
|
|
225
|
+
notes.push(`身份表 ${identity.path} 存在但**没有任何 role==='owner' 的条目** → 回落常量`);
|
|
226
|
+
} else if (identity && identity.error) {
|
|
227
|
+
notes.push(`身份表不可用(${identity.error}) → 回落常量`);
|
|
228
|
+
}
|
|
229
|
+
notes.push(`owner_user 默认值回落常量 '${FALLBACK_OWNER_USER}'(身份表缺位时的人工兜底,非硬编码设计)`);
|
|
230
|
+
return { ownerUser: FALLBACK_OWNER_USER, source: "fallback", notes };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------- 列与迁移(§3.8 幂等做法)
|
|
234
|
+
|
|
235
|
+
/** 读某表的列名(PRAGMA table_info;表不存在返回 [])。 */
|
|
236
|
+
export function columnsOf(db, table = MEMORIES_TABLE) {
|
|
237
|
+
try {
|
|
238
|
+
return db.prepare(`PRAGMA table_info(${table})`).all().map((r) => r.name);
|
|
239
|
+
} catch {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** 索引是否已存在。 */
|
|
245
|
+
export function hasIndex(db, name) {
|
|
246
|
+
try {
|
|
247
|
+
return db.prepare("SELECT 1 AS x FROM sqlite_master WHERE type='index' AND name=?").get(name) !== undefined;
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 单列的 `ALTER TABLE ADD COLUMN` 语句。`defaultLiteral` 由调用方按 §3.3-3 解析后传入。
|
|
255
|
+
* ⚠️ SQLite 的 ADD COLUMN 默认值**必须是常量表达式** ⇒ 动态默认值只能在 DDL 里写死为常量,
|
|
256
|
+
* "从身份表取默认值"落在这里(解析出常量再拼进 DDL),而不是数据库端函数。
|
|
257
|
+
*/
|
|
258
|
+
export function buildAddColumnSql(col, defaultLiteral, table = MEMORIES_TABLE) {
|
|
259
|
+
const parts = [`ALTER TABLE ${table} ADD COLUMN ${col.name} ${col.type}`];
|
|
260
|
+
if (col.notNull) parts.push("NOT NULL");
|
|
261
|
+
parts.push(`DEFAULT ${quoteLiteral(defaultLiteral)}`);
|
|
262
|
+
return parts.join(" ");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** SQL 字符串字面量转义(单引号翻倍)。 */
|
|
266
|
+
export function quoteLiteral(s) {
|
|
267
|
+
return `'${String(s).replace(/'/g, "''")}'`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 迁移计划(**只读**,不改库):算缺哪些列、要跑哪些语句、默认值取什么。
|
|
272
|
+
* 返回 `{ columns, missing, present, indexExists, statements, defaultOwner, notes }`。
|
|
273
|
+
*/
|
|
274
|
+
export function migrationPlan(db, { identity = null, table = MEMORIES_TABLE } = {}) {
|
|
275
|
+
const resolved = resolveDefaultOwner(identity);
|
|
276
|
+
const columns = columnsOf(db, table);
|
|
277
|
+
const missing = NEW_COLUMNS.filter((c) => !columns.includes(c.name));
|
|
278
|
+
const present = NEW_COLUMNS.filter((c) => columns.includes(c.name)).map((c) => c.name);
|
|
279
|
+
const statements = missing.map((c) =>
|
|
280
|
+
buildAddColumnSql(c, c.defaultKind === "owner" ? resolved.ownerUser : c.defaultLiteral, table),
|
|
281
|
+
);
|
|
282
|
+
const indexExists = hasIndex(db, VISIBILITY_INDEX.name);
|
|
283
|
+
if (!indexExists) statements.push(VISIBILITY_INDEX.sql);
|
|
284
|
+
return {
|
|
285
|
+
columns,
|
|
286
|
+
missing: missing.map((c) => c.name),
|
|
287
|
+
present,
|
|
288
|
+
indexExists,
|
|
289
|
+
statements,
|
|
290
|
+
defaultOwner: resolved,
|
|
291
|
+
notes: resolved.notes,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** 执行迁移(**幂等**;语句来自 migrationPlan,列已存在则天然无语句可跑)。返回实际执行的语句数组。 */
|
|
296
|
+
export function applyMigration(db, plan) {
|
|
297
|
+
const ran = [];
|
|
298
|
+
db.exec("BEGIN IMMEDIATE");
|
|
299
|
+
try {
|
|
300
|
+
for (const sql of plan.statements) {
|
|
301
|
+
db.exec(sql);
|
|
302
|
+
ran.push(sql);
|
|
303
|
+
}
|
|
304
|
+
db.exec("COMMIT");
|
|
305
|
+
} catch (e) {
|
|
306
|
+
try {
|
|
307
|
+
db.exec("ROLLBACK");
|
|
308
|
+
} catch {
|
|
309
|
+
/* rollback 失败时保留原始错误 */
|
|
310
|
+
}
|
|
311
|
+
throw e;
|
|
312
|
+
}
|
|
313
|
+
return ran;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------- 检索隔离(§3.3-4 · 不靠提示词靠 SQL)
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* **家人侧**过滤(§3.3-4 原文):`WHERE visibility IN ('family','public') OR owner_user = :self`。
|
|
320
|
+
*
|
|
321
|
+
* `options.registeredOwners`(可选):给出**身份表已登记 userid 集合**时启用**严格模式** —— 追加
|
|
322
|
+
* `AND owner_user IN (…)` 护栏,把"未登记 owner 的条"(超域条)挡在家人侧之外。理由:`visibility`
|
|
323
|
+
* 是"主人显式共享"的标记,但一条 `visibility='family'` 而 `owner_user` 未登记的条,其归属人未知,
|
|
324
|
+
* 不该因为共享标记而泄给家人;且判据 4 同时会判红。给不出身份表(不可用)⇒ **不返回任何他人条**
|
|
325
|
+
* (fail-closed:只留 `owner_user = :self`)—— 宁可家人看不到共享条,不可多看到。
|
|
326
|
+
*/
|
|
327
|
+
export function familyScopeSql(selfUser, options = {}) {
|
|
328
|
+
if (!selfUser) throw new Error("familyScopeSql 需要 selfUser(宿主 user id)");
|
|
329
|
+
const { registeredOwners = null, strict = false } = options;
|
|
330
|
+
if (Array.isArray(registeredOwners)) {
|
|
331
|
+
const uniq = [...new Set(registeredOwners.filter((x) => typeof x === "string" && x))];
|
|
332
|
+
if (uniq.length === 0) {
|
|
333
|
+
return { mode: "strict-empty", where: "owner_user = ?", params: [selfUser], note: "身份表为空/不可用 ⇒ 只返回本人条(fail-closed)" };
|
|
334
|
+
}
|
|
335
|
+
const ph = uniq.map(() => "?").join(", ");
|
|
336
|
+
return {
|
|
337
|
+
mode: "strict",
|
|
338
|
+
where: `(visibility IN ('family','public') OR owner_user = ?) AND owner_user IN (${ph})`,
|
|
339
|
+
params: [selfUser, ...uniq],
|
|
340
|
+
note: "严格模式:追加已登记 owner 护栏,挡超域条",
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
void strict;
|
|
344
|
+
return {
|
|
345
|
+
mode: "base",
|
|
346
|
+
where: "(visibility IN ('family','public') OR owner_user = ?)",
|
|
347
|
+
params: [selfUser],
|
|
348
|
+
note: "基础模式(§3.3-4 原文):未登记 owner 的 family/public 条**会**被命中 ⇒ 需配判据 4 兜底",
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** **主人侧注入层**过滤(§3.3-4:默认只注入 `owner_user = 主人` 的条)。 */
|
|
353
|
+
export function ownerScopeSql(ownerUser, options = {}) {
|
|
354
|
+
if (!ownerUser) throw new Error("ownerScopeSql 需要 ownerUser");
|
|
355
|
+
const { includeShared = false } = options;
|
|
356
|
+
if (includeShared) {
|
|
357
|
+
return { mode: "owner+shared", where: "(owner_user = ? OR visibility IN ('family','public'))", params: [ownerUser] };
|
|
358
|
+
}
|
|
359
|
+
return { mode: "owner-only", where: "owner_user = ?", params: [ownerUser] };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* 真跑一次过滤查询(夹具与实现层都用它,避免各写一份 SQL)。
|
|
364
|
+
* `options.tagLike`(可选):追加 `tags LIKE ?` —— 夹具用它把结果限定在夹具条内,
|
|
365
|
+
* 否则真实数据量一大就会被 `limit` 截断,断言会**假绿**(2026-09-20 本卡实测踩过:库内 599 条真实条
|
|
366
|
+
* 把夹具条挤出 LIMIT 500,导致"主人注入层命中主人自己的条"假红、"不注入家人条"假绿)。
|
|
367
|
+
*/
|
|
368
|
+
export function selectMemories(db, scope, { columns = "id, owner_user, visibility, tags", limit = 500, tagLike = null } = {}) {
|
|
369
|
+
const where = tagLike ? `(${scope.where}) AND tags LIKE ?` : scope.where;
|
|
370
|
+
const params = tagLike ? [...scope.params, tagLike] : scope.params;
|
|
371
|
+
const sql = `SELECT ${columns} FROM ${MEMORIES_TABLE} WHERE ${where} ORDER BY id LIMIT ${Number(limit)}`;
|
|
372
|
+
return db.prepare(sql).all(...params);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ---------------------------------------------------------------- 判据 4:身份绑定一致
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 判据 4(§5-3 / §3.3-3):`secretary-user list` 的 userid 集合 ⊇ `SELECT DISTINCT owner_user`,
|
|
379
|
+
* 差集 = 0(新 owner 一旦写库就必须在身份表里,未登记即判红)。
|
|
380
|
+
* 返回 `{ owners, registered, unregistered, ok }`。
|
|
381
|
+
*/
|
|
382
|
+
export function ownerBindingDiff(db, identity) {
|
|
383
|
+
const owners = db
|
|
384
|
+
.prepare(`SELECT DISTINCT owner_user AS u FROM ${MEMORIES_TABLE} WHERE owner_user IS NOT NULL ORDER BY owner_user`)
|
|
385
|
+
.all()
|
|
386
|
+
.map((r) => r.u);
|
|
387
|
+
const registered = identity && identity.ok ? [...identity.userids] : [];
|
|
388
|
+
const regSet = new Set(registered);
|
|
389
|
+
const unregistered = identity && identity.ok ? owners.filter((u) => !regSet.has(u)) : [...owners];
|
|
390
|
+
return {
|
|
391
|
+
owners,
|
|
392
|
+
registered,
|
|
393
|
+
unregistered,
|
|
394
|
+
ok: Boolean(identity && identity.ok) && unregistered.length === 0,
|
|
395
|
+
identityUsable: Boolean(identity && identity.ok),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------- 杂项
|
|
400
|
+
|
|
401
|
+
/** 文件 sha256(读不动时返回 null)。 */
|
|
402
|
+
export function sha256File(p) {
|
|
403
|
+
try {
|
|
404
|
+
return crypto.createHash("sha256").update(fs.readFileSync(p)).digest("hex");
|
|
405
|
+
} catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** 表行数。 */
|
|
411
|
+
export function tableCount(db, table = MEMORIES_TABLE) {
|
|
412
|
+
try {
|
|
413
|
+
return db.prepare(`SELECT COUNT(*) AS c FROM ${table}`).get().c;
|
|
414
|
+
} catch {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* `VACUUM INTO` 在线快照(**只读源库**,绝不写源库本体)。
|
|
421
|
+
* 目标已存在先删(VACUUM INTO 要求目标不存在)。
|
|
422
|
+
*/
|
|
423
|
+
export function vacuumInto(srcDbPath, destPath) {
|
|
424
|
+
const dest = path.resolve(destPath);
|
|
425
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
426
|
+
if (fs.existsSync(dest)) fs.rmSync(dest);
|
|
427
|
+
// 动态 import:本模块被纯逻辑单测引用时不强制加载 sqlite 绑定
|
|
428
|
+
return import("node:sqlite").then(({ DatabaseSync }) => {
|
|
429
|
+
const db = new DatabaseSync(srcDbPath, { readOnly: true });
|
|
430
|
+
try {
|
|
431
|
+
db.exec(`VACUUM INTO ${quoteLiteral(dest.replace(/\\/g, "/"))}`);
|
|
432
|
+
} finally {
|
|
433
|
+
db.close();
|
|
434
|
+
}
|
|
435
|
+
return dest;
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** 数据源信息(给人看的摘要)。 */
|
|
440
|
+
export function describeDb(db, table = MEMORIES_TABLE) {
|
|
441
|
+
return {
|
|
442
|
+
columns: columnsOf(db, table),
|
|
443
|
+
count: tableCount(db, table),
|
|
444
|
+
visibilityIndex: hasIndex(db, VISIBILITY_INDEX.name),
|
|
445
|
+
};
|
|
446
|
+
}
|