@kira905/dsh-memory-toolkit 0.1.3 → 0.2.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/CHANGELOG.md +92 -0
  2. package/README.en.md +225 -2
  3. package/README.md +180 -2
  4. package/lib/deploy-guard.mjs +463 -275
  5. package/lib/guard-http.mjs +59 -62
  6. package/lib/pitfall-form-map.mjs +94 -84
  7. package/lib/pitfall-score.mjs +262 -185
  8. package/lib/session-file-name.mjs +116 -100
  9. package/lib/zstd-frames.mjs +88 -67
  10. package/memory-db-tools/README.md +29 -1
  11. package/memory-db-tools/audit-analyze.mjs +1 -1
  12. package/memory-db-tools/audit-export.mjs +60 -16
  13. package/memory-db-tools/audit-struct-probe.mjs +29 -8
  14. package/memory-db-tools/lib/known-devices.mjs +137 -0
  15. package/memory-engine/README.md +58 -4
  16. package/memory-engine/bench/bench.mjs +1 -1
  17. package/memory-engine/cli/band-sentinel.mjs +164 -0
  18. package/memory-engine/cli/distill-rollback.mjs +112 -44
  19. package/memory-engine/cli/distill-sessions.mjs +237 -0
  20. package/memory-engine/cli/distill.mjs +196 -95
  21. package/memory-engine/cli/doc-index-rebuild.mjs +142 -108
  22. package/memory-engine/cli/doc-index.mjs +47 -38
  23. package/memory-engine/cli/domain-tags.mjs +246 -162
  24. package/memory-engine/cli/layer-migrate.mjs +176 -0
  25. package/memory-engine/cli/layer-move.mjs +189 -0
  26. package/memory-engine/cli/pitfall-hook.mjs +136 -70
  27. package/memory-engine/cli/recall.mjs +243 -110
  28. package/memory-engine/cli/selfcheck.mjs +93 -44
  29. package/memory-engine/cli/serve-start.mjs +221 -140
  30. package/memory-engine/cli/sweep.mjs +83 -44
  31. package/memory-engine/cli/sync.mjs +93 -57
  32. package/memory-engine/config/audit-devices.json +6 -0
  33. package/memory-engine/config/bands.json +7 -0
  34. package/memory-engine/config/layers.json +7 -0
  35. package/memory-engine/dict/LICENSE-/350/257/215/345/205/270.md +53 -0
  36. package/memory-engine/dict/lexicon-config.json +11 -0
  37. package/memory-engine/dict//350/257/215/345/205/270.txt +109750 -0
  38. package/memory-engine/dict//350/257/215/351/242/221.txt +20000 -0
  39. package/memory-engine/lib/band-sentinel.mjs +417 -0
  40. package/memory-engine/lib/cli-strict.mjs +128 -0
  41. package/memory-engine/lib/cli-util.mjs +147 -125
  42. package/memory-engine/lib/config.mjs +83 -96
  43. package/memory-engine/lib/dirty-ledger.mjs +127 -94
  44. package/memory-engine/lib/distill-rollback.mjs +173 -133
  45. package/memory-engine/lib/distill.mjs +878 -585
  46. package/memory-engine/lib/doc-index-store.mjs +208 -235
  47. package/memory-engine/lib/doc-index.mjs +351 -350
  48. package/memory-engine/lib/doc-scan.mjs +178 -156
  49. package/memory-engine/lib/doc-summarize.mjs +220 -205
  50. package/memory-engine/lib/domain-tags.mjs +358 -277
  51. package/memory-engine/lib/embedder.mjs +175 -194
  52. package/memory-engine/lib/hashes.mjs +22 -7
  53. package/memory-engine/lib/layer-migrate.mjs +330 -0
  54. package/memory-engine/lib/layer-move.mjs +307 -0
  55. package/memory-engine/lib/layers.mjs +191 -0
  56. package/memory-engine/lib/lexicon-recall.mjs +473 -0
  57. package/memory-engine/lib/memory-source.mjs +78 -80
  58. package/memory-engine/lib/pitfall-source.mjs +115 -114
  59. package/memory-engine/lib/policy-source.mjs +279 -261
  60. package/memory-engine/lib/recall-log.mjs +285 -0
  61. package/memory-engine/lib/recall.mjs +565 -240
  62. package/memory-engine/lib/selfcheck.mjs +142 -106
  63. package/memory-engine/lib/session-distill.mjs +603 -0
  64. package/memory-engine/lib/session-source.mjs +421 -0
  65. package/memory-engine/lib/sweep.mjs +107 -130
  66. package/memory-engine/lib/sync.mjs +110 -140
  67. package/memory-engine/lib/tick-hook.mjs +123 -148
  68. package/memory-engine/lib/vectors-store.mjs +213 -188
  69. package/memory-engine/test/12-layering.test.mjs +539 -0
  70. package/memory-engine/test/13-session-distill.test.mjs +616 -0
  71. package/memory-engine/tools/bridge-smoke.mjs +94 -55
  72. package/memory-engine/tools/fetch-model.mjs +167 -87
  73. package/memory-engine/tools/smoke-recall.mjs +94 -38
  74. package/memory-engine/tools/verify-recall-chain.mjs +413 -0
  75. package/package.json +1 -1
