@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.
Files changed (132) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +661 -0
  3. package/README.en.md +110 -0
  4. package/README.md +102 -2
  5. package/dsh-memory-local/FORK-BASE.json +24 -0
  6. package/dsh-memory-local/README.md +113 -0
  7. package/dsh-memory-local/build.mjs +101 -0
  8. package/dsh-memory-local/cordis.patch.yml +20 -0
  9. package/dsh-memory-local/deploy.mjs +306 -0
  10. package/dsh-memory-local/entity-status.mjs +75 -0
  11. package/dsh-memory-local/lib/bridge.mjs +108 -0
  12. package/dsh-memory-local/lib/host-adapter.js +1171 -0
  13. package/dsh-memory-local/lib/index.d.ts +128 -0
  14. package/dsh-memory-local/lib/index.js +944 -0
  15. package/dsh-memory-local/lib/injector.mjs +400 -0
  16. package/dsh-memory-local/lib/memory-read.mjs +116 -0
  17. package/dsh-memory-local/lib/module-gate.mjs +68 -0
  18. package/dsh-memory-local/lib/pointer.mjs +152 -0
  19. package/dsh-memory-local/lib/prod-gate.mjs +121 -0
  20. package/dsh-memory-local/lib/recall-log.mjs +118 -0
  21. package/dsh-memory-local/lib/recall.mjs +192 -0
  22. package/dsh-memory-local/lib/schema-migrate.mjs +365 -0
  23. package/dsh-memory-local/lib/user-message.mjs +32 -0
  24. package/dsh-memory-local/lib/vectors-read.mjs +110 -0
  25. package/dsh-memory-local/package.json +51 -0
  26. package/dsh-memory-local/restore.mjs +259 -0
  27. package/dsh-memory-local/rollback-injector-fix.mjs +166 -0
  28. package/dsh-memory-local/src/bridge.mjs +108 -0
  29. package/dsh-memory-local/src/host-adapter.js +1171 -0
  30. package/dsh-memory-local/src/index.d.ts +128 -0
  31. package/dsh-memory-local/src/index.js +944 -0
  32. package/dsh-memory-local/src/injector.mjs +400 -0
  33. package/dsh-memory-local/src/memory-read.mjs +116 -0
  34. package/dsh-memory-local/src/module-gate.mjs +68 -0
  35. package/dsh-memory-local/src/pointer.mjs +152 -0
  36. package/dsh-memory-local/src/recall-log.mjs +118 -0
  37. package/dsh-memory-local/src/recall.mjs +192 -0
  38. package/dsh-memory-local/src/schema-migrate.mjs +365 -0
  39. package/dsh-memory-local/src/user-message.mjs +32 -0
  40. package/dsh-memory-local/src/vectors-read.mjs +110 -0
  41. package/dsh-memory-local/test/fixtures-s2b.mjs +216 -0
  42. package/dsh-memory-local/test/probe-entity.mjs +170 -0
  43. package/dsh-memory-local/test/regression.mjs +270 -0
  44. package/dsh-memory-local/test/s2b.test.mjs +568 -0
  45. package/dsh-memory-local/test/schema-migrate.test.mjs +334 -0
  46. package/dsh-memory-local/test/test-adapter-converge.mjs +127 -0
  47. package/dsh-memory-local/test/visible-injections.test.mjs +239 -0
  48. package/dsh-memory-local/verify-after-restart.mjs +181 -0
  49. package/lib/deploy-guard.mjs +391 -0
  50. package/lib/guard-http.mjs +93 -0
  51. package/lib/pitfall-form-map.mjs +130 -0
  52. package/lib/pitfall-score.mjs +185 -0
  53. package/lib/session-file-name.mjs +160 -0
  54. package/lib/zstd-frames.mjs +78 -0
  55. package/memory-db-tools/README.md +68 -0
  56. package/memory-db-tools/_test/test-memory-scope.mjs +302 -0
  57. package/memory-db-tools/audit-analyze.mjs +352 -0
  58. package/memory-db-tools/audit-dump-ids.mjs +245 -0
  59. package/memory-db-tools/audit-export.mjs +270 -0
  60. package/memory-db-tools/audit-hit-probe.mjs +227 -0
  61. package/memory-db-tools/audit-ledger-probe.mjs +236 -0
  62. package/memory-db-tools/audit-persona-overlap.mjs +277 -0
  63. package/memory-db-tools/audit-struct-probe.mjs +213 -0
  64. package/memory-db-tools/audit-verify-unchanged.mjs +276 -0
  65. package/memory-db-tools/check-memory-db.mjs +8 -0
  66. package/memory-db-tools/dump-memory-db.mjs +12 -0
  67. package/memory-db-tools/lib/memory-scope.mjs +446 -0
  68. package/memory-db-tools/list-memory-summary.mjs +477 -0
  69. package/memory-db-tools/memory-identity-check.mjs +101 -0
  70. package/memory-db-tools/memory-schema-migrate.mjs +290 -0
  71. package/memory-db-tools/memory-scope-fixture.mjs +197 -0
  72. package/memory-db-tools/read-memories.mjs +26 -0
  73. package/memory-db-tools/read-memory-db.mjs +20 -0
  74. package/memory-db-tools/refine-memories.mjs +247 -0
  75. package/memory-db-tools/show-mem-times.mjs +22 -0
  76. package/memory-engine/README.md +70 -0
  77. package/memory-engine/bench/bench.mjs +221 -0
  78. package/memory-engine/cli/distill-rollback.mjs +86 -0
  79. package/memory-engine/cli/distill.mjs +156 -0
  80. package/memory-engine/cli/doc-index-rebuild.mjs +155 -0
  81. package/memory-engine/cli/doc-index.mjs +48 -0
  82. package/memory-engine/cli/domain-tags.mjs +251 -0
  83. package/memory-engine/cli/pitfall-hook.mjs +91 -0
  84. package/memory-engine/cli/recall.mjs +132 -0
  85. package/memory-engine/cli/selfcheck.mjs +57 -0
  86. package/memory-engine/cli/serve-start.mjs +247 -0
  87. package/memory-engine/cli/sweep.mjs +58 -0
  88. package/memory-engine/cli/sync.mjs +69 -0
  89. package/memory-engine/lib/cli-util.mjs +197 -0
  90. package/memory-engine/lib/config.mjs +135 -0
  91. package/memory-engine/lib/dirty-ledger.mjs +174 -0
  92. package/memory-engine/lib/distill-rollback.mjs +178 -0
  93. package/memory-engine/lib/distill.mjs +898 -0
  94. package/memory-engine/lib/doc-index-store.mjs +325 -0
  95. package/memory-engine/lib/doc-index.mjs +529 -0
  96. package/memory-engine/lib/doc-scan.mjs +271 -0
  97. package/memory-engine/lib/doc-summarize.mjs +305 -0
  98. package/memory-engine/lib/domain-tags.mjs +429 -0
  99. package/memory-engine/lib/embedder.mjs +242 -0
  100. package/memory-engine/lib/hashes.mjs +12 -0
  101. package/memory-engine/lib/memory-source.mjs +127 -0
  102. package/memory-engine/lib/pitfall-source.mjs +170 -0
  103. package/memory-engine/lib/policy-source.mjs +371 -0
  104. package/memory-engine/lib/recall.mjs +331 -0
  105. package/memory-engine/lib/selfcheck.mjs +131 -0
  106. package/memory-engine/lib/sweep.mjs +159 -0
  107. package/memory-engine/lib/sync.mjs +180 -0
  108. package/memory-engine/lib/tick-hook.mjs +218 -0
  109. package/memory-engine/lib/vectors-store.mjs +292 -0
  110. package/memory-engine/package-lock.json +1086 -0
  111. package/memory-engine/package.json +22 -0
  112. package/memory-engine/serve.mjs +171 -0
  113. package/memory-engine/test/01-store.test.mjs +127 -0
  114. package/memory-engine/test/02-sync.test.mjs +196 -0
  115. package/memory-engine/test/03-sweep.test.mjs +145 -0
  116. package/memory-engine/test/04-recall.test.mjs +214 -0
  117. package/memory-engine/test/05-selfcheck.test.mjs +175 -0
  118. package/memory-engine/test/06-doc-index.test.mjs +928 -0
  119. package/memory-engine/test/07-domain-tags.test.mjs +298 -0
  120. package/memory-engine/test/08-distill.test.mjs +686 -0
  121. package/memory-engine/test/09-pitfall-source.test.mjs +365 -0
  122. package/memory-engine/test/10-tick-hook.test.mjs +252 -0
  123. package/memory-engine/test/11-policy-source.test.mjs +224 -0
  124. package/memory-engine/test/_tickhook-worker.mjs +56 -0
  125. package/memory-engine/test/fixtures.mjs +205 -0
  126. package/memory-engine/test/harness.mjs +73 -0
  127. package/memory-engine/test/run.mjs +35 -0
  128. package/memory-engine/tools/bridge-smoke.mjs +80 -0
  129. package/memory-engine/tools/fetch-model.mjs +128 -0
  130. package/memory-engine/tools/smoke-recall.mjs +76 -0
  131. package/package.json +53 -4
  132. package/persona-memory-audit.mjs +217 -0
