dsh-prime-memory 0.22.2 → 0.24.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.en.md +421 -0
- package/CHANGELOG.ja.md +416 -0
- package/CHANGELOG.ko.md +416 -0
- package/CHANGELOG.md +120 -0
- package/dist/client.js +1266 -845
- package/dist/config.d.ts +131 -1341
- package/dist/config.js +89 -4
- package/dist/conflict-service.d.ts +7 -1
- package/dist/conflict-service.js +7 -1
- package/dist/contract.d.ts +211 -0
- package/dist/domain-gate.d.ts +11 -0
- package/dist/domain-gate.js +15 -0
- package/dist/hooks/obsolete-notice.d.ts +26 -0
- package/dist/hooks/obsolete-notice.js +64 -0
- package/dist/hooks/recall.d.ts +39 -0
- package/dist/hooks/recall.js +110 -30
- package/dist/index.d.ts +1 -1127
- package/dist/index.js +52 -2
- package/dist/memory-types.d.ts +69 -0
- package/dist/memory-types.js +165 -0
- package/dist/memport/format.d.ts +6 -1
- package/dist/memport/format.js +42 -2
- package/dist/pipeline/l1.d.ts +44 -2
- package/dist/pipeline/l1.js +163 -9
- package/dist/pipeline/md-sync.d.ts +65 -0
- package/dist/pipeline/md-sync.js +456 -0
- package/dist/pipeline/ruminate.d.ts +46 -1
- package/dist/pipeline/ruminate.js +109 -0
- package/dist/pipeline/runner.d.ts +10 -0
- package/dist/pipeline/runner.js +13 -2
- package/dist/pipeline/workspace-relink.d.ts +60 -0
- package/dist/pipeline/workspace-relink.js +73 -0
- package/dist/projection/recall.d.ts +51 -13
- package/dist/projection/recall.js +47 -12
- package/dist/prompts/examples.d.ts +73 -0
- package/dist/prompts/examples.js +178 -0
- package/dist/prompts/l1-dedup.d.ts +9 -3
- package/dist/prompts/l1-dedup.js +34 -13
- package/dist/prompts/l1-extraction.d.ts +5 -3
- package/dist/prompts/l1-extraction.js +49 -4
- package/dist/settings.d.ts +8 -0
- package/dist/settings.js +16 -1
- package/dist/stats.d.ts +12 -0
- package/dist/stats.js +352 -17
- package/dist/store/embedding-source.d.ts +8 -0
- package/dist/store/embedding-source.js +39 -2
- package/dist/store/file-versions.d.ts +2 -0
- package/dist/store/file-versions.js +2 -0
- package/dist/store/graph-store.d.ts +14 -0
- package/dist/store/graph-store.js +35 -0
- package/dist/store/l1-snapshot.d.ts +26 -1
- package/dist/store/l1-snapshot.js +53 -8
- package/dist/store/l1.d.ts +94 -3
- package/dist/store/l1.js +169 -4
- package/dist/store/memory-md-archive.d.ts +84 -0
- package/dist/store/memory-md-archive.js +155 -0
- package/dist/store/memory-md-disable.d.ts +74 -0
- package/dist/store/memory-md-disable.js +302 -0
- package/dist/store/memory-md-export.d.ts +50 -0
- package/dist/store/memory-md-export.js +227 -0
- package/dist/store/memory-md-ledger.d.ts +94 -0
- package/dist/store/memory-md-ledger.js +243 -0
- package/dist/store/memory-md-orphan.d.ts +27 -0
- package/dist/store/memory-md-orphan.js +153 -0
- package/dist/store/memory-md-parse.d.ts +35 -0
- package/dist/store/memory-md-parse.js +85 -0
- package/dist/store/memory-md-run.d.ts +48 -0
- package/dist/store/memory-md-run.js +151 -0
- package/dist/store/memory-md-skeleton.d.ts +18 -0
- package/dist/store/memory-md-skeleton.js +61 -0
- package/dist/store/memory-md-sync.d.ts +59 -0
- package/dist/store/memory-md-sync.js +152 -0
- package/dist/store/memory-md-types.d.ts +243 -0
- package/dist/store/memory-md-types.js +10 -0
- package/dist/store/memory-md.d.ts +117 -0
- package/dist/store/memory-md.js +230 -0
- package/dist/store/obsolete-notices.d.ts +39 -0
- package/dist/store/obsolete-notices.js +153 -0
- package/dist/store/recall-feedback.d.ts +75 -0
- package/dist/store/recall-feedback.js +213 -0
- package/dist/store/recall-usage.d.ts +11 -0
- package/dist/store/recall-usage.js +29 -0
- package/dist/store/session-recalled.d.ts +38 -3
- package/dist/store/session-recalled.js +156 -9
- package/dist/store/sqlite.d.ts +66 -0
- package/dist/store/sqlite.js +233 -15
- package/dist/store/supersede.d.ts +1 -1
- package/dist/store/supersede.js +1 -1
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +26 -7
- package/dist/tools/slots.d.ts +2 -1
- package/dist/tools/slots.js +2 -1
- package/dist/types.d.ts +24 -7
- package/dist/types.js +10 -2
- package/dist/util/maturity.d.ts +20 -3
- package/dist/util/maturity.js +18 -1
- package/dist/util/md-name.d.ts +129 -0
- package/dist/util/md-name.js +366 -0
- package/dist/util/md-normalize.d.ts +23 -0
- package/dist/util/md-normalize.js +131 -0
- package/dist/workspace-route.d.ts +22 -0
- package/dist/workspace-route.js +66 -0
- package/dsh.plugin.json +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 孤儿投射文件的归档(P4.4)。
|
|
3
|
+
*
|
|
4
|
+
* 孤儿是怎么长出来的:投射路径 = `<type>/<name>.memory.md`,而 `name` 会变——本轮的
|
|
5
|
+
* 撞车消歧(42 条挤 17 个同名文件)、下一次的身份改名、以及记录被物理删除,都会让
|
|
6
|
+
* **磁盘上的旧路径不再属于任何记录**。它们既不报错也不碍事,只是越积越多,而人无法分辨
|
|
7
|
+
* 哪份是历史、哪份是刚被覆盖的。
|
|
8
|
+
*
|
|
9
|
+
* 只归档**有账可查**的那部分,判据是一条硬边界:
|
|
10
|
+
* - 账本里记着这个绝对路径(= 确认是我们自己写出去的),且**现在没有记录映射到它** → 孤儿,搬走;
|
|
11
|
+
* - 账本里没有 = 来路不明。那可能是人自己放进投射目录、等着被同步进来的输入,
|
|
12
|
+
* **绝对不许动**(md 同步侧对这类文件只计数不入库,同一个纪律)。
|
|
13
|
+
*
|
|
14
|
+
* 搬 ≠ 删:整批进 `<dataDir>/memories-orphans/<批次时间戳>/`,保留 `<ws|home>/<类型>/<名>` 的
|
|
15
|
+
* 相对形状。删是 cleanup 的活,这里只负责别再挡在活档案里。账本条目跟着删——文件已经搬走,
|
|
16
|
+
* 留着基线下一轮对账就会一直报「文件缺失」。
|
|
17
|
+
*/
|
|
18
|
+
import { mkdir, rename, stat } from 'node:fs/promises';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import { disablePathOf, resolveMemoriesRoot, safeJoin, targetFor } from './memory-md.js';
|
|
21
|
+
import { deriveRelPath, looksAbsolute } from './memory-md-ledger.js';
|
|
22
|
+
/** 样本条数上限:孤儿可能上千,面板不需要全量。 */
|
|
23
|
+
const SAMPLE_CAP = 20;
|
|
24
|
+
/**
|
|
25
|
+
* 扫一遍账本,把无人认领的投射文件搬进归档目录。
|
|
26
|
+
*
|
|
27
|
+
* @param deps - 见 `OrphanDeps`。
|
|
28
|
+
* @returns 报告;`dir` 在干跑时是空串(还没建批次目录)。任何单条失败只计数并留痕,绝不抛挂整轮。
|
|
29
|
+
*/
|
|
30
|
+
export async function sweepOrphanFiles(deps) {
|
|
31
|
+
const { records, cfgRoot, dataDir, ledger, logger } = deps;
|
|
32
|
+
const now = deps.now ?? Date.now;
|
|
33
|
+
const startedAt = now();
|
|
34
|
+
const report = {
|
|
35
|
+
dryRun: deps.dryRun,
|
|
36
|
+
scanned: 0,
|
|
37
|
+
claimed: 0,
|
|
38
|
+
orphans: 0,
|
|
39
|
+
moved: 0,
|
|
40
|
+
failed: 0,
|
|
41
|
+
missing: 0,
|
|
42
|
+
remaining: 0,
|
|
43
|
+
capped: false,
|
|
44
|
+
dir: '',
|
|
45
|
+
samples: [],
|
|
46
|
+
};
|
|
47
|
+
const sample = (path, reason) => {
|
|
48
|
+
if (report.samples.length < SAMPLE_CAP)
|
|
49
|
+
report.samples.push({ path, reason });
|
|
50
|
+
};
|
|
51
|
+
// 认领集:每条记录现在的路径 + 它的 `.disable` 兄弟。后者必须在集合里,否则 N11 的失效
|
|
52
|
+
// 凭证会被当成孤儿搬走——那个后缀就是“这条已被判过时”的唯一文件侧表达。
|
|
53
|
+
const claimed = new Set();
|
|
54
|
+
for (const r of records) {
|
|
55
|
+
const abs = targetFor(r, cfgRoot, dataDir, logger).absPath;
|
|
56
|
+
if (abs === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
claimed.add(abs);
|
|
59
|
+
claimed.add(disablePathOf(abs));
|
|
60
|
+
}
|
|
61
|
+
const homeRoot = resolveMemoriesRoot(cfgRoot, dataDir, logger);
|
|
62
|
+
const stamp = new Date(now()).toISOString().replace(/[:.]/g, '-');
|
|
63
|
+
const archiveBase = deps.archiveRoot ?? join(dataDir, 'memories-orphans');
|
|
64
|
+
const archiveDir = join(archiveBase, stamp);
|
|
65
|
+
// 走的是**带键**的遍历:账本的键才是绝对路径,条目里的 root/relPath 只供恢复归档形状。
|
|
66
|
+
const rows = ledger.allWithKeys();
|
|
67
|
+
// 配额只数**动作**(搬走/失败的孤儿),确认过“仍被认领”的不吃配额——与导出侧同一口径。
|
|
68
|
+
// 旧写法在这里会把配额花在“翻了一页全是认领”上,于是后面的孤儿永远扫不到,
|
|
69
|
+
// 报告却说“没孤儿”:那是把谎报写进了护栏。
|
|
70
|
+
let worked = 0;
|
|
71
|
+
for (let i = 0; i < rows.length; i++) {
|
|
72
|
+
// 两条护栏都在循环顶部查(写在末尾会被 continue 跳过,导出侧栽过)。
|
|
73
|
+
if (worked >= deps.maxFiles || now() - startedAt >= deps.timeBudgetMs) {
|
|
74
|
+
report.capped = true;
|
|
75
|
+
report.remaining = rows.length - i;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
const { path, entry: e } = rows[i];
|
|
79
|
+
report.scanned++;
|
|
80
|
+
if (path.length === 0) {
|
|
81
|
+
report.failed++;
|
|
82
|
+
worked++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (claimed.has(path)) {
|
|
86
|
+
report.claimed++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
report.orphans++;
|
|
90
|
+
if (deps.dryRun) {
|
|
91
|
+
// 干跑里“报出一个孤儿”就是本轮将要做的一个动作,所以它吃配额。
|
|
92
|
+
worked++;
|
|
93
|
+
sample(path, 'orphan');
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// 账本说有文件、盘上没有:那是“文件缺失”,对账已经在报了,这里不重复处理,
|
|
97
|
+
// 也**不删账本条目**(删了就把“人把文件移走了”这个事实一起抹掉)。
|
|
98
|
+
if ((await stat(path).catch(() => undefined)) === undefined) {
|
|
99
|
+
report.missing++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const side = e.root === homeRoot ? 'home' : 'ws';
|
|
103
|
+
// **不直接信存下来的 relPath**:它曾经被加载器污染成绝对路径(真机 1167/1241 条),
|
|
104
|
+
// 而 `safeJoin` 拦不住它——`join(归档根, 'home/C:\\Users\\…')` 的结果仍以归档根开头,
|
|
105
|
+
// 于是拼出含冒号的非法目录、75 条搬家全 ENOENT。键才是事实源:按 root+键重推,
|
|
106
|
+
// 推不出再退回存值(存值看着像绝对路径就一律不走)。
|
|
107
|
+
const rel = deriveRelPath(e.root, path) ?? (looksAbsolute(e.relPath) ? undefined : e.relPath);
|
|
108
|
+
const dest = rel === undefined ? undefined : safeJoin(archiveDir, `${side}/${rel}`);
|
|
109
|
+
if (rel === undefined || dest === undefined) {
|
|
110
|
+
// 账本被人手工编辑过(relPath 越界或 root 丢失):宁可不动,也不要借这道手续把文件写到目录外。
|
|
111
|
+
report.failed++;
|
|
112
|
+
worked++;
|
|
113
|
+
sample(path, 'unsafe-relPath');
|
|
114
|
+
logger?.warn(`[memory] 孤儿投射跳过(推不出安全的归档相对路径): ${path} → ${e.relPath}`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
119
|
+
await rename(path, await freeSlot(dest));
|
|
120
|
+
ledger.delete(path);
|
|
121
|
+
report.moved++;
|
|
122
|
+
worked++;
|
|
123
|
+
sample(path, 'moved');
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
report.failed++;
|
|
127
|
+
worked++;
|
|
128
|
+
sample(path, 'failed');
|
|
129
|
+
logger?.warn(`[memory] 孤儿投射搬家失败: ${path} - ${err instanceof Error ? err.message : String(err)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!deps.dryRun && (report.moved > 0 || report.failed > 0))
|
|
133
|
+
report.dir = archiveDir;
|
|
134
|
+
await ledger.flush();
|
|
135
|
+
return report;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 目标位已被占用时加数字尾巴。
|
|
139
|
+
*
|
|
140
|
+
* 正常不会撞(每次跑都用新的批次目录),但账本里可能有两条指向同一 relPath 的不同绝对路径
|
|
141
|
+
* (不同 workspace 根),落进同一个 `ws/<类型>/<名>` 就会叠。叠了也不能覆盖——
|
|
142
|
+
* 那些文件是彼此不同的历史版本。
|
|
143
|
+
*/
|
|
144
|
+
async function freeSlot(dest) {
|
|
145
|
+
if ((await stat(dest).catch(() => undefined)) === undefined)
|
|
146
|
+
return dest;
|
|
147
|
+
for (let n = 2; n <= 999; n++) {
|
|
148
|
+
const candidate = `${dest}.${n}`;
|
|
149
|
+
if ((await stat(candidate).catch(() => undefined)) === undefined)
|
|
150
|
+
return candidate;
|
|
151
|
+
}
|
|
152
|
+
return `${dest}.${Date.now()}`;
|
|
153
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** 解析出的文件形状(值已按渲染时的类型还原)。 */
|
|
2
|
+
export interface MdParsed {
|
|
3
|
+
/** 文件名干(= 渲染时写入的 `name` 键)。 */
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
priority: number;
|
|
7
|
+
/** ISO 串(渲染时就是 ISO;空串 = 没有)。 */
|
|
8
|
+
createdAt: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
usageScenario: string[];
|
|
11
|
+
extKey: string;
|
|
12
|
+
/** 文件里记的 record_id,只当线索。 */
|
|
13
|
+
id: string;
|
|
14
|
+
/** 文件声明的正文指纹(`hash` 键原文);空串 = 老文件没带(未签名)。 */
|
|
15
|
+
hash: string;
|
|
16
|
+
/**
|
|
17
|
+
* 声明指纹与正文实算的比对:`true` 一致、`false` 不符、`undefined` = 文件没声明(无从判)。
|
|
18
|
+
*
|
|
19
|
+
* `false` **不是**拒收理由:正文被人改过恰恰是 P2 要回灌的正常输入;它只说明「文件不是
|
|
20
|
+
* 我们上次写出去的那份字节」,交给调用方计入报告并重盖指纹。结构性拒收仍在更上面
|
|
21
|
+
* (形状不符 / 正文为空)。
|
|
22
|
+
*/
|
|
23
|
+
hashValid: boolean | undefined;
|
|
24
|
+
/** frontmatter 带 `retired: true`。 */
|
|
25
|
+
retired: boolean;
|
|
26
|
+
/** 正文(已去首尾空白)。 */
|
|
27
|
+
content: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 解析一个投射文件。
|
|
31
|
+
*
|
|
32
|
+
* @param text - 文件全文。
|
|
33
|
+
* @returns 解析结果;形状不符(无 frontmatter / 无正文 / 键全不认识)时 `undefined`。
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseMdFile(text: string): MdParsed | undefined;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* md 投射文件的解析(P2 反向同步的读侧)。
|
|
3
|
+
*
|
|
4
|
+
* 只认 `memory-md.ts` 自己渲染出来的那种 frontmatter:`key: <JSON 值>`,正文在第二个
|
|
5
|
+
* `---` 之后。**不做通用 YAML**——通用解析器会接受我们从不产出的形状,那些形状进到
|
|
6
|
+
* 回灌路径就是"看起来能读、读回来是垃圾"。解析不出来就返回 `undefined`,调用方跳过并
|
|
7
|
+
* 计数;绝不猜、绝不抛。
|
|
8
|
+
*
|
|
9
|
+
* 一条刻意的不对称:文件里的 `id` 只当**线索**,不当身份。id 每轮新铸(F1),一次合并
|
|
10
|
+
* 就换;真正认人靠账本键(绝对路径)与 `name`。
|
|
11
|
+
*
|
|
12
|
+
* 另有一条自校验:frontmatter 里的 `hash` 是投射时盖下的正文指纹(N13)。这里只**核对**它
|
|
13
|
+
* 与正文本体是否一致(`hashValid`),不替调用方决定怎么处置——那属于同步侧的裁决。
|
|
14
|
+
*/
|
|
15
|
+
import { contentHash } from './memory-md.js';
|
|
16
|
+
/** 取 frontmatter 块:首行必须是 `---`,到下一个 `---` 为止。 */
|
|
17
|
+
function splitFrontmatter(text) {
|
|
18
|
+
// BOM 与 CRLF 都要先吃掉:人在记事本里改一行就会引入 \r,而 `\r` 会让 key 匹配不上。
|
|
19
|
+
const normalized = text.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
20
|
+
const lines = normalized.split('\n');
|
|
21
|
+
if (lines.length < 2 || lines[0].trim() !== '---')
|
|
22
|
+
return undefined;
|
|
23
|
+
const end = lines.findIndex((l, i) => i > 0 && l.trim() === '---');
|
|
24
|
+
if (end < 0)
|
|
25
|
+
return undefined;
|
|
26
|
+
return { meta: lines.slice(1, end), body: lines.slice(end + 1).join('\n').trim() };
|
|
27
|
+
}
|
|
28
|
+
/** 一行 `key: <JSON>` → [key, 值];值解析失败则退回原始字符串。 */
|
|
29
|
+
function readLine(line) {
|
|
30
|
+
const m = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(line.trim());
|
|
31
|
+
if (m === null)
|
|
32
|
+
return undefined;
|
|
33
|
+
const raw = m[2];
|
|
34
|
+
if (raw.length === 0)
|
|
35
|
+
return [m[1], ''];
|
|
36
|
+
try {
|
|
37
|
+
return [m[1], JSON.parse(raw)];
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [m[1], raw];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function str(v) {
|
|
44
|
+
return typeof v === 'string' ? v : '';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 解析一个投射文件。
|
|
48
|
+
*
|
|
49
|
+
* @param text - 文件全文。
|
|
50
|
+
* @returns 解析结果;形状不符(无 frontmatter / 无正文 / 键全不认识)时 `undefined`。
|
|
51
|
+
*/
|
|
52
|
+
export function parseMdFile(text) {
|
|
53
|
+
const parts = splitFrontmatter(text);
|
|
54
|
+
if (parts === undefined)
|
|
55
|
+
return undefined;
|
|
56
|
+
const fields = new Map();
|
|
57
|
+
for (const line of parts.meta) {
|
|
58
|
+
const kv = readLine(line);
|
|
59
|
+
if (kv !== undefined)
|
|
60
|
+
fields.set(kv[0], kv[1]);
|
|
61
|
+
}
|
|
62
|
+
// 正文为空 = 这条记忆没内容了。人手工清空正文不该被回灌成"一条空记忆",
|
|
63
|
+
// 那既不是有效输入,也会让对账以为文件是权威。
|
|
64
|
+
if (parts.body.length === 0)
|
|
65
|
+
return undefined;
|
|
66
|
+
const scenario = Array.isArray(fields.get('usageScenario'))
|
|
67
|
+
? fields.get('usageScenario').filter((s) => typeof s === 'string' && s.length > 0)
|
|
68
|
+
: [];
|
|
69
|
+
const prio = Number(fields.get('priority'));
|
|
70
|
+
const declaredHash = str(fields.get('hash'));
|
|
71
|
+
return {
|
|
72
|
+
name: str(fields.get('name')),
|
|
73
|
+
type: str(fields.get('type')),
|
|
74
|
+
priority: Number.isFinite(prio) ? prio : 60,
|
|
75
|
+
createdAt: str(fields.get('createdAt')),
|
|
76
|
+
updatedAt: str(fields.get('updatedAt')),
|
|
77
|
+
usageScenario: scenario,
|
|
78
|
+
extKey: str(fields.get('extKey')),
|
|
79
|
+
id: str(fields.get('id')),
|
|
80
|
+
hash: declaredHash,
|
|
81
|
+
hashValid: declaredHash.length === 0 ? undefined : declaredHash === contentHash(parts.body),
|
|
82
|
+
retired: fields.get('retired') === true,
|
|
83
|
+
content: parts.body,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { MdRunStatus, SuffixReport } from './memory-md-types.js';
|
|
2
|
+
import type { MdLedgerStore } from './memory-md-ledger.js';
|
|
3
|
+
import type { MemoryLogger, MemoryRecord } from '../types.js';
|
|
4
|
+
/** 进度快照的形状与契约层共用一份定义(在 `memory-md-types.ts`),本模块只转发。 */
|
|
5
|
+
export type { MdRunStatus };
|
|
6
|
+
/** 只读快照副本(防调用方改到执行中的那份)。 */
|
|
7
|
+
export declare function mdRunStatus(): MdRunStatus;
|
|
8
|
+
export interface MdRunDeps {
|
|
9
|
+
/** 全表快照(含退场行);本函数按游标切片喂给两个执行器。 */
|
|
10
|
+
records: readonly MemoryRecord[];
|
|
11
|
+
cfgRoot: string | undefined;
|
|
12
|
+
dataDir: string;
|
|
13
|
+
ledger: MdLedgerStore;
|
|
14
|
+
/** 单轮护栏(与 `cfg.memExport` 同值)。 */
|
|
15
|
+
maxRecords: number;
|
|
16
|
+
timeBudgetMs: number;
|
|
17
|
+
/** 跨轮总闸:最多滚几轮 / 最多花多少毫秒。 */
|
|
18
|
+
maxPasses?: number;
|
|
19
|
+
totalBudgetMs?: number;
|
|
20
|
+
logger?: MemoryLogger;
|
|
21
|
+
/** 注入时钟(测试用)。 */
|
|
22
|
+
now?: () => number;
|
|
23
|
+
}
|
|
24
|
+
/** 滚动执行的汇总(形状与单轮报告一致,多出的字段告诉人滚了几轮)。 */
|
|
25
|
+
export interface MdRunResult {
|
|
26
|
+
root: string;
|
|
27
|
+
inWorkspace: number;
|
|
28
|
+
scanned: number;
|
|
29
|
+
written: number;
|
|
30
|
+
unchanged: number;
|
|
31
|
+
skipped: number;
|
|
32
|
+
/** 0 = 两阶段都滚到完;>0 = 总预算用尽,剩这么多条没处理,再点一次续跑。 */
|
|
33
|
+
remaining: number;
|
|
34
|
+
capped: boolean;
|
|
35
|
+
/** 实际滚了几轮。 */
|
|
36
|
+
passes: number;
|
|
37
|
+
suffix: SuffixReport;
|
|
38
|
+
appliedMaxRecords: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 滚到完:先把导出游标推到表尾,再把收敛游标推到表尾。
|
|
42
|
+
*
|
|
43
|
+
* 两阶段分开推进度,是因为收敛必须等导出把该写的文件写完——那时盘态最全,
|
|
44
|
+
* 判"盘上没这份"才准(反过来会把它误报成 `absent`)。
|
|
45
|
+
*
|
|
46
|
+
* @throws 已有任务在跑时抛(不排队,见模块头纪律 1)。
|
|
47
|
+
*/
|
|
48
|
+
export declare function runProjection(deps: MdRunDeps): Promise<MdRunResult>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* md 投影长任务的**滚动执行**与进度面。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要这一层:`exportRecords` / `convergeDisabledSuffix` 各自是"一次有界的一轮"
|
|
5
|
+
* (护栏 `maxRecords` + 墙钟 `timeBudgetMs`),设计上就假定调用方会再点一次。真机反馈
|
|
6
|
+
* 是"点三下才清干净,每次一秒多"——把"要不要继续"这个判断丢给人点是错的:任务自己知道
|
|
7
|
+
* 有没有做完(`capped`),就该自己滚到完。形状照本包既有的两个长任务(`ruminate` /
|
|
8
|
+
* `rebuild`):一个后台任务 + 一个只读状态端点,面板拿状态画进度条。
|
|
9
|
+
*
|
|
10
|
+
* 三条纪律:
|
|
11
|
+
* 1. **同一时刻只跑一个投影任务**:两个任务同时改同一批文件与同一本账,产生的差异
|
|
12
|
+
* 连对账都解释不清。第二个进来直接抛,不排队(排队会让人以为"点了没反应")。
|
|
13
|
+
* 2. **总预算兜底**:单轮的墙钟护栏防的是"一条超大记录挂死一次点击",跨轮还得有一道
|
|
14
|
+
* 总闸(`totalBudgetMs`)+ 轮数上限(`maxPasses`),否则一个病态目录能让任务永远
|
|
15
|
+
* 在滚。用尽时如实写进 `error`,人再点一次接着滚(游标语义幂等,不重复也不漏)。
|
|
16
|
+
* 3. **进度只读不阻塞**:状态是模块级一份快照,端点读它就返回;执行侧只在轮与轮的
|
|
17
|
+
* 边界更新它(不在内层循环里写,省掉上千次无意义赋值)。
|
|
18
|
+
*/
|
|
19
|
+
import { exportRecords } from './memory-md-export.js';
|
|
20
|
+
import { convergeDisabledSuffix } from './memory-md-disable.js';
|
|
21
|
+
/** 模块级唯一快照:投影是"同一时刻一个"的任务,不需要多路会话态。 */
|
|
22
|
+
const status = {
|
|
23
|
+
running: false, phase: 'idle', pass: 0, done: 0, total: 0,
|
|
24
|
+
written: 0, renamed: 0, detail: '', startedAt: '', endedAt: '',
|
|
25
|
+
};
|
|
26
|
+
/** 只读快照副本(防调用方改到执行中的那份)。 */
|
|
27
|
+
export function mdRunStatus() {
|
|
28
|
+
return { ...status };
|
|
29
|
+
}
|
|
30
|
+
function blankSuffix() {
|
|
31
|
+
return {
|
|
32
|
+
scanned: 0, renamed: 0, revived: 0, edited: 0, already: 0, both: 0, absent: 0, skipped: 0, remaining: 0, capped: false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** 让出事件循环一次:滚动期间不能独占 tick(进度端点要能挤进来应答)。 */
|
|
36
|
+
function tick() {
|
|
37
|
+
return new Promise((resolveTick) => { setImmediate(resolveTick); });
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 滚到完:先把导出游标推到表尾,再把收敛游标推到表尾。
|
|
41
|
+
*
|
|
42
|
+
* 两阶段分开推进度,是因为收敛必须等导出把该写的文件写完——那时盘态最全,
|
|
43
|
+
* 判"盘上没这份"才准(反过来会把它误报成 `absent`)。
|
|
44
|
+
*
|
|
45
|
+
* @throws 已有任务在跑时抛(不排队,见模块头纪律 1)。
|
|
46
|
+
*/
|
|
47
|
+
export async function runProjection(deps) {
|
|
48
|
+
if (status.running)
|
|
49
|
+
throw new Error('已有 md 投影任务在跑,请等它结束(进度见面板)');
|
|
50
|
+
const clock = deps.now ?? Date.now;
|
|
51
|
+
const maxPasses = deps.maxPasses ?? 12;
|
|
52
|
+
const totalBudgetMs = deps.totalBudgetMs ?? 300_000;
|
|
53
|
+
const started = clock();
|
|
54
|
+
const total = deps.records.length;
|
|
55
|
+
Object.assign(status, {
|
|
56
|
+
running: true, phase: 'export', pass: 0, done: 0, total,
|
|
57
|
+
written: 0, renamed: 0, detail: `开始投影 ${total} 条`, startedAt: new Date(started).toISOString(), endedAt: '',
|
|
58
|
+
});
|
|
59
|
+
const out = {
|
|
60
|
+
root: '', inWorkspace: 0, scanned: 0, written: 0, unchanged: 0, skipped: 0,
|
|
61
|
+
remaining: 0, capped: false, passes: 0, suffix: blankSuffix(), appliedMaxRecords: deps.maxRecords,
|
|
62
|
+
};
|
|
63
|
+
const agg = blankSuffix();
|
|
64
|
+
try {
|
|
65
|
+
// ── 阶段 A:导出游标 ──
|
|
66
|
+
let cursor = 0;
|
|
67
|
+
for (let pass = 1; cursor < total && pass <= maxPasses; pass++) {
|
|
68
|
+
status.pass = pass;
|
|
69
|
+
status.phase = 'export';
|
|
70
|
+
const rep = await exportRecords({
|
|
71
|
+
records: deps.records.slice(cursor),
|
|
72
|
+
cfgRoot: deps.cfgRoot,
|
|
73
|
+
dataDir: deps.dataDir,
|
|
74
|
+
ledger: deps.ledger,
|
|
75
|
+
maxRecords: deps.maxRecords,
|
|
76
|
+
timeBudgetMs: deps.timeBudgetMs,
|
|
77
|
+
logger: deps.logger,
|
|
78
|
+
now: deps.now,
|
|
79
|
+
});
|
|
80
|
+
out.root = rep.root;
|
|
81
|
+
out.inWorkspace += rep.inWorkspace;
|
|
82
|
+
out.scanned += rep.scanned;
|
|
83
|
+
out.written += rep.written;
|
|
84
|
+
out.unchanged += rep.unchanged;
|
|
85
|
+
out.skipped += rep.skipped;
|
|
86
|
+
status.written = out.written;
|
|
87
|
+
// `remaining` 是"这一轮的输入里没处理完的条数",所以游标 = 表长 - 剩余。
|
|
88
|
+
cursor = total - rep.remaining;
|
|
89
|
+
status.done = cursor;
|
|
90
|
+
status.detail = `导出第 ${pass} 轮:写 ${rep.written} · 未变 ${rep.unchanged} · 进度 ${cursor}/${total}`;
|
|
91
|
+
out.passes = pass;
|
|
92
|
+
out.capped = rep.capped;
|
|
93
|
+
if (clock() - started >= totalBudgetMs)
|
|
94
|
+
break;
|
|
95
|
+
await tick();
|
|
96
|
+
}
|
|
97
|
+
out.remaining = Math.max(0, total - cursor);
|
|
98
|
+
// ── 阶段 B:收敛游标(导出没滚完时也要跑:已写出的那批的后缀要先对上) ──
|
|
99
|
+
if (out.remaining === 0) {
|
|
100
|
+
let cur2 = 0;
|
|
101
|
+
for (let pass = 1; cur2 < total && pass <= maxPasses; pass++) {
|
|
102
|
+
status.pass = out.passes + pass;
|
|
103
|
+
status.phase = 'converge';
|
|
104
|
+
const s = await convergeDisabledSuffix({
|
|
105
|
+
records: deps.records.slice(cur2),
|
|
106
|
+
cfgRoot: deps.cfgRoot,
|
|
107
|
+
dataDir: deps.dataDir,
|
|
108
|
+
ledger: deps.ledger,
|
|
109
|
+
maxRecords: deps.maxRecords,
|
|
110
|
+
timeBudgetMs: deps.timeBudgetMs,
|
|
111
|
+
logger: deps.logger,
|
|
112
|
+
now: deps.now,
|
|
113
|
+
});
|
|
114
|
+
for (const k of ['scanned', 'renamed', 'revived', 'edited', 'already', 'both', 'absent', 'skipped'])
|
|
115
|
+
agg[k] += s[k];
|
|
116
|
+
agg.remaining = s.remaining;
|
|
117
|
+
agg.capped = s.capped;
|
|
118
|
+
cur2 = total - s.remaining;
|
|
119
|
+
status.renamed = agg.renamed;
|
|
120
|
+
status.done = total + cur2;
|
|
121
|
+
status.detail = `后缀对齐第 ${pass} 轮:改名 ${s.renamed} · 进度 ${cur2}/${total}`;
|
|
122
|
+
out.passes += 1;
|
|
123
|
+
out.capped = s.capped;
|
|
124
|
+
if (clock() - started >= totalBudgetMs)
|
|
125
|
+
break;
|
|
126
|
+
await tick();
|
|
127
|
+
}
|
|
128
|
+
out.remaining = Math.max(0, total - cur2);
|
|
129
|
+
}
|
|
130
|
+
out.suffix = agg;
|
|
131
|
+
status.phase = out.remaining > 0 ? 'failed' : 'done';
|
|
132
|
+
status.done = total + Math.max(0, Math.min(total, agg.scanned));
|
|
133
|
+
status.detail = out.remaining > 0
|
|
134
|
+
? `总预算用尽:还剩 ${out.remaining} 个文件,再点一次续跑`
|
|
135
|
+
: `投影完成:写 ${out.written} · 改名 ${agg.renamed} · 共 ${out.passes} 轮`;
|
|
136
|
+
if (out.remaining > 0) {
|
|
137
|
+
status.notice = `单次任务预算(${maxPasses} 轮 / ${totalBudgetMs}ms)用尽,还剩 ${out.remaining} 个文件未处理——再点一次从断点续跑`;
|
|
138
|
+
out.capped = true;
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
status.phase = 'failed';
|
|
144
|
+
status.notice = err instanceof Error ? err.message : String(err);
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
status.running = false;
|
|
149
|
+
status.endedAt = new Date(clock()).toISOString();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MemoryLogger } from '../types.js';
|
|
2
|
+
/** 每类目录下的索引文件名(不带 `.memory.md`,因此天然落在投射/扫描面之外)。 */
|
|
3
|
+
export declare const SKELETON_INDEX_FILE = "_index.md";
|
|
4
|
+
export interface SkeletonDeps {
|
|
5
|
+
/** 要补骨架的投射根(绝对路径)列表。单根必补、workspace 根按需由调用方决定。 */
|
|
6
|
+
roots: readonly string[];
|
|
7
|
+
/** 目录名(= 类型)→ 库里条数;缺省按 0 计(0 也写,否则"没目录"与"0 条"仍无法区分)。 */
|
|
8
|
+
countsByType: ReadonlyMap<string, number>;
|
|
9
|
+
/** 诊断口:任何单目录写失败只走它留痕,绝不抛挂整轮。 */
|
|
10
|
+
logger?: MemoryLogger;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 往每个投射根补齐 37 类目录及各自的 `_index.md`。
|
|
14
|
+
*
|
|
15
|
+
* @param deps - 见 `SkeletonDeps`。
|
|
16
|
+
* @returns 无(副作用为落盘)。任何单目录失败只 warn,不抛。
|
|
17
|
+
*/
|
|
18
|
+
export declare function ensureTypeSkeleton(deps: SkeletonDeps): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 类型目录骨架 + `_index.md`(pm-name-distill Phase 4 / task_4.1)。
|
|
3
|
+
*
|
|
4
|
+
* 投射根(`<dataDir>/memories/` 或 `cfg.memExport.root`)过去只有"库里出现过记录"的类型目录
|
|
5
|
+
* (实测 6 个);词表已扩到 37 类。空目录会被 git/压缩包吐掉,而"没这个目录"与"这类 0 条"
|
|
6
|
+
* 在人眼里长得一样——所以每类目录固定放一个 `_index.md`(类名 + 库里条数),目录集合从
|
|
7
|
+
* **词表真源** `memoryTypeDirs()` 派生,不再手抄。
|
|
8
|
+
*
|
|
9
|
+
* 三条纪律:
|
|
10
|
+
* 1. **幂等**:内容不变就不重写(也不刷 mtime)。投射是"库 → 文件"的旁路,重刷 mtime 会让
|
|
11
|
+
* 下一轮对账把"我们没动过"误判成"文件被改过"——与 `exportRecords` 的 mtime 纪律同口径。
|
|
12
|
+
* 2. **失败只 warn 不抛**:旁路写挂不能弄挂记忆本体(照 `resolveMemoriesRoot` / 导出侧的降级口径)。
|
|
13
|
+
* 3. **原子落盘**:临时文件 + rename,走仓内既有范式 `atomicWriteText`(带 fsync / 安全路径校验 /
|
|
14
|
+
* 失败清理);根不存在由它内部 `ensureDir` recursive 建出。
|
|
15
|
+
*
|
|
16
|
+
* `_index.md` 刻意**不是**投射文件:md-sync 只认 `.memory.md` / `.memory.md.disable` 后缀
|
|
17
|
+
* (`listProjectionFiles`),孤儿扫描只遍历账本键(`sweepOrphanFiles`)——本模块从不把
|
|
18
|
+
* `_index.md` 写进账本,故两条通路都不会碰它(防误伤用例把这两条钉成断言)。
|
|
19
|
+
*/
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { readFile } from 'node:fs/promises';
|
|
22
|
+
import { MEMORY_TYPE_LABELS, memoryTypeDirs } from '../memory-types.js';
|
|
23
|
+
import { atomicWriteText } from '../util/io.js';
|
|
24
|
+
/** 每类目录下的索引文件名(不带 `.memory.md`,因此天然落在投射/扫描面之外)。 */
|
|
25
|
+
export const SKELETON_INDEX_FILE = '_index.md';
|
|
26
|
+
/**
|
|
27
|
+
* 渲染一个目录的索引文本(纯函数:同一 (dir,label,count) 永远同一字节,幂等判据靠它)。
|
|
28
|
+
* 含类名标签、类型 id、条数三项——条数为 0 也如实写"0"。
|
|
29
|
+
*/
|
|
30
|
+
function renderIndex(dir, label, count) {
|
|
31
|
+
return `# ${label}\n\n- 类型: ${dir}\n- 条数: ${count}\n`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 往每个投射根补齐 37 类目录及各自的 `_index.md`。
|
|
35
|
+
*
|
|
36
|
+
* @param deps - 见 `SkeletonDeps`。
|
|
37
|
+
* @returns 无(副作用为落盘)。任何单目录失败只 warn,不抛。
|
|
38
|
+
*/
|
|
39
|
+
export async function ensureTypeSkeleton(deps) {
|
|
40
|
+
const { roots, countsByType, logger } = deps;
|
|
41
|
+
// 目录集合来自词表真源,杜绝手抄漂移(换词表不改这里也不会静默少目录)。
|
|
42
|
+
const dirs = memoryTypeDirs();
|
|
43
|
+
for (const root of roots) {
|
|
44
|
+
for (const dir of dirs) {
|
|
45
|
+
const abs = join(root, dir, SKELETON_INDEX_FILE);
|
|
46
|
+
const count = countsByType.get(dir) ?? 0;
|
|
47
|
+
const label = MEMORY_TYPE_LABELS[dir] ?? dir;
|
|
48
|
+
const text = renderIndex(dir, label, count);
|
|
49
|
+
try {
|
|
50
|
+
// 读现内容比对:一致就跳过——既不重写,也不碰 mtime(atomicWriteText 会 rename 换 inode 刷 mtime)。
|
|
51
|
+
const existing = await readFile(abs, 'utf8').catch(() => undefined);
|
|
52
|
+
if (existing === text)
|
|
53
|
+
continue;
|
|
54
|
+
await atomicWriteText(abs, text, { logger });
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
logger?.warn(`[memory] 类型骨架写入失败(旁路,不影响投射本体): ${abs} - ${err instanceof Error ? err.message : String(err)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { MemExportReport, SuffixReport } from './memory-md-types.js';
|
|
2
|
+
import type { MdLedgerStore } from './memory-md-ledger.js';
|
|
3
|
+
import type { MemoryLogger, MemoryRecord } from '../types.js';
|
|
4
|
+
/** 生效的投射配置(现读,含 `live.mdExportMaxRecords` 覆盖后的预算)。 */
|
|
5
|
+
export interface FileSyncCfg {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
root: string | undefined;
|
|
8
|
+
maxRecords: number;
|
|
9
|
+
timeBudgetMs: number;
|
|
10
|
+
}
|
|
11
|
+
export interface FileSyncDeps {
|
|
12
|
+
/** 取**变更后**的快照(判据要看新的 `validTo` / `name`)。 */
|
|
13
|
+
getByIds: (ids: readonly string[]) => readonly MemoryRecord[];
|
|
14
|
+
exportCfg: () => FileSyncCfg;
|
|
15
|
+
dataDir: string;
|
|
16
|
+
/** 账本未装配时整个挂钩空转(与投射面其他入口一致)。 */
|
|
17
|
+
ledger: () => MdLedgerStore | undefined;
|
|
18
|
+
logger?: MemoryLogger;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* `L1Store` 侧的注入面:两类语义变更,调用方只管点火。
|
|
22
|
+
*
|
|
23
|
+
* - `sync`:有效性变了(增 / 退场 / 恢复)→ 导出该批 + 对齐后缀;
|
|
24
|
+
* - `renamed`:身份(`name`)变了 → 投射文件跟着搬(文件名就是 `name`,不搬就出现
|
|
25
|
+
* 「库里的名字与盘上的文件对不上」的偏离,收敛器也修不了,因为它按当前 name 算路径)。
|
|
26
|
+
*/
|
|
27
|
+
export interface L1FileSyncHook {
|
|
28
|
+
sync: (ids: readonly string[], why: string) => void;
|
|
29
|
+
renamed: (oldNameById: ReadonlyMap<string, string>, why: string) => void;
|
|
30
|
+
}
|
|
31
|
+
/** 一轮「导出 + 后缀收敛」的结果(反刍增量投射复用同一形状,不各写一套日志)。 */
|
|
32
|
+
export interface SyncFilesResult {
|
|
33
|
+
exported: MemExportReport;
|
|
34
|
+
suffix: SuffixReport;
|
|
35
|
+
}
|
|
36
|
+
/** 一轮「导出 + 后缀收敛」必需的最小依赖(不需要按 id 回查的调用方拿这一份就够)。 */
|
|
37
|
+
export type SyncFilesCoreDeps = Pick<FileSyncDeps, 'exportCfg' | 'dataDir' | 'ledger' | 'logger'>;
|
|
38
|
+
/**
|
|
39
|
+
* 该批记录的投射跟随:导出 → 后缀收敛。**不打日志**(调用方自己的口径不同:钩子说
|
|
40
|
+
* “联动”,反刍说“增量”,统一格式反而把两者都稀释);也不抛,异常由调用方收敛。
|
|
41
|
+
*
|
|
42
|
+
* @returns 两个报告;投射面关着或账本未装配时返回 `undefined`(调用方据此不打日志)。
|
|
43
|
+
*/
|
|
44
|
+
export declare function syncFiles(deps: SyncFilesCoreDeps, records: readonly MemoryRecord[]): Promise<SyncFilesResult | undefined>;
|
|
45
|
+
/** 按 id 取快照后跑一轮 `syncFiles`(钩子走这个入口,判据只有一份)。 */
|
|
46
|
+
export declare function syncFilesByIds(deps: FileSyncDeps, ids: readonly string[], why: string): Promise<SyncFilesResult | undefined>;
|
|
47
|
+
/**
|
|
48
|
+
* 造注入给 `L1Store` 的挂钩。契约与 `disableProjectionFile` 一致:同步返回、异常只落日志。
|
|
49
|
+
*
|
|
50
|
+
* 双触发是**允许的**(`recall-feedback(-1)` 仍直接调 `disableProjectionFile` 拿确定性凭证):
|
|
51
|
+
* 两条路径落到同一形状,第二次要么 ENOENT 兜底、要么 `already` 幂等。
|
|
52
|
+
*
|
|
53
|
+
* **串行队列(真机测出来的,不是洁癖)**:同一批文件与同一本账不能被两个后台任务同时
|
|
54
|
+
* 动。旧写法每次点火就自己开一个 promise,于是「先 `appendNew` 再 `retire`」这种紧邻的两次
|
|
55
|
+
* 点火会重叠:第二个任务的收敛器在第一个还没写出文件时判「盘上没这份」(`absent`),
|
|
56
|
+
* 什么都不做——退场记录就永久停在 `.memory.md` 上,直到下次全量导出才被抬回来。
|
|
57
|
+
* 串成一条链后,后一个任务看到的是前一个的终态;执行时再按 id 回查快照,拿到的也是最新有效性。
|
|
58
|
+
*/
|
|
59
|
+
export declare function makeFileSyncHook(deps: FileSyncDeps): L1FileSyncHook;
|