@@ -1,12 +1,12 @@
1
- // 记忆引擎 v2 · 离线层 —— 文档索引**逃生舱**(方案 §7「doc-index.db 损坏 / 陈旧 → 重建器一键全量刷新」)
1
+ // 文档索引的「逃生舱」:库烂了 / 陈旧了,用它重建;重建前自动留快照,出事能一键盖回去。
2
2
  //
3
- // 用法:
4
- // node cli/doc-index-rebuild.mjs # 一键全量刷新(范围内按 path 重跑,幂等)
5
- // node cli/doc-index-rebuild.mjs --purge # 先清空范围内全部条再重建(模拟库损坏/陈旧)
6
- // node cli/doc-index-rebuild.mjs --no-snapshot # 不写重建前快照(默认会写)
7
- // node cli/doc-index-rebuild.mjs --restore <快照文件> # 回滚:用快照覆盖当前库
8
- // node cli/doc-index-rebuild.mjs --list-snapshots # 列出可用快照
9
- // 路径可注入:--docs-root / --dsh-root / --doc-index-db / --state-file / --home / --snapshot-dir
3
+ // 支持的动作与开关:
4
+ // 无开关 一键全量刷新(范围内按 path 重跑,幂等)
5
+ // --purge 先清空范围内的条再重建(模拟"库损坏 / 陈旧")
6
+ // --no-snapshot 不留重建前快照(缺省会留)
7
+ // --restore <快照> 回滚:用快照覆盖当前库
8
+ // --list-snapshots 列出可用快照
9
+ // 路径注入:--docs-root / --dsh-root / --doc-index-db / --state-file / --home / --snapshot-dir
10
10
  //
11
11
  // 退出码:0 成功 / 1 有失败 / 2 用法错
12
12
  import fs from 'node:fs';
@@ -16,140 +16,174 @@ import { isMainEntry, parseArgs, makeLogger } from '../lib/cli-util.mjs';
16
16
  import { runDocIndexOnce } from '../lib/doc-index.mjs';
17
17
  import { openDocIndexStore, snapshotDb } from '../lib/doc-index-store.mjs';
18
18
 
19
- const EXIT = { OK: 0, FAIL: 1, USAGE: 2 };
19
+ const RC = { ok: 0, fail: 1, usage: 2 };
20
+ const SNAP_HEAD = 'doc-index-';
21
+ const SNAP_TAIL = '.db';
20
22
 