@@ -0,0 +1,259 @@
1
+ /**
2
+ * restore.mjs —— 把 web profile 的 `dsh-memory` 实体原子还原成**上游官方原物**(一键回滚)。
3
+ *
4
+ * 还原源 = deploy.mjs 首次替换时归档的 `_archive\g24-s2a-memory-deploy-*\official-entity\`
5
+ * (该目录不存在就不动手:没有还原源时回滚等于破坏)。
6
+ *
7
+ * 流程(可回滚的原子交换):
8
+ * 1) 找含上游原物的备份目录(最新一个)
9
+ * 2) 把当前实体快照到 `_archive\g24-s2a-memory-prerestore-<stamp>\entity-before-restore\`
10
+ * —— 还原本身也可被审视/再回滚
11
+ * 3) 上游原物拷进 `<target>.restore-tmp-<stamp>`,逐文件核 sha(与 OFFICIAL-ENTITY.json 对齐)
12
+ * 4) 交换:target → `<target>.restore-old-<stamp>`、tmp → target,然后删掉 old
13
+ * 5) 复核还原后 sha,打印「重新部署」命令
14
+ *
15
+ * 退出码:0 = 成功(含 --dry-run / --self-test 通过) / 1 = 还原失败 / 2 = 用法错误(含未知参数 / 缺 --allow-production) / 3 = 无还原源。
16
+ *
17
+ * 🔒 生产闸:`--target` 落在生产树(`$DSH_HOME` 与可选 `DSH_PROD_ROOTS`,realpath + 逻辑双比)内时,
18
+ * **真还原**必须显式 `--allow-production`(否则 rc=2);`--dry-run` 是只读档,不受闸门限制。
19
+ *
20
+ * 用法:
21
+ * node restore.mjs --target <DSH_HOME> [--profile web] [--dry-run] [--list] [--allow-production]
22
+ * node restore.mjs --self-test # 沙箱夹具演练(不碰任何真实 home)
23
+ */
24
+ import fs from 'node:fs'
25
+ import os from 'node:os'
26
+ import path from 'node:path'
27
+ import crypto from 'node:crypto'
28
+ import { fileURLToPath } from 'node:url'
29
+ import { parseArgs, describeUnknown, EXIT } from '../lib/deploy-guard.mjs'
30
+ // R-060 隔离闸(2026-10-16 接入):把本文件原来"只比是否等于默认 home"的精确字符串比较,换成共享件
31
+ // 「realpath + 逻辑路径双比」的生产树判定(`tools\lib\production-guard.mjs`)。
32
+ import { gateProductionWrite, refuseLine } from './lib/prod-gate.mjs'
33
+
34
+ const KNOWN_ARGS = [
35
+ '--dry-run', '--list', '--self-test', '--allow-production',
36
+ { flag: '--target', takesValue: true },
37
+ { flag: '--profile', takesValue: true, alias: '--p' },
38
+ { flag: '--archive', takesValue: true },
39
+ ]
40
+
41
+ const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex')
42
+ const stamp = () => {
43
+ const d = new Date()
44
+ const p = (n) => String(n).padStart(2, '0')
45
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`
46
+ }
47
+ const listTree = (root) => {
48
+ const out = []
49
+ const walk = (dir) => {
50
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
51
+ const full = path.join(dir, e.name)
52
+ if (e.isDirectory()) walk(full)
53
+ else if (e.isFile()) out.push(path.relative(root, full).replaceAll('\\', '/'))
54
+ }
55
+ }
56
+ if (fs.existsSync(root)) walk(root)
57
+ return out.sort()
58
+ }
59
+
60
+ /** 找含上游原物的备份目录(最新优先)。 */
61
+ function findSnapshot(archiveRoot) {
62
+ if (!fs.existsSync(archiveRoot)) return []
63
+ return fs.readdirSync(archiveRoot)
64
+ .filter((n) => n.startsWith('g24-s2a-memory-deploy-'))
65
+ .map((n) => path.join(archiveRoot, n))
66
+ .filter((d) => fs.existsSync(path.join(d, 'official-entity', 'package.json')))
67
+ .sort()
68
+ }
69
+
70
+ /** 校验一份实体目录与 OFFICIAL-ENTITY.json 的逐文件 sha 是否一致。 */
71
+ function verifyAgainstManifest(entityDir, manifestFile) {
72
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'))
73
+ const bad = []
74
+ for (const f of manifest.files) {
75
+ const full = path.join(entityDir, f.file)
76
+ if (!fs.existsSync(full)) { bad.push(`${f.file}: missing`); continue }
77
+ const got = sha256(fs.readFileSync(full))
78
+ if (got !== f.sha256) bad.push(`${f.file}: sha ${got.slice(0, 12)} != ${f.sha256.slice(0, 12)}`)
79
+ }
80
+ return { manifest, bad }
81
+ }
82
+
83
+ /** 核心还原(供正常路径与 self-test 共用)。 */
84
+ function restoreEntity({ target, backupDir, archiveRoot, dry }) {
85
+ const officialDir = path.join(backupDir, 'official-entity')
86
+ const manifestFile = path.join(backupDir, 'OFFICIAL-ENTITY.json')
87
+ const out = { target, backupDir, actions: [], ok: false }
88
+
89
+ const verified = verifyAgainstManifest(officialDir, manifestFile)
90
+ if (verified.bad.length) throw new Error(`备份原物自身校验失败:${verified.bad.join('; ')}`)
91
+ out.manifestFiles = verified.manifest.files.length
92
+
93
+ const tag = stamp()
94
+ const preDir = path.join(archiveRoot, `g24-s2a-memory-prerestore-${tag}`)
95
+ const tmpDir = `${target}.restore-tmp-${tag}`
96
+ const oldDir = `${target}.restore-old-${tag}`
97
+ out.actions.push(`snapshot current entity -> ${path.join(preDir, 'entity-before-restore')}`)
98
+ out.actions.push(`stage upstream entity -> ${tmpDir}`)
99
+ out.actions.push(`swap: ${target} -> ${oldDir}, then ${tmpDir} -> ${target}`)
100
+ out.preRestoreDir = preDir
101
+ if (dry) return out
102
+
103
+ // 2) 快照当前实体(还原也可被再回滚)
104
+ fs.mkdirSync(preDir, { recursive: true })
105
+ if (fs.existsSync(target)) fs.cpSync(target, path.join(preDir, 'entity-before-restore'), { recursive: true })
106
+ fs.writeFileSync(path.join(preDir, 'RESTORE-INFO.json'), `${JSON.stringify({
107
+ restoredAt: new Date().toISOString(), restoredFrom: backupDir, target,
108
+ }, null, 2)}\n`, 'utf8')
109
+
110
+ // 3) stage + 核 sha
111
+ fs.rmSync(tmpDir, { recursive: true, force: true })
112
+ fs.cpSync(officialDir, tmpDir, { recursive: true })
113
+ const staged = verifyAgainstManifest(tmpDir, manifestFile)
114
+ if (staged.bad.length) { fs.rmSync(tmpDir, { recursive: true, force: true }); throw new Error(`stage 校验失败:${staged.bad.join('; ')}`) }
115
+
116
+ // 4) 交换
117
+ if (fs.existsSync(target)) fs.renameSync(target, oldDir)
118
+ fs.renameSync(tmpDir, target)
119
+ fs.rmSync(oldDir, { recursive: true, force: true })
120
+
121
+ // 5) 复核
122
+ const after = verifyAgainstManifest(target, manifestFile)
123
+ if (after.bad.length) throw new Error(`还原后复核失败:${after.bad.join('; ')}`)
124
+ out.ok = true
125
+ out.restoredFiles = after.manifest.files.length
126
+ return out
127
+ }
128
+
129
+ // ── --self-test:沙箱夹具演练(不碰真实 home / 真实 _archive) ─────────────────
130
+ if (process.argv.includes('--self-test')) {
131
+ const pa = parseArgs(process.argv.slice(2), KNOWN_ARGS)
132
+ if (!pa.ok) { console.error(`[restore] ${describeUnknown(pa.unknown, KNOWN_ARGS)}`); process.exit(EXIT.USAGE) }
133
+ const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'g24-s2a-restore-selftest-'))
134
+ const archiveRoot = path.join(sandbox, 'archive')
135
+ const target = path.join(sandbox, 'home', 'profiles', 'web', 'node_modules', 'dsh-memory')
136
+ const failures = []
137
+ const check = (name, ok, detail = '') => { console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`); if (!ok) failures.push(name) }
138
+ try {
139
+ // 夹具:一份「官方原物」备份目录 + 一份「fork 实体」当前态
140
+ const backupDir = path.join(archiveRoot, 'g24-s2a-memory-deploy-20260921-000000')
141
+ const officialDir = path.join(backupDir, 'official-entity')
142
+ fs.mkdirSync(path.join(officialDir, 'lib'), { recursive: true })
143
+ fs.writeFileSync(path.join(officialDir, 'lib', 'index.js'), 'export const marker = "UPSTREAM";\n', 'utf8')
144
+ fs.writeFileSync(path.join(officialDir, 'package.json'), `${JSON.stringify({ name: 'dsh-memory', version: '0.1.0' }, null, 2)}\n`, 'utf8')
145
+ const files = listTree(officialDir).map((f) => ({
146
+ file: f, bytes: fs.statSync(path.join(officialDir, f)).size, sha256: sha256(fs.readFileSync(path.join(officialDir, f))),
147
+ }))
148
+ fs.writeFileSync(path.join(backupDir, 'OFFICIAL-ENTITY.json'), `${JSON.stringify({ files }, null, 2)}\n`, 'utf8')
149
+
150
+ fs.mkdirSync(path.join(target, 'lib'), { recursive: true })
151
+ fs.writeFileSync(path.join(target, 'lib', 'index.js'), 'export const marker = "FORK";\n', 'utf8')
152
+ fs.writeFileSync(path.join(target, 'package.json'), `${JSON.stringify({ name: 'dsh-memory', version: '0.1.0', fork: { name: 'dsh-memory-local' } }, null, 2)}\n`, 'utf8')
153
+
154
+ // ① 无还原源时拒绝
155
+ const emptyArchive = path.join(sandbox, 'empty-archive')
156
+ fs.mkdirSync(emptyArchive, { recursive: true })
157
+ check('no snapshot -> findSnapshot returns []', findSnapshot(emptyArchive).length === 0)
158
+ check('snapshot discovered', findSnapshot(archiveRoot).length === 1, JSON.stringify(findSnapshot(archiveRoot)))
159
+
160
+ // ② dry-run 不动盘
161
+ const dryRun = restoreEntity({ target, backupDir, archiveRoot, dry: true })
162
+ check('dry-run reports actions', dryRun.actions.length === 3 && dryRun.ok === false)
163
+ check('dry-run leaves the fork entity untouched',
164
+ fs.readFileSync(path.join(target, 'lib', 'index.js'), 'utf8').includes('FORK'))
165
+
166
+ // ③ 真还原
167
+ const real = restoreEntity({ target, backupDir, archiveRoot, dry: false })
168
+ check('restore ok', real.ok === true && real.restoredFiles === 2, JSON.stringify(real.restoredFiles))
169
+ check('entity is upstream again',
170
+ fs.readFileSync(path.join(target, 'lib', 'index.js'), 'utf8').includes('UPSTREAM'))
171
+ check('fork package.json (with fork section) is gone',
172
+ JSON.parse(fs.readFileSync(path.join(target, 'package.json'), 'utf8')).fork === undefined)
173
+ check('pre-restore snapshot captured the fork entity',
174
+ fs.readFileSync(path.join(real.preRestoreDir, 'entity-before-restore', 'lib', 'index.js'), 'utf8').includes('FORK'))
175
+ check('no leftover swap dirs',
176
+ !fs.existsSync(`${target}.restore-old-`) && fs.readdirSync(path.dirname(target)).every((n) => !n.includes('.restore-tmp-') && !n.includes('.restore-old-')),
177
+ JSON.stringify(fs.readdirSync(path.dirname(target))))
178
+
179
+ // ④ 幂等:再还原一次仍然成功(还原上游原物到上游原物)
180
+ const again = restoreEntity({ target, backupDir, archiveRoot, dry: false })
181
+ check('second restore is idempotent', again.ok === true)
182
+ check('entity still upstream', fs.readFileSync(path.join(target, 'lib', 'index.js'), 'utf8').includes('UPSTREAM'))
183
+ } catch (error) {
184
+ check('self-test ran without throwing', false, error instanceof Error ? error.message : String(error))
185
+ } finally {
186
+ fs.rmSync(sandbox, { recursive: true, force: true })
187
+ }
188
+ console.log(failures.length === 0 ? '\nrestore self-test: ALL CHECKS PASS' : `\nrestore self-test: ${failures.length} CHECK(S) FAILED`)
189
+ process.exit(failures.length === 0 ? EXIT.OK : EXIT.GATE)
190
+ }
191
+
192
+ // ── 正常路径 ─────────────────────────────────────────────────────────────────
193
+ const pa = parseArgs(process.argv.slice(2), KNOWN_ARGS)
194
+ if (!pa.ok) {
195
+ console.error(`[restore] ${describeUnknown(pa.unknown, KNOWN_ARGS)}`)
196
+ process.exit(EXIT.USAGE)
197
+ }
198
+ const dry = !!pa.flags['--dry-run']
199
+ const archiveRoot = pa.values['--archive'] ?? process.env.DSH_ARCHIVE ?? path.join(process.cwd(), '_archive')
200
+ const profileName = pa.values['--profile'] ?? 'web'
201
+ const snapshots = findSnapshot(archiveRoot)
202
+
203
+ if (pa.flags['--list']) {
204
+ console.log(snapshots.length === 0 ? `no upstream snapshots under ${archiveRoot}` : snapshots.join('\n'))
205
+ process.exit(snapshots.length === 0 ? 3 : EXIT.OK)
206
+ }
207
+
208
+ const rawTarget = pa.values['--target']
209
+ if (!rawTarget) {
210
+ console.error('[restore] 缺少 --target <DSH_HOME>:本脚本没有默认目标(写死生产路径是明令禁止的)')
211
+ process.exit(EXIT.USAGE)
212
+ }
213
+ const home = path.resolve(rawTarget)
214
+ const target = path.join(home, 'profiles', profileName, 'node_modules', 'dsh-memory')
215
+
216
+ // ── 生产闸(R-060 · 2026-10-16 接入)──────────────────────────────────────────
217
+ // 旧判据 = `home.toLowerCase() === path.resolve(defaultHome).toLowerCase()`:两个洞
218
+ // ① 只比"**等于**"默认 home ⇒ `--target <生产树内的子目录>` 一律放行(2026-10-16 实测:真把上游原物写进了
219
+ // `~\.dsh\<子目录>\profiles\...`,无人拦);
220
+ // ② 只比**逻辑**字符串、不走 realpath ⇒ junction 跨面可绕(R-060 取证⑦ V7 卡形态)。
221
+ // 新判据 = 「realpath + 逻辑路径双比」前缀判定:目标落在生产树(`$DSH_HOME`)内 ⇒
222
+ // 真还原必须显式 `--allow-production`(否则 rc=2);**dry-run 是第一道门** ⇒ 只读档不设闸(旧实现连
223
+ // `--dry-run` 打生产也拒 —— 那是误拦,本次一并修掉:只读档零写入,闸门不该拦计划)。
224
+ const gate = gateProductionWrite({
225
+ targets: [{ path: home, label: '--target' }],
226
+ allowProduction: pa.flags['--allow-production'] === true,
227
+ dryRun: dry,
228
+ op: '还原',
229
+ reason: `dsh-memory-local restore --target ${home}${dry ? '(只读 dry-run)' : '(真还原)'}`,
230
+ })
231
+ if (!gate.ok) {
232
+ console.error(refuseLine(gate))
233
+ process.exit(gate.exitCode) // 2(缺放行 = 用法错;与生态既有 --allow-production 口径一致)
234
+ }
235
+
236
+ if (snapshots.length === 0) {
237
+ console.error(`[restore] 在 ${archiveRoot} 下找不到含上游原物的 g24-s2a-memory-deploy-* 备份 —— 无还原源,拒绝动手。`)
238
+ process.exit(3)
239
+ }
240
+ const backupDir = snapshots.at(-1)
241
+
242
+ console.log(`target : ${target}`)
243
+ console.log(`restore : ${backupDir}\\official-entity`)
244
+ console.log(`archive : ${archiveRoot}`)
245
+ if (!fs.existsSync(target)) console.log('[restore] 注意:目标实体当前不存在(将直接铺上游原物)')
246
+
247
+ try {
248
+ const res = restoreEntity({ target, backupDir, archiveRoot, dry })
249
+ for (const a of res.actions) console.log(` ${dry ? 'would ' : ''}${a}`)
250
+ if (dry) { console.log('\n[restore] dry-run:未改任何文件'); process.exit(EXIT.OK) }
251
+ console.log(`\n[restore] 已还原上游原物:${res.restoredFiles} 个文件(逐文件 sha 复核通过)`)
252
+ console.log(`[restore] 还原前实体快照:${res.preRestoreDir}\\entity-before-restore`)
253
+ console.log('[restore] 未重启任何 DSH。重新装上 fork:')
254
+ console.log(` node ${path.join(path.dirname(fileURLToPath(import.meta.url)), 'deploy.mjs')} --target ${home}`)
255
+ process.exit(EXIT.OK)
256
+ } catch (error) {
257
+ console.error(`[restore] FAIL ${error instanceof Error ? error.message : String(error)}`)
258
+ process.exit(EXIT.GATE)
259
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * rollback-injector-fix.mjs —— 冲刺600-M1《注入去重修复》的**一键回滚入口**。
3
+ *
4
+ * 回滚语义(本卡专属):把生产实体 `dsh-memory` 的 `lib/injector.mjs`
5
+ * 从「修复后」退回「改动前」——**只动这一个文件**,不碰实体其余 13 个文件、不碰库、不碰 wiring。
6
+ *
7
+ * 锚(改动前后逐字节固定):
8
+ * BEFORE(改动前): fadbec42e2311b7cc2f09109a5ca3ef34e761760a409a28a84704d179c538f9c 14,939 B
9
+ * AFTER (改动后): fceb32b48aa6a057ee8d1652e91522d6aab3daa60d7f1a10e784cd49d75b71ea 16,368 B
10
+ *
11
+ * 安全口径:
12
+ * · **默认 dry-run**(只打印将要做的事 + 逐条校验,不写任何文件);`--apply` 才真写。
13
+ * · 写前把关:目标当前 sha 必须 == AFTER(否则说明实体已不是本卡交付的那一份 ⇒ fail-closed 拒绝,
14
+ * 避免把别人的改动误盖)。
15
+ * · 写前把「当前实体」备份到 `<回滚点>\rollback-runs\<stamp>\`(本动作自身也可被回看)。
16
+ * · 写后复核 sha == BEFORE,不等即报错(不回滚一半)。
17
+ * · 目标 home = 本机默认 `~/.dsh`(生产)时必须显式 `--allow-production`。
18
+ * · 默认写完后走 **P0 闸门** 请求重启(`request-restart.mjs`);`--no-restart` 跳过。
19
+ * ⚠️ 只请求、不裸杀进程;无守护鱼时本脚本**不碰** dsh。
20
+ *
21
+ * 用法:
22
+ * node rollback-injector-fix.mjs --target <DSH_HOME> [--point <回滚点目录>] [--apply] [--no-restart] [--allow-production]
23
+ * node rollback-injector-fix.mjs --status --target <DSH_HOME> # 只读:当前实体是改动前/后/其它
24
+ *
25
+ * 退出码:0 = 成功(含 dry-run 通过 / 无需回滚) · 1 = 门禁失败 · 2 = 用法错误 · 3 = 重启请求失败
26
+ */
27
+ import fs from 'node:fs'
28
+ import os from 'node:os'
29
+ import path from 'node:path'
30
+ import crypto from 'node:crypto'
31
+ import { spawnSync } from 'node:child_process'
32
+
33
+ const BEFORE = { sha: 'fadbec42e2311b7cc2f09109a5ca3ef34e761760a409a28a84704d179c538f9c', bytes: 14939, label: '改动前(0.1.5 之前的老取事件面写法)' }
34
+ const AFTER = { sha: 'fceb32b48aa6a057ee8d1652e91522d6aab3daa60d7f1a10e784cd49d75b71ea', bytes: 16368, label: '改动后(M1 修复:新面优先 + 老线回落)' }
35
+ const ARCHIVE = process.env.DSH_ARCHIVE || path.join(process.cwd(), '_archive')
36
+ const RESTART_CLI = process.env.DSH_RESTART_CLI || path.join(process.cwd(), 'tools', 'request-restart.mjs')
37
+
38
+ const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex')
39
+ const say = (s) => process.stdout.write(`${s}\n`)
40
+
41
+ // ── 参数 ──────────────────────────────────────────────────────────────────────
42
+ const argv = process.argv.slice(2)
43
+ const flag = (n) => argv.includes(`--${n}`)
44
+ const valu = (n) => { const i = argv.indexOf(`--${n}`); return i >= 0 && argv[i + 1] ? argv[i + 1] : null }
45
+ const USAGE = 'node rollback-injector-fix.mjs --target <DSH_HOME> [--point <dir>] [--apply] [--no-restart] [--allow-production] | --status'
46
+ const known = new Set(['--apply', '--dry-run', '--no-restart', '--allow-production', '--status', '--help', '--target', '--point', '--reason'])
47
+ for (let i = 0; i < argv.length; i += 1) {
48
+ const a = argv[i]
49
+ if (!a.startsWith('--')) continue
50
+ if (!known.has(a)) { say(`[rollback-m1] 未知参数 ${a}\n用法:${USAGE}`); process.exit(2) }
51
+ if ((a === '--target' || a === '--point' || a === '--reason') && !argv[i + 1]) { say(`[rollback-m1] ${a} 缺值\n用法:${USAGE}`); process.exit(2) }
52
+ if (a === '--target' || a === '--point' || a === '--reason') i += 1
53
+ }
54
+ if (flag('help') || argv.length === 0) { say(`用法:${USAGE}`); process.exit(argv.length === 0 ? 2 : 0) }
55
+
56
+ const rawTarget = valu('target')
57
+ if (!rawTarget) { say(`[rollback-m1] --target <DSH_HOME> 必填(没有默认目标)\n用法:${USAGE}`); process.exit(2) }
58
+ const home = path.resolve(rawTarget)
59
+ if (!fs.existsSync(home)) { say(`[rollback-m1] --target 不存在:${home}`); process.exit(2) }
60
+ const defaultHome = path.join(os.homedir(), '.dsh')
61
+ const isProduction = home.toLowerCase() === path.resolve(defaultHome).toLowerCase()
62
+ if (isProduction && !flag('allow-production')) {
63
+ say(`[rollback-m1] 目标 ${home} 是本机默认 home(= 生产);确认要动生产请显式追加 --allow-production。`)
64
+ process.exit(2)
65
+ }
66
+ const apply = flag('apply')
67
+ const doRestart = !flag('no-restart')
68
+ const statusOnly = flag('status')
69
+
70
+ const targetFile = path.join(home, 'profiles', 'web', 'node_modules', 'dsh-memory', 'lib', 'injector.mjs')
71
+
72
+ // ── 回滚点定位(before 快照) ──────────────────────────────────────────────────
73
+ function findPoint() {
74
+ const explicit = valu('point')
75
+ if (explicit) {
76
+ const p = path.resolve(explicit)
77
+ const f = path.join(p, 'before', 'prod-entity', 'lib', 'injector.mjs')
78
+ return fs.existsSync(f) ? { dir: p, file: f } : null
79
+ }
80
+ if (!fs.existsSync(ARCHIVE)) return null
81
+ const dirs = fs.readdirSync(ARCHIVE)
82
+ .filter((n) => n.startsWith('m1-injector-fix-'))
83
+ .map((n) => path.join(ARCHIVE, n))
84
+ .sort()
85
+ .reverse()
86
+ for (const dir of dirs) {
87
+ const f = path.join(dir, 'before', 'prod-entity', 'lib', 'injector.mjs')
88
+ if (!fs.existsSync(f)) continue
89
+ if (sha256(fs.readFileSync(f)) === BEFORE.sha) return { dir, file: f }
90
+ }
91
+ return null
92
+ }
93
+
94
+ const point = findPoint()
95
+ const currentBytes = fs.existsSync(targetFile) ? fs.readFileSync(targetFile) : null
96
+ const currentSha = currentBytes ? sha256(currentBytes) : null
97
+ const state = currentSha === AFTER.sha ? 'AFTER' : currentSha === BEFORE.sha ? 'BEFORE' : currentSha ? 'OTHER' : 'ABSENT'
98
+
99
+ say('[rollback-m1] 冲刺600-M1 注入去重修复 · 一键回滚')
100
+ say(` target : ${targetFile}`)
101
+ say(` home : ${home}${isProduction ? ' ⚠ PRODUCTION HOME' : ' [non-default home]'}`)
102
+ say(` mode : ${statusOnly ? 'status(只读)' : apply ? 'APPLY(真写)' : 'DRY-RUN(默认,不写)'}`)
103
+ say(` 当前实体 : ${currentSha ? `${currentSha} ${currentBytes.length} B` : '(文件不存在)'}`)
104
+ say(` 状态 : ${state} ${state === 'AFTER' ? '(= 本卡改动后,可回滚)' : state === 'BEFORE' ? '(= 改动前,已回滚/无需回滚)' : state === 'OTHER' ? '(⚠ 既不是改动前也不是改动后 ⇒ 拒绝覆盖,请人工确认)' : ''}`)
105
+ say(` 回滚点 : ${point ? point.dir : '(未找到含 before/prod-entity/lib/injector.mjs 且 sha 命中改动前锚的回滚点)'}`)
106
+ say(` 改动前锚 : ${BEFORE.sha} ${BEFORE.bytes} B ${BEFORE.label}`)
107
+ say(` 改动后锚 : ${AFTER.sha} ${AFTER.bytes} B ${AFTER.label}`)
108
+ if (point) {
109
+ const src = fs.readFileSync(point.file)
110
+ say(` 回滚源复核 : ${sha256(src)} ${src.length} B ${sha256(src) === BEFORE.sha && src.length === BEFORE.bytes ? '✅ 与改动前锚逐字节相同' : '❌ 不一致(回滚源被污染 ⇒ 拒绝)'}`)
111
+ }
112
+
113
+ if (statusOnly) {
114
+ say(`[rollback-m1] status 结束(只读,零写入)。`)
115
+ process.exit(0)
116
+ }
117
+
118
+ // ── 门禁 ─────────────────────────────────────────────────────────────────────
119
+ if (state === 'ABSENT') { say('[rollback-m1] 目标文件不存在 ⇒ 无法回滚(实体可能未部署)。'); process.exit(1) }
120
+ if (state === 'BEFORE') { say('[rollback-m1] 当前已是改动前版本 ⇒ 无需回滚(零写入)。'); process.exit(0) }
121
+ if (state === 'OTHER') { say('[rollback-m1] 目标 sha 既不是改动前也不是改动后 ⇒ fail-closed 拒绝覆盖(请人工确认后再动)。'); process.exit(1) }
122
+ if (!point) { say('[rollback-m1] 找不到可信回滚点 ⇒ fail-closed 中止(零写入)。'); process.exit(1) }
123
+ const srcBuf = fs.readFileSync(point.file)
124
+ if (sha256(srcBuf) !== BEFORE.sha || srcBuf.length !== BEFORE.bytes) {
125
+ say('[rollback-m1] 回滚源与改动前锚不一致 ⇒ fail-closed 中止(零写入)。')
126
+ process.exit(1)
127
+ }
128
+
129
+ if (!apply) {
130
+ say('[rollback-m1] DRY-RUN 结论:门禁全过,可安全回滚。实际执行请追加 --apply。')
131
+ say(` [dry-run] 将把 ${point.file} 覆盖到 ${targetFile}(${BEFORE.bytes} B)`)
132
+ say(` [dry-run] 覆盖前会先把当前实体备份到 ${path.join(point.dir, 'rollback-runs', '<stamp>')}`)
133
+ say(` [dry-run] 覆盖后会复核 sha == ${BEFORE.sha}`)
134
+ say(` [dry-run] ${doRestart ? '随后走 P0 闸门请求重启' : '--no-restart:不请求重启'}`)
135
+ process.exit(0)
136
+ }
137
+
138
+ // ── 真回滚 ───────────────────────────────────────────────────────────────────
139
+ const stamp = (() => { const d = new Date(); const p = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${String(d.getMilliseconds()).padStart(3, '0')}` })()
140
+ const runDir = path.join(point.dir, 'rollback-runs', stamp)
141
+ fs.mkdirSync(runDir, { recursive: true })
142
+ fs.writeFileSync(path.join(runDir, 'injector.before-rollback.mjs'), currentBytes)
143
+ fs.writeFileSync(path.join(runDir, 'MANIFEST.json'), `${JSON.stringify({
144
+ at: new Date().toISOString(), target: targetFile, home, productionHome: isProduction,
145
+ before: { sha256: currentSha, bytes: currentBytes.length },
146
+ after: { sha256: BEFORE.sha, bytes: BEFORE.bytes },
147
+ source: point.file, note: '此次回滚动作自身的留痕:before = 回滚前实体,after = 回滚后应等于改动前锚。',
148
+ }, null, 2)}\n`, 'utf8')
149
+ say(`[rollback-m1] 回滚前实体已备份:${path.join(runDir, 'injector.before-rollback.mjs')}(sha ${currentSha})`)
150
+
151
+ fs.writeFileSync(targetFile, srcBuf)
152
+ const afterSha = sha256(fs.readFileSync(targetFile))
153
+ say(`[rollback-m1] 已覆盖:${targetFile}`)
154
+ say(`[rollback-m1] 复核 sha = ${afterSha} ${afterSha === BEFORE.sha ? '✅ 与改动前锚相同' : '❌ 不一致'}`)
155
+ if (afterSha !== BEFORE.sha) { say('[rollback-m1] 回滚后复核失败 —— 请人工检查(备份与回滚源都在回滚点里)。'); process.exit(1) }
156
+
157
+ if (!doRestart) { say('[rollback-m1] --no-restart:回滚完成,未请求重启(改动要生效需自行走 P0 闸门重启)。'); process.exit(0) }
158
+
159
+ // ── P0 闸门重启 ──────────────────────────────────────────────────────────────
160
+ if (!fs.existsSync(RESTART_CLI)) { say(`[rollback-m1] 找不到重启闸门:${RESTART_CLI} ⇒ 未请求重启(回滚已生效于文件,等下次重启)。`); process.exit(3) }
161
+ const reason = valu('reason') ?? `冲刺600-M1 回滚:dsh-memory 的 lib/injector.mjs 已退回改动前(${AFTER.sha.slice(0, 8)}→${BEFORE.sha.slice(0, 8)})`
162
+ const r = spawnSync(process.execPath, [RESTART_CLI, '--reason', reason], { encoding: 'utf8', windowsHide: true })
163
+ say(`[rollback-m1] 重启闸门输出:\n${(r.stdout ?? '').trim()}`)
164
+ if (r.status !== 0) { say(`[rollback-m1] 重启请求失败(rc=${r.status}):${(r.stderr ?? '').trim().slice(0, 300)}`); process.exit(3) }
165
+ say('[rollback-m1] 回滚完成 + 已走 P0 闸门请求重启。')
166
+ process.exit(0)
@@ -0,0 +1,108 @@
1
+ /**
2
+ * FORK-EDIT(2026-09-21): 回环编码桥客户端(方案 §3.1 编码通道契约 / §3.4 fail-open)
3
+ *
4
+ * 契约(S1 已实测,见 S1 交付 §3.7):
5
+ * · 桥只绑 127.0.0.1,端口从 8201 起、被占则 +1 顺延(serve.mjs,最多 10 次)
6
+ * · GET /health → { ready, modelLoading, model, dim, port, ... }
7
+ * · POST /encode {"texts":[…]} → { vectors: [[…]], model, dim }
8
+ *
9
+ * 本模块的铁律:
10
+ * ① 桥不在 / 未就绪 / 超时 / 任何异常 ⇒ 抛错,由调用方 fail-open(绝不阻塞对话);
11
+ * ② **超时不排队**(方案 §3.4:发出即设 250 ms 超时并放弃,桥侧不做补偿)——
12
+ * 单飞守卫:上一发未回则本轮直接跳过,不堆叠请求;
13
+ * ③ 只允许回环地址(非 127.0.0.1 / localhost / ::1 一律拒,绝不给桥改绑非回环的机会);
14
+ * ④ 探活结果(含**失败**)按 TTL 缓存 ⇒ 桥不在时不会每轮都连打 10 个端口。
15
+ */
16
+ const LOOPBACK = /^(127\.0\.0\.1|localhost|::1)$/;
17
+
18
+ export function createBridgeClient({
19
+ host = '127.0.0.1',
20
+ port = 8201,
21
+ portScanMax = 10,
22
+ encodeTimeoutMs = 250,
23
+ healthTimeoutMs = 120,
24
+ healthTtlMs = 10000,
25
+ } = {}) {
26
+ if (!LOOPBACK.test(String(host))) {
27
+ throw new Error(`编码桥只允许回环地址(方案 §3.1),收到 host=${host}`);
28
+ }
29
+
30
+ let cache = null; // { ok, port, dim, model, at, error }
31
+ let inFlight = false;
32
+ let encodeCalls = 0;
33
+
34
+ const fetchJson = async (url, init, ms) => {
35
+ const res = await fetch(url, { ...init, signal: AbortSignal.timeout(ms) });
36
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
37
+ return await res.json();
38
+ };
39
+
40
+ /** 探活:从 port 起顺延,找第一个 ready 的桥;成功与失败都缓存 TTL。 */
41
+ const probe = async (force = false) => {
42
+ const now = Date.now();
43
+ if (!force && cache && now - cache.at < healthTtlMs) return cache;
44
+ let lastErr = 'unknown';
45
+ for (let p = port; p <= port + portScanMax; p += 1) {
46
+ try {
47
+ const h = await fetchJson(`http://${host}:${p}/health`, {}, healthTimeoutMs);
48
+ cache = {
49
+ ok: Boolean(h && h.ready),
50
+ port: p,
51
+ dim: Number(h && h.dim) || 0,
52
+ model: (h && h.model) || null,
53
+ modelLoading: Boolean(h && h.modelLoading),
54
+ at: Date.now(),
55
+ error: h && h.ready ? null : 'model-not-ready',
56
+ };
57
+ return cache;
58
+ } catch (e) {
59
+ lastErr = String(e && e.message ? e.message : e);
60
+ }
61
+ }
62
+ cache = { ok: false, port: null, dim: 0, model: null, at: Date.now(), error: lastErr };
63
+ return cache;
64
+ };
65
+
66
+ const encode = async (text) => {
67
+ const h = await probe();
68
+ if (!h || !h.ok) throw new Error(`编码桥不可用:${(h && h.error) || 'not ready'}`);
69
+ if (inFlight) throw new Error('编码桥上一发未返回(超时不排队,本轮跳过)');
70
+ inFlight = true;
71
+ try {
72
+ encodeCalls += 1;
73
+ const out = await fetchJson(
74
+ `http://${host}:${h.port}/encode`,
75
+ {
76
+ method: 'POST',
77
+ headers: { 'content-type': 'application/json' },
78
+ body: JSON.stringify({ texts: [String(text ?? '')] }),
79
+ },
80
+ encodeTimeoutMs,
81
+ );
82
+ const vec = out && out.vectors && out.vectors[0];
83
+ if (!vec) throw new Error('编码桥返回空向量');
84
+ return Float32Array.from(vec);
85
+ } finally {
86
+ inFlight = false;
87
+ }
88
+ };
89
+
90
+ return {
91
+ kind: 'bridge',
92
+ host,
93
+ port,
94
+ describe: () => ({ kind: 'bridge', host, port, encodeTimeoutMs, healthTtlMs, encodeCalls }),
95
+ probe,
96
+ health: () => probe(),
97
+ stats: () => ({ encodeCalls, inFlight }),
98
+ get dim() {
99
+ return cache ? cache.dim : 0;
100
+ },
101
+ get modelId() {
102
+ return cache ? cache.model : null;
103
+ },
104
+ encode,
105
+ // 桥一次一条(召回只需 1 条查询向量)
106
+ encodeBatch: async (texts) => Promise.all(texts.map((t) => encode(t))),
107
+ };
108
+ }