dsh-prime-memory 0.18.5 → 0.18.7

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.
@@ -1,281 +0,0 @@
1
- import { isTag } from './metadata-validators.js';
2
- import { callLLM, parseJsonLogged } from './llm.js';
3
- export const ROOM_CANDIDATES_KEY = 'roomCandidates';
4
- export const ROOM_REVIEW_KEY = 'roomReview';
5
- /** 解析标注器输出的逐条候选(容错:id 配对失败丢弃;isTag 校验;目录内优先,≤3)。 */
6
- export function parseRoomCandidateItems(parsed, ids, roomCatalog) {
7
- const idSet = new Set(ids);
8
- const catalogSet = new Set(roomCatalog.map((r) => r.room));
9
- const byId = new Map();
10
- for (const item of parsed) {
11
- if (!idSet.has(item.id))
12
- continue;
13
- const cleaned = [];
14
- for (const r of item.rooms) {
15
- const slug = typeof r === 'string' ? r.trim().toLowerCase() : '';
16
- if (!slug || !isTag(slug))
17
- continue;
18
- if (!cleaned.includes(slug))
19
- cleaned.push(slug);
20
- }
21
- // 目录内优先排前,其余兜底;截断到 3
22
- cleaned.sort((a, b) => Number(catalogSet.has(b)) - Number(catalogSet.has(a)));
23
- byId.set(item.id, cleaned.slice(0, 3));
24
- }
25
- return [...byId.entries()].map(([id, rooms]) => ({ id, rooms }));
26
- }
27
- /** 候选标注系统 prompt:喂现有 Room 词表(带计数),优先挂靠、少量新建兜底。 */
28
- export function roomCandidateSystemPrompt(roomCatalog) {
29
- const catalog = roomCatalog.length > 0
30
- ? roomCatalog
31
- .slice(0, 40)
32
- .map((r) => `- ${r.room}(现有 ${r.count} 条)`)
33
- .join('\n')
34
- : '(暂无现有 Room)';
35
- return ('你是记忆库的 Room 归类器。给你若干条记忆(带 id)与现有 Room 词表(带计数),' +
36
- '为每条记忆选出它最可能归属的 1-3 个 Room slug。规则:\n' +
37
- '1. **优先从现有 Room 词表里选**(保持分类收敛,避免碎片化);\n' +
38
- '2. 词表确实没有合适主题时,才提议最多 2 个新 slug(小写字母/数字/连字符);\n' +
39
- '3. 完全无法判断时返回空数组(宁缺毋滥,不硬猜)。\n' +
40
- `现有 Room 词表:\n${catalog}\n\n` +
41
- '输出 JSON 数组:[{"id":"<原id>","rooms":["slug1","slug2"]}]。只输出 JSON,不要多余文字。');
42
- }
43
- /** 候选标注器:一批孤儿记录 → 候选 Room(优先现有词表;失败返回空,调用方零改动)。 */
44
- export async function roomCandidateChunk(ctx, cfg, logger, chunk, roomCatalog) {
45
- const list = chunk.map((r, i) => `${i + 1}. id=${r.id}\n${r.content.slice(0, 300)}`).join('\n\n');
46
- try {
47
- const raw = await callLLM(ctx, cfg, {
48
- system: roomCandidateSystemPrompt(roomCatalog),
49
- user: `记忆列表:\n${list}`,
50
- maxTokens: 4096,
51
- layer: 'l1-extract',
52
- logger,
53
- });
54
- const parsed = parseJsonLogged(raw, 'Room 候选标注', logger);
55
- if (!Array.isArray(parsed))
56
- return [];
57
- const shape = parsed
58
- .map((item) => ({
59
- id: typeof item?.id === 'string' ? item.id : '',
60
- rooms: Array.isArray(item?.rooms) ? item.rooms : [],
61
- }))
62
- .filter((x) => x.id !== '');
63
- return parseRoomCandidateItems(shape, chunk.map((r) => r.id), roomCatalog);
64
- }
65
- catch (err) {
66
- logger.warn(`[memory] Room 候选标注块失败(跳过 ${chunk.length} 条,原记录未改): ${err instanceof Error ? err.message : String(err)}`);
67
- return [];
68
- }
69
- }
70
- /** 活跃孤儿的选择器:untagged + 未退场,跳过已 skipped 的。 */
71
- function selectPendingOrphans(l1, limit) {
72
- const out = [];
73
- const seen = new Set();
74
- for (let offset = 0; offset < 2000; offset += 200) {
75
- const page = l1.list({ untagged: true, retired: false, limit: 200, offset });
76
- for (const r of page.items) {
77
- if (seen.has(r.id))
78
- continue;
79
- seen.add(r.id);
80
- if (r.metadata?.[ROOM_REVIEW_KEY] === 'skipped')
81
- continue;
82
- out.push(r);
83
- if (out.length >= limit)
84
- return out;
85
- }
86
- if (page.items.length < 200)
87
- break;
88
- }
89
- return out;
90
- }
91
- export async function annotateOrphanCandidates(ctx, cfg, io, limit = 40, overrides = {}) {
92
- const selected = selectPendingOrphans(io.l1, limit);
93
- if (selected.length === 0)
94
- return { selected: 0, candidates: 0, written: 0 };
95
- // 合并词表:注册表条目排前(附 description,对 LLM 归类价值最高),封顶 120;
96
- // 无注册表 = 纯自生长聚合(计数降序)。整批共享一张词表(一次 LLM 调用)。
97
- const grown = io.l1.listRooms();
98
- const regActive = io.registry?.listActive() ?? [];
99
- const regSet = new Set(regActive.map((e) => e.slug));
100
- const merged = [
101
- ...regActive.map((e) => ({ room: e.slug, count: 0, label: e.label, description: e.description })),
102
- ...grown.filter((r) => !regSet.has(r.room)).map((r) => ({ room: r.room, count: r.count })),
103
- ].slice(0, 120);
104
- const rows = overrides.chunkAnnotator !== undefined
105
- ? await overrides.chunkAnnotator(selected, merged)
106
- : await roomCandidateChunk(ctx, cfg, io.logger, selected, merged);
107
- let written = 0;
108
- for (const { id, rooms: candidates } of rows) {
109
- if (candidates.length === 0)
110
- continue;
111
- const fresh = io.l1.getByIds([id])[0];
112
- if (!fresh)
113
- continue;
114
- const meta = { ...(fresh.metadata ?? {}) };
115
- meta[ROOM_CANDIDATES_KEY] = candidates;
116
- meta[ROOM_REVIEW_KEY] = 'pending';
117
- if (io.l1.patchMetadata(id, meta))
118
- written++;
119
- }
120
- io.logger.info(`[memory] 孤儿候选预标记:选中 ${selected.length},产候选 ${rows.length},写 pending ${written} 条(待 agent 复查)`);
121
- return { selected: selected.length, candidates: rows.length, written };
122
- }
123
- /** 待复查队列:pending 且有候选的活跃孤儿,按更新时间升序(最旧的先复查)。 */
124
- function pendingQueue(l1) {
125
- const out = [];
126
- const seen = new Set();
127
- for (let offset = 0; offset < 2000; offset += 200) {
128
- const page = l1.list({ untagged: true, retired: false, limit: 200, offset });
129
- for (const r of page.items) {
130
- if (seen.has(r.id))
131
- continue;
132
- seen.add(r.id);
133
- const meta = (r.metadata ?? {});
134
- if (meta[ROOM_REVIEW_KEY] === 'pending' && Array.isArray(meta[ROOM_CANDIDATES_KEY]))
135
- out.push(r);
136
- }
137
- if (page.items.length < 200)
138
- break;
139
- }
140
- return out.sort((a, b) => a.updatedAt - b.updatedAt);
141
- }
142
- /** merge 单飞哨兵(模块级;与 ruminate/wing-backfill 的时间错开由调用方保证)。 */
143
- let roomMergeRunning = false;
144
- /**
145
- * Room merge/rename 编排(破坏性动作,分类管理 beta.5):
146
- * - dryRun(缺省 true):只统计 affected + 前 6 条预览,**零写入**;
147
- * - 实跑:执行前备份(matching rows → rooms-merge-backups/)→ 游标重写
148
- * (写前重读-合并-写回,其余 metadata 键保全)→ 注册表 markMerged/renameSlug
149
- * → 返回受影响 (family, scene) 供调用方入队 recluster('room-merge');
150
- * - 单飞:并发 merge 直接抛错;与 relabel/ruminate 的时间错开由调用方保证。
151
- * 抛错语义:非法参数/注册表冲突直接抛(调用方转 notice)。
152
- */
153
- export async function mergeRoom(io, from, to, opts = {}) {
154
- const dryRun = opts.dryRun !== false;
155
- const cap = Math.min(Math.max(opts.cap ?? 500, 1), 2000);
156
- if (from === to)
157
- throw new Error('from 与 to 相同');
158
- if (!from.trim())
159
- throw new Error('from 不能为空');
160
- if (roomMergeRunning)
161
- throw new Error('Room merge 已在进行中(单飞)');
162
- const previewRows = io.l1.listByTagAll(from, 7);
163
- const affected = io.l1.listByTagAll(from, cap + 1).length;
164
- const preview = previewRows.slice(0, 6).map((r) => r.id);
165
- if (dryRun)
166
- return { dryRun: true, affected, preview, applied: 0, hasMore: affected > cap };
167
- if (roomMergeRunning)
168
- throw new Error('Room merge 已在进行中(单飞)');
169
- roomMergeRunning = true;
170
- try {
171
- const backup = io.l1.backupTagRecords(from);
172
- io.logger.info(`[memory] Room merge 备份:${from} → ${to},${backup.count} 条 → ${backup.file}`);
173
- let applied = 0;
174
- const hasMore = false; // 循环到收敛,单次调用内 hasMore 恒 false(重写返回值沿用结果形状)
175
- const families = new Set();
176
- const scenes = new Set();
177
- // 游标重写:上限 cap/次,hasMore 时继续(总量受 cap×轮次约束,单飞内收敛)
178
- for (;;) {
179
- const r = io.l1.rewriteTag(from, to, cap);
180
- applied += r.rewritten;
181
- for (const f of r.families)
182
- families.add(f);
183
- for (const sc of r.scenes)
184
- scenes.add(sc);
185
- if (!r.hasMore || r.rewritten === 0)
186
- break;
187
- }
188
- await io.registry.markMerged(from, to);
189
- io.l1.invalidateRooms();
190
- io.logger.info(`[memory] Room merge 完成:${from} → ${to},重写 ${applied} 条;受影响场景 ${scenes.size} 个`);
191
- return {
192
- dryRun: false,
193
- affected,
194
- preview,
195
- applied,
196
- hasMore,
197
- backupFile: backup.file,
198
- ...(families.size > 0 || scenes.size > 0 ? { families: [...families], scenes: [...scenes] } : {}),
199
- };
200
- }
201
- finally {
202
- roomMergeRunning = false;
203
- }
204
- }
205
- /** rename = merge 1:1 + 注册表改名(冲突时抛错提示走 merge)。
206
- * 归类语义:改名**不是退役**——merge 编排的 markMerged 会把旧条目置 retired,
207
- * 这里在改名后恢复其先前状态(active 条目改名后仍 active,不掉出标注词表)。 */
208
- export async function renameRoom(io, from, to, opts = {}) {
209
- const priorStatus = io.registry.bySlug?.(from)?.status ?? 'active';
210
- const r = await mergeRoom(io, from, to, opts);
211
- if (!r.dryRun && r.applied > 0) {
212
- await io.registry.renameSlug(from, to);
213
- if (priorStatus === 'active')
214
- await io.registry.setStatus?.(to, 'active');
215
- }
216
- return r;
217
- }
218
- /** 下一条待复查(供 agent 逐个过):记录摘要 + 候选 + 现有 Room 词表 + 剩余数。 */
219
- export function nextReview(l1) {
220
- const queue = pendingQueue(l1);
221
- if (queue.length === 0)
222
- return null;
223
- const r = queue[0];
224
- const meta = (r.metadata ?? {});
225
- return {
226
- id: r.id,
227
- content: r.content.slice(0, 600),
228
- type: r.type,
229
- sceneName: r.scene_name,
230
- candidates: meta[ROOM_CANDIDATES_KEY] ?? [],
231
- roomCatalog: l1.listRooms().slice(0, 40),
232
- remaining: queue.length,
233
- };
234
- }
235
- /** 待复查剩余数(面板/工具兜底信息)。 */
236
- export function pendingReviewCount(l1) {
237
- return pendingQueue(l1).length;
238
- }
239
- /**
240
- * 确认复查结果:候选(或 agent 改写的)写入 metadata.tags(**进 Room**),
241
- * 清候选、标 confirmed。**高权限门控由调用方执行**(与 memory_delete 同款)。
242
- * 返回写入的 tags;id 不存在/状态非法返回 null。
243
- */
244
- export function confirmReview(l1, id, rooms) {
245
- const found = l1.getByIds([id])[0];
246
- if (!found)
247
- return null;
248
- const meta = { ...(found.metadata ?? {}) };
249
- // 已确认/已跳过的不允许重复确认(防止 agent 循环空转)
250
- if (meta[ROOM_REVIEW_KEY] === 'confirmed')
251
- return null;
252
- const cleaned = [];
253
- for (const r of rooms) {
254
- if (typeof r !== 'string')
255
- continue;
256
- const slug = r.trim().toLowerCase();
257
- if (!slug || !isTag(slug) || cleaned.includes(slug))
258
- continue;
259
- cleaned.push(slug);
260
- }
261
- if (cleaned.length === 0)
262
- return null;
263
- meta.tags = cleaned;
264
- delete meta[ROOM_CANDIDATES_KEY];
265
- meta[ROOM_REVIEW_KEY] = 'confirmed';
266
- if (!l1.patchMetadata(id, meta))
267
- return null;
268
- l1.invalidateRooms();
269
- return { tags: cleaned };
270
- }
271
- /** 跳过:标 skipped(不再进 pending 队列)。已 confirmed 的记录不受 skip 影响(挂了 Room 的不算长尾)。 */
272
- export function skipReview(l1, id) {
273
- const found = l1.getByIds([id])[0];
274
- if (!found)
275
- return false;
276
- const meta = { ...(found.metadata ?? {}) };
277
- if (meta[ROOM_REVIEW_KEY] === 'confirmed')
278
- return false;
279
- meta[ROOM_REVIEW_KEY] = 'skipped';
280
- return l1.patchMetadata(id, meta);
281
- }
@@ -1,43 +0,0 @@
1
- import type { MemoryLogger } from '../types.js';
2
- export declare const RECALL_USAGE_FILE_VERSION: 1;
3
- export interface RecallUsageEntry {
4
- attempts: number;
5
- used: number;
6
- lastAttemptAt: number | null;
7
- lastUsedAt: number | null;
8
- }
9
- export interface RecallUsageSummary {
10
- /** 有统计的记忆条数。 */
11
- tracked: number;
12
- /** 尝试总次数(Σ attempts)。 */
13
- attempted: number;
14
- /** 使用总次数(Σ used)。 */
15
- used: number;
16
- /** 使用率(百分比整数;attempted=0 时 0)。 */
17
- rate: number;
18
- }
19
- export declare class RecallUsageStore {
20
- private readonly logger?;
21
- private readonly file;
22
- private entries;
23
- private loaded;
24
- private degraded;
25
- private degradedLogged;
26
- private writeTimer;
27
- private writeChain;
28
- constructor(dataDir: string, logger?: MemoryLogger | undefined);
29
- init(): Promise<void>;
30
- private ensureLoaded;
31
- /** 一轮召回:候选集 attempts++/lastAttemptAt=now;used 子集额外 used++/lastUsedAt=now
32
- * (used 不在候选内 = 防御路径,同样算一次尝试——用了就是被尝试过)。 */
33
- recordAttempt(candidateIds: readonly string[], usedIds: readonly string[]): void;
34
- get(id: string): RecallUsageEntry | undefined;
35
- summary(): RecallUsageSummary;
36
- /** 立即落盘(去抖窗口内的兜底;测试/停机用)。 */
37
- flush(): Promise<void>;
38
- /** 同步等待在途写(测试用;无在途写时立即返回)。 */
39
- flushSync(): void;
40
- private prune;
41
- private schedulePersist;
42
- private persist;
43
- }
@@ -1,149 +0,0 @@
1
- /**
2
- * 召回使用统计 sidecar(老化权重效果统计的数据面)。
3
- *
4
- * 口径(用户裁定):**进 topN(被注入上下文)才算被使用**;参与尝试未进 = 只有
5
- * attempts。每轮召回 recordAttempt(候选集, 注入集)——内存即时、去抖原子持久化
6
- * (召回是热路径,统计只碰内存 Map)。
7
- *
8
- * 存储:`recall-usage.json` slots 式 sidecar,与 rooms-registry/session-modes 同族
9
- * 纪律:缺失 = 合法空表;损坏/不可读 → 只读降级(内存态照常,停止回写),绝不阻塞
10
- * 召回读路径(R2 红线)。
11
- */
12
- import * as path from 'node:path';
13
- import { readJsonStrict, atomicWriteText, ensureDir } from '../util/io.js';
14
- export const RECALL_USAGE_FILE_VERSION = 1;
15
- /** 条目上限(防已退场/被取代记忆的 id 无限累积):超限按 lastAttemptAt 淘汰最旧。 */
16
- const MAX_ENTRIES = 50_000;
17
- /** 去抖持久化间隔(召回热路径;窗口内断电最多丢最近统计,不影响记忆本体)。 */
18
- const PERSIST_DEBOUNCE_MS = 5_000;
19
- export class RecallUsageStore {
20
- logger;
21
- file;
22
- entries = new Map();
23
- loaded = false;
24
- degraded;
25
- degradedLogged = false;
26
- writeTimer;
27
- writeChain = Promise.resolve();
28
- constructor(dataDir, logger) {
29
- this.logger = logger;
30
- this.file = path.join(dataDir, 'recall-usage.json');
31
- }
32
- async init() {
33
- await ensureDir(path.dirname(this.file));
34
- const read = await readJsonStrict(this.file);
35
- if (!read.ok) {
36
- if (read.reason !== 'missing') {
37
- this.degraded = `${read.reason}${'detail' in read && read.detail ? `: ${read.detail}` : ''}`;
38
- this.logger?.warn(`[memory] 召回使用统计只读降级: ${this.degraded}`);
39
- }
40
- this.loaded = true;
41
- return;
42
- }
43
- const raw = read.value.entries ?? {};
44
- for (const [id, e] of Object.entries(raw)) {
45
- if (!e || typeof e.attempts !== 'number')
46
- continue;
47
- this.entries.set(id, {
48
- attempts: e.attempts,
49
- used: typeof e.used === 'number' ? e.used : 0,
50
- lastAttemptAt: typeof e.lastAttemptAt === 'number' ? e.lastAttemptAt : null,
51
- lastUsedAt: typeof e.lastUsedAt === 'number' ? e.lastUsedAt : null,
52
- });
53
- }
54
- this.loaded = true;
55
- }
56
- ensureLoaded() {
57
- if (!this.loaded)
58
- throw new Error('召回使用统计未初始化(先 await init())');
59
- }
60
- /** 一轮召回:候选集 attempts++/lastAttemptAt=now;used 子集额外 used++/lastUsedAt=now
61
- * (used 不在候选内 = 防御路径,同样算一次尝试——用了就是被尝试过)。 */
62
- recordAttempt(candidateIds, usedIds) {
63
- this.ensureLoaded();
64
- if (candidateIds.length === 0 && usedIds.length === 0)
65
- return;
66
- const now = Date.now();
67
- const cand = new Set(candidateIds);
68
- for (const id of cand) {
69
- const e = this.entries.get(id) ?? { attempts: 0, used: 0, lastAttemptAt: null, lastUsedAt: null };
70
- e.attempts += 1;
71
- e.lastAttemptAt = now;
72
- this.entries.set(id, e);
73
- }
74
- for (const id of usedIds) {
75
- const e = this.entries.get(id) ?? { attempts: 0, used: 0, lastAttemptAt: null, lastUsedAt: null };
76
- e.used += 1;
77
- if (!cand.has(id)) {
78
- e.attempts += 1;
79
- e.lastAttemptAt = now;
80
- }
81
- e.lastUsedAt = now;
82
- this.entries.set(id, e);
83
- }
84
- this.prune();
85
- this.schedulePersist();
86
- }
87
- get(id) {
88
- this.ensureLoaded();
89
- const e = this.entries.get(id);
90
- return e ? { ...e } : undefined;
91
- }
92
- summary() {
93
- this.ensureLoaded();
94
- let attempted = 0;
95
- let used = 0;
96
- for (const e of this.entries.values()) {
97
- attempted += e.attempts;
98
- used += e.used;
99
- }
100
- return { tracked: this.entries.size, attempted, used, rate: attempted > 0 ? Math.round((used / attempted) * 100) : 0 };
101
- }
102
- /** 立即落盘(去抖窗口内的兜底;测试/停机用)。 */
103
- flush() {
104
- if (this.writeTimer) {
105
- clearTimeout(this.writeTimer);
106
- this.writeTimer = undefined;
107
- }
108
- return this.persist();
109
- }
110
- /** 同步等待在途写(测试用;无在途写时立即返回)。 */
111
- flushSync() {
112
- void this.flush();
113
- }
114
- prune() {
115
- if (this.entries.size <= MAX_ENTRIES)
116
- return;
117
- const byOldest = [...this.entries.entries()].sort((a, b) => (a[1].lastAttemptAt ?? 0) - (b[1].lastAttemptAt ?? 0));
118
- for (const [id] of byOldest.slice(0, this.entries.size - MAX_ENTRIES))
119
- this.entries.delete(id);
120
- }
121
- schedulePersist() {
122
- if (this.degraded || this.writeTimer)
123
- return;
124
- this.writeTimer = setTimeout(() => {
125
- this.writeTimer = undefined;
126
- void this.persist();
127
- }, PERSIST_DEBOUNCE_MS);
128
- this.writeTimer.unref?.(); // 不为统计写hold进程(停机即丢窗口内统计,可接受)
129
- }
130
- async persist() {
131
- if (this.degraded)
132
- return;
133
- this.writeChain = this.writeChain.then(async () => {
134
- try {
135
- await ensureDir(path.dirname(this.file));
136
- const data = { version: RECALL_USAGE_FILE_VERSION, entries: Object.fromEntries(this.entries) };
137
- await atomicWriteText(this.file, JSON.stringify(data));
138
- }
139
- catch (err) {
140
- this.degraded = err instanceof Error ? err.message : String(err);
141
- if (!this.degradedLogged) {
142
- this.degradedLogged = true;
143
- this.logger?.warn(`[memory] 召回使用统计写入失败,只读降级: ${this.degraded}`);
144
- }
145
- }
146
- });
147
- await this.writeChain;
148
- }
149
- }
@@ -1,66 +0,0 @@
1
- import type { MemoryLogger } from '../types.js';
2
- /** 版本常量(file-versions 同款集中纪律;registry 为独立 sidecar 文件)。 */
3
- export declare const ROOMS_REGISTRY_FILE_VERSION: 1;
4
- export interface RoomRegistryEntry {
5
- /** Room slug(小写字母数字连字符;与自生长 tags 同域,isTag 校验)。 */
6
- slug: string;
7
- /** 人类可读名(面板/标注器提示;可中文)。 */
8
- label?: string;
9
- /** 归类说明(喂标注器,对 LLM 归类价值最高)。 */
10
- description?: string;
11
- /** 来源:pre-registered(预注册)| grown(从自生长 slug 收编)。 */
12
- source: 'pre-registered' | 'grown';
13
- /** 合并/改名时的旧 slug(标注器与导出消歧;不参与 list({tag}) 查询)。 */
14
- aliases?: string[];
15
- status: 'active' | 'retired';
16
- createdAt: string;
17
- updatedAt: string;
18
- }
19
- export type RoomRegistryInput = Pick<RoomRegistryEntry, 'slug'> & Partial<Pick<RoomRegistryEntry, 'label' | 'description' | 'source' | 'aliases'>>;
20
- /**
21
- * 两级 Room slug(`major` 或 `major/minor`):事实源在 metadata-validators.ts
22
- * (normTags 写回闸同源放行),此处 re-export 兼容既有导入。
23
- */
24
- export { isRoomSlug } from '../metadata-validators.js';
25
- /** 大类归属:`major/minor` 取 major;平级 slug 自身即大类。 */
26
- export declare function majorOf(slug: string): string;
27
- export declare class RoomRegistryStore {
28
- private readonly logger?;
29
- private readonly file;
30
- private entries;
31
- private rev;
32
- private loaded;
33
- private degraded;
34
- private degradedLogged;
35
- private writeChain;
36
- constructor(dataDir: string, logger?: MemoryLogger | undefined);
37
- /** 载入(启动时 await 一次)。缺失 = 合法空表;损坏/不可读 = 只读降级(回退自生长目录)。 */
38
- init(): Promise<void>;
39
- private ensureLoaded;
40
- /** 全量条目(含 retired;副本返回,活引用纪律)。 */
41
- list(): RoomRegistryEntry[];
42
- /** active 条目(rooms-get 合并与标注器词表来源)。 */
43
- listActive(): RoomRegistryEntry[];
44
- /** 精确 slug 查找(含 retired;含别名命中)。 */
45
- bySlug(slug: string): RoomRegistryEntry | undefined;
46
- /** 注册(幂等:已存在——含别名命中——时原地补全 label/description,不新建)。上限 200 防刷。 */
47
- register(input: RoomRegistryInput): Promise<{
48
- entry: RoomRegistryEntry;
49
- created: boolean;
50
- }>;
51
- /** 编辑已有条目的 label/description(注册后可重编辑;未提供的字段不动;未找到=false)。 */
52
- update(slug: string, patch: {
53
- label?: string;
54
- description?: string;
55
- }): Promise<boolean>;
56
- /** 改状态(active↔retired);无变化返回 false。 */
57
- setStatus(slug: string, status: 'active' | 'retired'): Promise<boolean>;
58
- /** 改名(merge 1:1 收尾):slug 换名 + 旧 slug 进 aliases。新名与既有条目冲突时走 merge。
59
- * 目标用两级制词表(isRoomSlug)——把 standalone room 归类到 hall 下(改名 major/minor)
60
- * 走的就是这条路;isTag 会误拒带 slash 的目标。 */
61
- renameSlug(oldSlug: string, newSlug: string): Promise<boolean>;
62
- /** 合并收尾:fromSlug 条目转 retired + toSlug 进 aliases(记录重写由调用方游标执行)。 */
63
- markMerged(fromSlug: string, toSlug: string): Promise<boolean>;
64
- /** 写穿持久化(串行链;失败降级内存态并告警一次)。 */
65
- private persist;
66
- }