23
+ /** 本地时间戳(快照文件名):YYYYMMDD-HHMMSS。 */
24
+ function stamp(when = new Date()) {
25
+ const two = (v) => `${v}`.padStart(2, '0');
26
+ const day = `${when.getFullYear()}${two(when.getMonth() + 1)}${two(when.getDate())}`;
27
+ const clock = [when.getHours(), when.getMinutes(), when.getSeconds()].map(two).join('');
28
+ return `${day}-${clock}`;
29
+ }
30
+
31
+ /** 落点解析:home 一定,其余三项才有默认值。 */
21
32
  function resolvePaths(flags, env = process.env) {
22
- const home = path.resolve(String(flags.home || env.DSH_HOME || path.join(os.homedir(), '.dsh')));
33
+ const fallbackHome = path.join(os.homedir(), '.dsh');
34
+ const home = path.resolve(String(flags.home || env.DSH_HOME || fallbackHome));
23
35
  const vectorsDir = path.join(home, 'memory-vectors');
24
36
  const dbPath = path.resolve(String(flags['doc-index-db'] || env.DOC_INDEX_DB || path.join(vectorsDir, 'doc-index.db')));
25
- const snapshotDir = path.resolve(String(flags['snapshot-dir'] || path.join(vectorsDir, 'doc-index-snapshots')));
26
- return { home, vectorsDir, dbPath, snapshotDir };
37
+ return {
38
+ home,
39
+ vectorsDir,
40
+ dbPath,
41
+ snapshotDir: path.resolve(String(flags['snapshot-dir'] || path.join(vectorsDir, 'doc-index-snapshots'))),
42
+ };
27
43
  }
28
44
 
29
- function ts() {
30
- const d = new Date();
31
- const p = (n) => String(n).padStart(2, '0');
32
- return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
33
- }
34
-
35
- /** 重建前快照(VACUUM INTO;库不存在则跳过——没有可备份的东西)。 */
36
- export function takeSnapshot({ dbPath, snapshotDir, log = () => {} }) {
37
- if (!fs.existsSync(dbPath)) return { ok: true, skipped: true, reason: '库不存在(首次建库无需快照)' };
38
- const dest = path.join(snapshotDir, `doc-index-${ts()}.db`);
45
+ /** 开库 → 干活 → 无论成败都把库关掉(快照与清表都借这个壳)。 */
46
+ function withStore(dbPath, work) {
39
47
  const store = openDocIndexStore(dbPath, { create: false });
40
48
  try {
41
- snapshotDb(store.db, dest);
49
+ return work(store.db);
42
50
  } finally {
43
- try {
44
- store.db.close();
45
- } catch {
46
- /* ignore */
47
- }
51
+ try { store.db.close(); } catch { /* 关不掉不盖住真正的错 */ }
48
52
  }
49
- const size = fs.statSync(dest).size;
50
- log(`[rebuild] 重建前快照:${dest}(${size} B)`);
51
- return { ok: true, dest, size };
52
53
  }
53
54
 
54
- /** 列出快照(按时间倒序)。 */
55
+ /** 重建前快照(VACUUM INTO)。库还不存在 ⇒ 没有可备份的东西,跳过即可。 */
56
+ export function takeSnapshot({ dbPath, snapshotDir, log = () => {} }) {
57
+ const hasDb = fs.existsSync(dbPath);
58
+ if (!hasDb) return { ok: true, skipped: true, reason: '库不存在(首次建库无需快照)' };
59
+ const dest = path.join(snapshotDir, `${SNAP_HEAD}${stamp()}${SNAP_TAIL}`);
60
+ withStore(dbPath, (db) => snapshotDb(db, dest));
61
+ const { size } = fs.statSync(dest);
62
+ log(`[rebuild] 重建前快照已留:${dest}(${size} B)`);
63
+ return { ok: true, size, dest };
64
+ }
65
+
66
+ /** 列快照(新→旧)。目录不在 = 没有快照,不是错误。 */
55
67
  export function listSnapshots(snapshotDir) {
68
+ let entries;
56
69
  try {
57
- return fs
58
- .readdirSync(snapshotDir)
59
- .filter((n) => n.startsWith('doc-index-') && n.endsWith('.db'))
60
- .map((n) => ({ name: n, file: path.join(snapshotDir, n), size: fs.statSync(path.join(snapshotDir, n)).size, mtime: fs.statSync(path.join(snapshotDir, n)).mtimeMs }))
61
- .sort((a, b) => b.mtime - a.mtime);
70
+ entries = fs.readdirSync(snapshotDir);
71
+ } catch {
72
+ return [];
73
+ }
74
+ try {
75
+ const picked = [];
76
+ for (const name of entries) {
77
+ const looksLikeSnap = name.startsWith(SNAP_HEAD) && name.endsWith(SNAP_TAIL);
78
+ if (!looksLikeSnap) continue;
79
+ const file = path.join(snapshotDir, name);
80
+ const info = fs.statSync(file);
81
+ picked.push({ name, file, size: info.size, mtime: info.mtimeMs });
82
+ }
83
+ picked.sort((a, b) => b.mtime - a.mtime);
84
+ return picked;
62
85
  } catch {
63
86
  return [];
64
87
  }
65
88
  }
66
89
 
90
+ function existenceProblem(file, label) {
91
+ return fs.existsSync(file) ? null : `${label}:${file}`;
92
+ }
93
+
67
94
  /**
68
- * 回滚:用快照覆盖当前库(先把当前库另存为 `.before-restore-<ts>`,绝不静默丢)。
69
- * ⚠️ WAL 副作用:覆盖前必须清掉 `-wal` / `-shm`,否则新库会被旧 WAL 污染。
95
+ * 回滚:快照盖回当前库。当前库先另存一份 `.before-restore-<ts>`,绝不静默丢东西。
96
+ * 注意覆盖前必须清 `-wal` / `-shm`,否则新库会被旧 WAL 污染。
70
97
  */
71
98
  export function restoreFromSnapshot({ dbPath, snapshotFile, log = () => {} }) {
72
- if (!fs.existsSync(snapshotFile)) return { ok: false, error: `快照不存在:${snapshotFile}` };
73
- if (!fs.existsSync(dbPath)) return { ok: false, error: `目标库不存在:${dbPath}` };
74
- const backup = `${dbPath}.before-restore-${ts()}`;
75
- fs.copyFileSync(dbPath, backup);
76
- for (const suffix of ['-wal', '-shm']) {
77
- const f = `${dbPath}${suffix}`;
99
+ const blocker = existenceProblem(snapshotFile, '快照不存在') ?? existenceProblem(dbPath, '目标库不存在');
100
+ if (blocker) return { ok: false, error: blocker };
101
+ const backup = `${dbPath}.before-restore-${stamp()}`;
102
+ copyFile(dbPath, backup);
103
+ for (const sidecar of ['-wal', '-shm']) {
104
+ const f = `${dbPath}${sidecar}`;
78
105
  if (fs.existsSync(f)) fs.rmSync(f, { force: true });
79
106
  }
80
- fs.copyFileSync(snapshotFile, dbPath);
81
- log(`[rebuild] 已回滚:${snapshotFile} → ${dbPath}(回滚前副本:${backup})`);
107
+ copyFile(snapshotFile, dbPath);
108
+ log(`[rebuild] 快照已回滚:${snapshotFile} → ${dbPath}(回滚前副本 ${backup})`);
82
109
  return { ok: true, backup, from: snapshotFile };
83
110
  }
84
111
 
85
- export async function main() {
86
- const { flags } = parseArgs();
87
- const json = Boolean(flags.json);
88
- const log = makeLogger({ json, quiet: Boolean(flags.quiet) });
89
- const { dbPath, snapshotDir, home } = resolvePaths(flags);
112
+ function copyFile(from, to) {
113
+ fs.copyFileSync(from, to);
114
+ }
90
115
 
91
- try {
92
- if (flags['list-snapshots']) {
93
- const snaps = listSnapshots(snapshotDir);
94
- if (json) process.stdout.write(JSON.stringify({ ok: true, snapshotDir, snapshots: snaps }, null, 2) + '\n');
95
- else {
96
- log(`快照目录:${snapshotDir}`);
97
- if (!snaps.length) log('(无快照)');
98
- for (const s of snaps) log(` ${new Date(s.mtime).toLocaleString('zh-CN')} ${s.size} B ${s.file}`);
99
- }
100
- return EXIT.OK;
101
- }
116
+ /** 子命令:列快照。 */
117
+ function cmdList({ flags, json, log, paths }) {
118
+ const snaps = listSnapshots(paths.snapshotDir);
119
+ if (json) {
120
+ process.stdout.write(`${JSON.stringify({ ok: true, snapshotDir: paths.snapshotDir, snapshots: snaps }, null, 2)}\n`);
121
+ return RC.ok;
122
+ }
123
+ log(`快照目录:${paths.snapshotDir}`);
124
+ if (!snaps.length) log('(这个目录里还没有快照)');
125
+ for (const s of snaps) log(` ${new Date(s.mtime).toLocaleString('zh-CN')} ${s.size} B ${s.file}`);
126
+ return RC.ok;
127
+ }
102
128
 
103
- if (typeof flags.restore === 'string') {
104
- const r = restoreFromSnapshot({ dbPath, snapshotFile: path.resolve(flags.restore), log });
105
- if (json) process.stdout.write(JSON.stringify({ ok: r.ok, ...r }, null, 2) + '\n');
106
- return r.ok ? EXIT.OK : EXIT.FAIL;
107
- }
129
+ /** 子命令:从快照回滚。 */
130
+ function cmdRestore({ flags, json, log, paths }) {
131
+ const r = restoreFromSnapshot({ dbPath: paths.dbPath, snapshotFile: path.resolve(flags.restore), log });
132
+ if (json) process.stdout.write(`${JSON.stringify({ ok: r.ok, ...r }, null, 2)}\n`);
133
+ return r.ok ? RC.ok : RC.fail;
134
+ }
108
135
 
109
- // ① 重建前快照(默认开)
110
- let snapshot = { ok: true, skipped: true, reason: '--no-snapshot' };
111
- if (!flags['no-snapshot'] && !flags['dry-run']) {
112
- snapshot = takeSnapshot({ dbPath, snapshotDir, log });
113
- }
136
+ /** 子命令:一键重建(缺省动作)。 */
137
+ async function cmdRebuild({ flags, json, log, paths }) {
138
+ // ① 先留退路(--no-snapshot / --dry-run 时不写)
139
+ let snapshot = { ok: true, skipped: true, reason: '--no-snapshot' };
140
+ const wantSnapshot = !flags['no-snapshot'] && !flags['dry-run'];
141
+ if (wantSnapshot) snapshot = takeSnapshot({ dbPath: paths.dbPath, snapshotDir: paths.snapshotDir, log });
114
142
 
115
- // ② --purge:先清空范围内全部条(模拟"库损坏 / 陈旧"后的干净重建)
116
- if (flags.purge && !flags['dry-run'] && fs.existsSync(dbPath)) {
117
- const store = openDocIndexStore(dbPath, { create: false });
118
- try {
119
- const n = store.db.prepare('DELETE FROM doc_index').run().changes;
120
- log(`[rebuild] --purge:已清空 doc_index(删除 ${Number(n) || 0} 条)`);
121
- } finally {
122
- try {
123
- store.db.close();
124
- } catch {
125
- /* ignore */
126
- }
127
- }
128
- }
143
+ // ② --purge:先把范围内的条清空,模拟"库烂了"从零重建
144
+ const canPurge = flags.purge && !flags['dry-run'] && fs.existsSync(paths.dbPath);
145
+ if (canPurge) {
146
+ const removed = withStore(paths.dbPath, (db) => db.prepare('DELETE FROM doc_index').run().changes);
147
+ log(`[rebuild] 清表完成:doc_index 删了 ${Number(removed) || 0} 条`);
148
+ }
149
+
150
+ // ③ 全量刷新:ignore 差分、按 path 覆盖,所以重跑幂等
151
+ const r = await runDocIndexOnce({ flags: { ...flags, rebuild: true, home: paths.home, 'doc-index-db': paths.dbPath } });
152
+ for (const line of r.lines) log(line);
153
+
154
+ const payload = {
155
+ ok: r.status !== 'failed',
156
+ status: r.status,
157
+ dbPath: paths.dbPath,
158
+ snapshot,
159
+ stats: r.stats,
160
+ dailyLine: r.state.lastDailyLine,
161
+ };
162
+ if (json) process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
163
+ else log(`[rebuild] 收工:${r.state.lastDailyLine}`);
164
+ return r.status === 'failed' ? RC.fail : RC.ok;
165
+ }
129
166
 
130
- // ③ 一键全量刷新(force = 忽略差分,按 path 覆盖)
131
- const r = await runDocIndexOnce({
132
- flags: { ...flags, rebuild: true, home, 'doc-index-db': dbPath },
133
- });
134
- for (const l of r.lines) log(l);
135
-
136
- const out = {
137
- ok: r.status !== 'failed',
138
- status: r.status,
139
- dbPath,
140
- snapshot,
141
- stats: r.stats,
142
- dailyLine: r.state.lastDailyLine,
143
- };
144
- if (json) process.stdout.write(JSON.stringify(out, null, 2) + '\n');
145
- else log(`[rebuild] 完成:${r.state.lastDailyLine}`);
146
- return r.status === 'failed' ? EXIT.FAIL : EXIT.OK;
167
+ const SUBCOMMANDS = [
168
+ { pick: (flags) => Boolean(flags['list-snapshots']), run: cmdList },
169
+ { pick: (flags) => typeof flags.restore === 'string', run: cmdRestore },
170
+ ];
171
+
172
+ export async function main() {
173
+ const { flags } = parseArgs();
174
+ const asJson = Boolean(flags.json);
175
+ const log = makeLogger({ json: asJson, quiet: Boolean(flags.quiet) });
176
+ const paths = resolvePaths(flags);
177
+ const chosen = SUBCOMMANDS.find((c) => c.pick(flags));
178
+ try {
179
+ return await (chosen ? chosen.run({ flags, json: asJson, log, paths }) : cmdRebuild({ flags, json: asJson, log, paths }));
147
180
  } catch (e) {
148
- const msg = String(e && e.stack ? e.stack : e);
149
- if (json) process.stdout.write(JSON.stringify({ ok: false, error: msg }, null, 2) + '\n');
181
+ const msg = String(e?.stack ?? e);
182
+ if (asJson) process.stdout.write(`${JSON.stringify({ ok: false, error: msg }, null, 2)}\n`);
150
183
  else log(`❌ ${msg}`);
151
- return EXIT.USAGE;
184
+ return RC.usage;
152
185
  }
153
186
  }
154
187
 
155
- if (isMainEntry(import.meta.url)) process.exitCode = await main();
188
+ const launchedDirectly = isMainEntry(import.meta.url);
189
+ if (launchedDirectly) process.exitCode = await main();
@@ -1,48 +1,57 @@
1
- // 记忆引擎 v2 · 离线层 —— 文档索引 CLI(方案 §3.2)
1
+ // 文档索引 CLI(离线层):跑一轮「文档 → 索引库」的增量同步。
2
2
  //
3
- // 用法:
4
- // node cli/doc-index.mjs # 增量(默认范围 = 首期 10 篇)
5
- // node cli/doc-index.mjs --all # 扫描面全量(⚠️ 全量是 §10 T2 触发器事项)
6
- // node cli/doc-index.mjs --only docs/a.md,docs/b.md
7
- // node cli/doc-index.mjs --rebuild # 忽略差分,范围内全部重跑(按 path 覆盖,幂等)
8
- // node cli/doc-index.mjs --dry-run # 只算不写(不建库、不改 state、不写日报)
9
- // node cli/doc-index.mjs --llm # 摘要走便宜模型(受 §3.5 成本闸)
10
- // node cli/doc-index.mjs --json # 机器可读(日志走 stderr)
11
- // 路径可注入:--docs-root / --dsh-root / --doc-index-db / --state-file / --home
3
+ // 常见用法
4
+ // 什么都不给 增量跑一轮,范围是首期那 10 篇
5
+ // --all 扫描面全量(重活,别随手跑)
6
+ // --only a.md,b.md 只处理点名的几篇
7
+ // --rebuild 无视差分,范围内按 path 全跑(覆盖写,重跑幂等)
8
+ // --dry-run 只算不写:不建库、不动 state、不写日报
9
+ // --llm 摘要改走便宜模型(同样受成本闸约束)
10
+ // --json 机器可读;人话日志改走 stderr
11
+ // 路径都能改:--docs-root / --dsh-root / --doc-index-db / --state-file / --home
12
12
  import { isMainEntry, parseArgs, makeLogger } from '../lib/cli-util.mjs';
13
13
  import { runDocIndexOnce } from '../lib/doc-index.mjs';
14
14
 
15
+ /** 机器读 JSON,人读逐行 —— 两条路别混。 */
16
+ function narrate(run, { json, log }) {
17
+ if (!json) {
18
+ for (const line of run.lines) log(line);
19
+ return;
20
+ }
21
+ const digest = {
22
+ ok: run.ok,
23
+ status: run.status,
24
+ exitCode: run.exitCode,
25
+ stats: run.stats,
26
+ dailyLine: run.state.lastDailyLine,
27
+ paths: run.state.paths,
28
+ };
29
+ process.stdout.write(`${JSON.stringify(digest, null, 2)}\n`);
30
+ }
31
+
32
+ /** 出错也要分两条路说清楚,然后按既定语义回 2。 */
33
+ function complain(e, { json, log }) {
34
+ const msg = String(e?.stack ?? e);
35
+ if (json) process.stdout.write(`${JSON.stringify({ ok: false, error: msg }, null, 2)}\n`);
36
+ else log(`❌ ${msg}`);
37
+ return 2;
38
+ }
39
+
15
40
  export async function main() {
16
- const { flags } = parseArgs();
17
- const json = Boolean(flags.json);
18
- const log = makeLogger({ json, quiet: Boolean(flags.quiet) });
41
+ const parsed = parseArgs();
42
+ const flags = parsed.flags;
43
+ const asJson = Boolean(flags.json);
44
+ const log = makeLogger({ json: asJson, quiet: Boolean(flags.quiet) });
45
+ const channel = { json: asJson, log };
46
+
19
47
  try {
20
- const r = await runDocIndexOnce({ flags });
21
- if (json) {
22
- process.stdout.write(
23
- JSON.stringify(
24
- {
25
- ok: r.ok,
26
- status: r.status,
27
- exitCode: r.exitCode,
28
- stats: r.stats,
29
- dailyLine: r.state.lastDailyLine,
30
- paths: r.state.paths,
31
- },
32
- null,
33
- 2,
34
- ) + '\n',
35
- );
36
- } else {
37
- for (const l of r.lines) log(l);
38
- }
39
- process.exitCode = r.exitCode;
48
+ const run = await runDocIndexOnce({ flags });
49
+ narrate(run, channel);
50
+ process.exitCode = run.exitCode;
40
51
  } catch (e) {
41
- const msg = String(e && e.stack ? e.stack : e);
42
- if (json) process.stdout.write(JSON.stringify({ ok: false, error: msg }, null, 2) + '\n');
43
- else log(`❌ ${msg}`);
44
- process.exitCode = 2;
52
+ process.exitCode = complain(e, channel);
45
53
  }
46
54
  }
47
55
 
48
- if (isMainEntry(import.meta.url)) await main();
56
+ const invokedDirectly = isMainEntry(import.meta.url);
57
+ if (invokedDirectly) await main();