@a9i5k4/dsh-auto-memory 3.0.0 → 3.0.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/README.md +19 -7
- package/README.zh-CN.md +19 -7
- package/docs/FRONTEND-CO-CREATION.md +191 -0
- package/docs/GM53-HOMEPAGE-PROMPT.md +323 -0
- package/docs/HOMEPAGE-CONTENT-FOR-GM53.md +299 -0
- package/docs/PROMO-PROMPT-3.0.md +100 -0
- package/docs/USER-GUIDE.en.md +2 -2
- package/docs/USER-GUIDE.zh-CN.md +2 -2
- package/docs/WHITEPAPER.md +207 -0
- package/docs/internal/ARCHITECTURE-FOR-ZCODE-20260920.md +397 -0
- package/docs/internal/ART-DIRECTION-DEEPSEEK-20260920.md +351 -0
- package/docs/internal/ART-DIRECTION-WIREFRAME.md +191 -181
- package/docs/internal/ART-DIRECTION-WIREFRAME.md.bak-superseded +181 -0
- package/docs/internal/BATTLE-PLAN-20260917.md +871 -0
- package/docs/internal/FEATURE-INVENTORY.md +531 -0
- package/docs/internal/G-SERIES-EXECUTION-20260917.md +248 -0
- package/docs/internal/G3-DESIGN-20260918.md +82 -0
- package/docs/internal/G3-DISK-FORMAT-GAP-20260919.md +92 -0
- package/docs/internal/HANDOFF-TO-ZCODE-20260920.md +309 -0
- package/docs/internal/HERMES-DATA-VERIFICATION-20260919.md +120 -0
- package/docs/internal/HERMES-LEGACY-STATUS-20260919.md +74 -0
- package/docs/internal/ISSUE-55-58-VERIFICATION-20260918.md +175 -0
- package/docs/internal/ISSUE10-FIX-EXECUTION-20260919.md +389 -0
- package/docs/internal/ISSUE10-PLAN-20260919.md +254 -0
- package/docs/internal/ISSUE10B-FORENSICS-20260919.md +468 -0
- package/docs/internal/ISSUE9-PURGE-AND-R1-PLAIN-20260919.md +150 -0
- package/docs/internal/ISSUE9-RESIDUAL-FORENSICS-20260919.md +114 -0
- package/docs/internal/LESSON-TO-CANDIDATE-STATUS-20260919.md +79 -0
- package/docs/internal/MEMORY-GOVERNANCE-20260917.md +309 -0
- package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md +705 -0
- package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md.bak-s10 +649 -0
- package/docs/internal/PROCEDURAL-MEMORY-AND-APPROVAL-DESIGN-20260918.md +225 -0
- package/docs/internal/PROGRESS-20260917.md +93 -0
- package/docs/internal/PROMPT-GAP-AUDIT-20260920.md +128 -0
- package/docs/internal/R1-DEGRADE-AUDIT-20260918.md +163 -0
- package/docs/internal/R1-READABILITY-FORENSICS-20260919.md +127 -0
- package/docs/internal/R2-EVIDENCE-DEEP-AUDIT-20260918.md +140 -0
- package/docs/internal/R3-DEGRADE-LEDGER-DESIGN-20260918.md +138 -0
- package/docs/internal/R4-RECALL-QUOTA-PLAN-20260918.md +218 -0
- package/docs/internal/RESUME-20260918.md +171 -0
- package/docs/internal/RESUME-20260919.md +104 -0
- package/docs/internal/RHINELAB-TO-DEEPSEEK-FEASIBILITY.md +198 -0
- package/docs/internal/ROADMAP-20260917-WEEK.md +134 -0
- package/docs/internal/S10-CONSTRUCTION-HANDOFF-20260917.md +13 -3
- package/docs/internal/S10-GAP-INVENTORY-20260917.md +239 -0
- package/docs/internal/T6-EXECUTION-20260920.md +130 -0
- package/docs/internal/TELEMETRY-EFFECT-REPORT-DESIGN-20260918.md +146 -0
- package/docs/internal/THESIS-GAP-ANALYSIS-20260918.md +89 -0
- package/docs/internal/THESIS-OUTLINE-20260918.md +147 -0
- package/docs/internal/THREE-LAYER-CONTRACT.md +10 -1
- package/docs/internal/UPSTREAM-ISSUE-PR-TRIAGE-20260919.md +297 -0
- package/docs/internal/UPSTREAM-ISSUES-3RD-AUDIT-20260920.md +104 -0
- package/docs/screenshots/promo/promo-0-banner-v3.png +0 -0
- package/lib/activation-host.js +63 -9
- package/lib/board-mode.js +1 -1
- package/lib/client.js +892 -27
- package/lib/config-io.js +156 -0
- package/lib/context-bridge.js +3 -0
- package/lib/context-host.js +16 -9
- package/lib/degrade.js +385 -0
- package/lib/dsh-home.js +143 -0
- package/lib/episodic-store.js +52 -2
- package/lib/evidence-store.js +8 -1
- package/lib/fact-store.js +21 -2
- package/lib/index-sync.js +13 -1
- package/lib/index.js +1507 -158
- package/lib/intent-clean-safe.js +258 -40
- package/lib/l0-extract.js +231 -16
- package/lib/m4-corpus.js +8 -2
- package/lib/m7-index-sync-host.js +8 -1
- package/lib/memory-envelope.js +6 -1
- package/lib/memory-hub.js +127 -12
- package/lib/memory-index.js +4 -2
- package/lib/note-status-apply.js +118 -0
- package/lib/note-status.js +196 -0
- package/lib/procedure-store.js +84 -3
- package/lib/python-sidecar-client.js +29 -3
- package/lib/recall-fusion.js +83 -12
- package/lib/rules-edit.js +159 -0
- package/lib/semantic-decide.js +41 -8
- package/lib/semantic-js.js +51 -6
- package/lib/shadow-host.js +3 -5
- package/lib/skill-export-host.js +153 -0
- package/lib/skill-export.js +239 -0
- package/lib/storage-manage.js +6 -0
- package/lib/temporal-parse.js +191 -159
- package/lib/tier0-catalog.js +45 -3
- package/lib/wb-contract.js +198 -2
- package/lib/wb-sidecar.js +54 -3
- package/package.json +1 -1
package/lib/semantic-js.js
CHANGED
|
@@ -17,6 +17,7 @@ import { existsSync, readFileSync, mkdirSync, readdirSync, renameSync, statSync,
|
|
|
17
17
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
18
18
|
import { createRequire } from 'node:module'
|
|
19
19
|
import path from 'node:path'
|
|
20
|
+
import { resolveDshHomePre } from './dsh-home.js'
|
|
20
21
|
import { homedir } from 'node:os'
|
|
21
22
|
|
|
22
23
|
export const JS_SEMANTIC_ENGINE_VERSION = 'js_semantic_engine_v1'
|
|
@@ -70,7 +71,8 @@ const E5_MODELS_SUBDIR = 'multilingual-e5-small'
|
|
|
70
71
|
function defaultModelsDirCandidates(pluginDir) {
|
|
71
72
|
// 用户目录优先(#15 后续/B 修复):~/.dsh/models/js-semantic/ 跨插件升级存活——
|
|
72
73
|
// 包目录(lib/models)在 npm 更新时会被整体重装,下载的 130MB 模型曾被冲掉。
|
|
73
|
-
|
|
74
|
+
// ★#86-3:统一口径
|
|
75
|
+
const dshHome = resolveDshHomePre()
|
|
74
76
|
return [
|
|
75
77
|
path.join(dshHome, 'models', 'js-semantic'),
|
|
76
78
|
path.join(pluginDir, 'models'),
|
|
@@ -161,9 +163,20 @@ export function deepScanPeerTransformers(roots) {
|
|
|
161
163
|
* semanticAssetProbe 共享实现(index.js 的 status API / 档位解析 / 引导卡同源):
|
|
162
164
|
* 模型资产=发行包 lib/models 优先,其次开发树;peer=resolvePeerTransformersDir。
|
|
163
165
|
* extraDirs 透传给 peer 探测(深度扫描热接入位)。返回形状与 v0.1.36 完全一致。
|
|
166
|
+
*
|
|
167
|
+
* ★ issue #70 修复(2026-09-19):新增 `degraded` 字段。
|
|
168
|
+
* 旧实现是纯 `existsSync` 探测 —— **资产在磁盘上存在 ≠ 引擎可用**:
|
|
169
|
+
* 一旦运行期 `ensureTier()` 失败(onnx 损坏 / 维度不符 / peer 加载失败),引擎置 `degraded`
|
|
170
|
+
* 并此后恒抛;但探针只回 `ready = assetPresent && peerPresent` ⇒
|
|
171
|
+
* **引导卡显示「✓ 就绪」、档位解析给出 c2,而实际每次检索都在静默走词法兜底**(状态自相矛盾)。
|
|
172
|
+
* 现把降级状态并进 `ready`(并单列 `degraded` 供诊断),使三条用户可见路径(引导卡 / 档位 / status)一致。
|
|
173
|
+
* @param {string} pluginDir
|
|
174
|
+
* @param {string[]} [extraDirs]
|
|
175
|
+
* @param {string} [degradedReason] 当前引擎的降级原因(空串=未降级)
|
|
164
176
|
*/
|
|
165
|
-
export function probeJsSemanticAssets(pluginDir, extraDirs) {
|
|
166
|
-
|
|
177
|
+
export function probeJsSemanticAssets(pluginDir, extraDirs, degradedReason) {
|
|
178
|
+
// ★#86-3:统一口径
|
|
179
|
+
const dshHome = resolveDshHomePre()
|
|
167
180
|
const modelCands = [
|
|
168
181
|
path.join(dshHome, 'models', 'js-semantic', E5_MODELS_SUBDIR, 'onnx', 'model_quantized.onnx'),
|
|
169
182
|
path.join(pluginDir, 'models', E5_MODELS_SUBDIR, 'onnx', 'model_quantized.onnx'),
|
|
@@ -172,10 +185,15 @@ export function probeJsSemanticAssets(pluginDir, extraDirs) {
|
|
|
172
185
|
const modelOnnx = modelCands.find((c) => existsSync(c)) || modelCands[0]
|
|
173
186
|
const assetPresent = existsSync(modelOnnx)
|
|
174
187
|
const peerDir = resolvePeerTransformersDir(pluginDir, extraDirs)
|
|
188
|
+
const filesReady = Boolean(assetPresent && peerDir)
|
|
189
|
+
const degraded = String(degradedReason || '')
|
|
175
190
|
return {
|
|
176
191
|
assetPresent,
|
|
177
192
|
peerPresent: Boolean(peerDir),
|
|
178
|
-
|
|
193
|
+
// ★ 资产齐备**且未降级**才算就绪(否则 UI 报"就绪"而实际降级)
|
|
194
|
+
ready: filesReady && !degraded,
|
|
195
|
+
filesReady,
|
|
196
|
+
degraded,
|
|
179
197
|
assetBytes: assetPresent ? statSync(modelOnnx).size : 0,
|
|
180
198
|
assetPath: modelOnnx,
|
|
181
199
|
}
|
|
@@ -213,6 +231,8 @@ export function createJsSemanticEnginePre(opts = {}) {
|
|
|
213
231
|
let tier = null // {embedQuery, embedPassages, model}
|
|
214
232
|
let tierPromise = null
|
|
215
233
|
let degraded = '' // 非空 = 初始化失败原因(调用方回退词法)
|
|
234
|
+
let degradedAt = 0 // ★ issue #70:最近一次置位的时刻(用于有界自动重试的冷却判定)
|
|
235
|
+
let statsRetries = 0 // ★ issue #70:因冷却到期而清空降级态的累计次数(诊断用)
|
|
216
236
|
let lastRankError = '' // 运行期排名失败原因(诊断用;rank 恒返回 null 由调用方回退)
|
|
217
237
|
let idx = { miv: null, entries: [] } // entries [{memoryId, vec}]
|
|
218
238
|
let rebuilding = null // 单飞行重建 promise
|
|
@@ -223,7 +243,21 @@ export function createJsSemanticEnginePre(opts = {}) {
|
|
|
223
243
|
|
|
224
244
|
async function ensureTier() {
|
|
225
245
|
if (tier) return tier
|
|
226
|
-
|
|
246
|
+
// ★ issue #70 修复(2026-09-19):`degraded` 由**单向闩锁**改为**有界自动重试**。
|
|
247
|
+
// 旧行为:首次 `ensureTier()` 失败 ⇒ `degraded` 置位 ⇒ 此后恒抛,
|
|
248
|
+
// **唯一清除出口是测试钩子 `_resetForTest`** ⇒ 用户补齐资产 / 修好 onnx 后,
|
|
249
|
+
// 本进程内**永久** C2 降级(只能重启宿主),且 UI 仍显示"✓ 就绪"(见 probeJsSemanticAssets 的修复)。
|
|
250
|
+
// 现行为:冷却期(默认 60s,可配 `degradedRetryMs`)过后允许**再试一次**;
|
|
251
|
+
// 重试仍失败则更新时间戳继续冷却 —— 既不会每轮都去加载大模型,也不会永久闩死。
|
|
252
|
+
if (degraded) {
|
|
253
|
+
const since = Date.now() - degradedAt
|
|
254
|
+
const retryMs = Number((opts && opts.degradedRetryMs) || 60000)
|
|
255
|
+
if (since < retryMs) throw new Error(degraded)
|
|
256
|
+
// 冷却已过 ⇒ 清空降级态、允许重建(tierPromise 也一并清掉,避免复用已失败的 promise)
|
|
257
|
+
degraded = ''
|
|
258
|
+
tierPromise = null
|
|
259
|
+
statsRetries++
|
|
260
|
+
}
|
|
227
261
|
if (!tierPromise) {
|
|
228
262
|
tierPromise = (async () => {
|
|
229
263
|
if (opts.injectEmbedder) return opts.injectEmbedder
|
|
@@ -268,6 +302,7 @@ export function createJsSemanticEnginePre(opts = {}) {
|
|
|
268
302
|
return tier
|
|
269
303
|
} catch (e) {
|
|
270
304
|
degraded = String(e && e.message || e).slice(0, 160)
|
|
305
|
+
degradedAt = Date.now() // ★ issue #70:记时刻,供冷却判定
|
|
271
306
|
tierPromise = null
|
|
272
307
|
throw e
|
|
273
308
|
}
|
|
@@ -305,6 +340,10 @@ export function createJsSemanticEnginePre(opts = {}) {
|
|
|
305
340
|
assetPresent: existsSync(path.join(modelsDir(), E5_MODELS_SUBDIR, 'onnx', 'model_quantized.onnx')),
|
|
306
341
|
ready: !!tier,
|
|
307
342
|
degraded,
|
|
343
|
+
// ★ issue #70:暴露降级时刻与重试计数,供面板/诊断判断"是否处于冷却期、下次何时可重试"
|
|
344
|
+
degradedAt: degraded ? degradedAt : 0,
|
|
345
|
+
degradedRetryMs: Number((opts && opts.degradedRetryMs) || 60000),
|
|
346
|
+
degradedRetries: statsRetries,
|
|
308
347
|
lastRankError,
|
|
309
348
|
model: tier ? tier.model : null,
|
|
310
349
|
indexedRecords: idx.entries.length,
|
|
@@ -357,7 +396,7 @@ export function createJsSemanticEnginePre(opts = {}) {
|
|
|
357
396
|
const t = await ensureTier()
|
|
358
397
|
return t.embedPassages(Array.isArray(texts) ? texts : [])
|
|
359
398
|
},
|
|
360
|
-
_resetForTest() { tier = null; tierPromise = null; degraded = ''; idx = { miv: null, entries: [] }; rebuilding = null },
|
|
399
|
+
_resetForTest() { tier = null; tierPromise = null; degraded = ''; degradedAt = 0; statsRetries = 0; idx = { miv: null, entries: [] }; rebuilding = null },
|
|
361
400
|
}
|
|
362
401
|
}
|
|
363
402
|
|
|
@@ -413,6 +452,12 @@ export function createSemanticDownloaderPre(opts = {}) {
|
|
|
413
452
|
const total = Number(res.headers.get('content-length')) || fileRec.bytes
|
|
414
453
|
mkdirSync(tmpDir(), { recursive: true })
|
|
415
454
|
const dst = path.join(tmpDir(), path.basename(fileRec.rel))
|
|
455
|
+
// ★ 每次尝试从零开始(2026-09-19 上游 PR #79 / issue #65 同步落地):
|
|
456
|
+
// tmp 只在 run() 开始清一次,但 fetchToFile 会被**多个镜像依次调用**;
|
|
457
|
+
// 而写入用 flag:'a' 追加 ⇒ 源 A 断流留下的半截文件会被续上源 B 的完整流。
|
|
458
|
+
// 致命处:sha256 只对**网络流**累积(hash.update)、**不回读文件** ⇒ 拼接体校验照样通过并落位,
|
|
459
|
+
// 直到加载期才失败。故每次尝试前必须清掉残留。
|
|
460
|
+
try { rmSync(dst, { force: true }) } catch (_) {}
|
|
416
461
|
const hash = createHash('sha256')
|
|
417
462
|
const reader = res.body.getReader()
|
|
418
463
|
const fd = writeFileSync // 占位避免未用告警;真正写入走手动缓冲
|
package/lib/shadow-host.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { mkdirSync, appendFileSync, writeFileSync, readFileSync, readdirSync, existsSync, statSync, rmSync } from 'node:fs'
|
|
12
12
|
import path from 'node:path'
|
|
13
|
+
import { resolveDshHomeForEnginePre } from './dsh-home.js'
|
|
13
14
|
import { homedir } from 'node:os'
|
|
14
15
|
import { createHash } from 'node:crypto'
|
|
15
16
|
import {
|
|
@@ -126,11 +127,8 @@ export function createShadowHost({ engine }) {
|
|
|
126
127
|
/** durable audit 目录与日期分片。 */
|
|
127
128
|
function auditDir() { return path.join(dshHome(), 'memory', 'retrieval-pre', 'audit') }
|
|
128
129
|
function dshHome() {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// M4-4 修复:必须拼接 .dsh(此前漏拼导致 audit 写到 <home>/memory 错误位置)
|
|
132
|
-
const base = engine.__homedirFn ? engine.__homedirFn() : (process.env.USERPROFILE || process.env.HOME || '')
|
|
133
|
-
return base ? path.join(base, '.dsh') : '.'
|
|
130
|
+
// ★#86-3:统一口径(M4-4 的「必须拼 .dsh」约定已由 resolveDshHomePre 保证)。
|
|
131
|
+
return resolveDshHomeForEnginePre(engine)
|
|
134
132
|
}
|
|
135
133
|
|
|
136
134
|
/** 串行 durable append(§15.4):engine 级链式;失败只计 audit-write-failed 不重试不污染 Session。 */
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M9-3 Skill 导出层 · 宿主侧(IO 落地)。
|
|
3
|
+
*
|
|
4
|
+
* 纯渲染在 `skill-export.js`;本文件只负责**写盘**与**项目标注解析**。
|
|
5
|
+
*
|
|
6
|
+
* ★ 用户 2026-09-19 拍板:
|
|
7
|
+
* - ⑪-2 **晋升为 `active` 后自动导出** ⇒ 由 host 在 `activate` 成功后调用 `exportSkillForPre()`。
|
|
8
|
+
* - ⑪-3 目录 = **用户级**(`<dshHome>/skills/`,DSH 四条发现路径之一,可跨项目迁移);
|
|
9
|
+
* 导出物**必须标注适用项目**。
|
|
10
|
+
*
|
|
11
|
+
* 设计纪律:
|
|
12
|
+
* ① **fail-soft 但可观察**:返回 `{ok:false, reason}`,不抛出、不静默;
|
|
13
|
+
* ② **不自动执行**任何导出程序(程序只是参考资料);
|
|
14
|
+
* ③ 写盘走**临时文件 + rename**(避免半截文件被 DSH 当成合法 SKILL.md);
|
|
15
|
+
* ④ 不覆盖**非本插件产出**的同名目录(有 `mem-skill-` 前缀 + 自检标记才接管)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { mkdirSync, writeFileSync, renameSync, existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
19
|
+
import { join } from 'node:path'
|
|
20
|
+
import { renderSkillMarkdownPre, validateSkillMarkdownPre } from './skill-export.js'
|
|
21
|
+
|
|
22
|
+
/** 导出目录前缀(用于识别"这是本插件的产物",避免误覆盖用户手写技能)。 */
|
|
23
|
+
export const SKILL_EXPORT_PREFIX_V1 = 'mem-skill-'
|
|
24
|
+
/** 自检标记:出现在 SKILL.md 里即证明是本插件导出物。 */
|
|
25
|
+
export const SKILL_EXPORT_STAMP_V1 = 'mem-skill-export-v1'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 解析技能导出根目录。
|
|
29
|
+
* 优先级:opts.skillsRoot > <dshHome>/skills
|
|
30
|
+
* (`<dshHome>` 由调用方从 DSH 环境取得;本函数不猜路径。)
|
|
31
|
+
*/
|
|
32
|
+
export function resolveSkillsRootPre(opts = {}) {
|
|
33
|
+
if (opts.skillsRoot) return String(opts.skillsRoot)
|
|
34
|
+
const home = String(opts.dshHome || '').trim()
|
|
35
|
+
if (!home) return null
|
|
36
|
+
return join(home, 'skills')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 从项目根路径推出一个稳定的项目名(用于 ⑪-3 的「适用项目」标注)。 */
|
|
40
|
+
export function projectNameFromPre(projectPath) {
|
|
41
|
+
const p = String(projectPath || '').trim().replace(/[\\/]+$/, '')
|
|
42
|
+
if (!p) return ''
|
|
43
|
+
const base = p.split(/[\\/]/).filter(Boolean).pop() || ''
|
|
44
|
+
return base
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 收集可附带的程序文件(⑪-1):只收**明确的程序后缀**,不递归、不猜。 */
|
|
48
|
+
export function collectProgramsPre(projectPath, names) {
|
|
49
|
+
const out = []
|
|
50
|
+
if (!Array.isArray(names) || !names.length) return out
|
|
51
|
+
for (const n of names) {
|
|
52
|
+
const s = String(n || '').trim()
|
|
53
|
+
if (!s) continue
|
|
54
|
+
// 只认相对文件名,防目录穿越
|
|
55
|
+
if (s.includes('..') || /^[\\/]/.test(s) || /^[a-zA-Z]:/.test(s)) continue
|
|
56
|
+
out.push(s)
|
|
57
|
+
}
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readIfExists(p) {
|
|
62
|
+
try { return existsSync(p) ? readFileSync(p, 'utf8') : null } catch (_) { return null }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 导出一个 procedure 为 `<skillsRoot>/<dirName>/SKILL.md`。
|
|
67
|
+
*
|
|
68
|
+
* @param {object} procedure procedure 记录(stage 应为 active)
|
|
69
|
+
* @param {object} opts
|
|
70
|
+
* @param {string} opts.skillsRoot 技能根目录(必填;见 resolveSkillsRootPre)
|
|
71
|
+
* @param {string} opts.projectPath 项目根绝对路径
|
|
72
|
+
* @param {string} opts.projectName 项目名(缺省由 projectPath 推导)
|
|
73
|
+
* @param {string[]} opts.programs 附带程序相对路径列表
|
|
74
|
+
* @param {string} opts.exportedAt ISO 时间
|
|
75
|
+
* @param {boolean} opts.force 为 true 时允许覆盖**非本插件**的同名目录
|
|
76
|
+
* @returns {{ok:boolean, reason?:string, dir?:string, file?:string, bytes?:number}}
|
|
77
|
+
*/
|
|
78
|
+
export function exportSkillForPre(procedure, opts = {}) {
|
|
79
|
+
try {
|
|
80
|
+
const skillsRoot = String(opts.skillsRoot || '')
|
|
81
|
+
if (!skillsRoot) return { ok: false, reason: 'no-skills-root' }
|
|
82
|
+
|
|
83
|
+
const projectPath = String(opts.projectPath || '')
|
|
84
|
+
const projectName = String(opts.projectName || '').trim() || projectNameFromPre(projectPath)
|
|
85
|
+
if (!projectName) return { ok: false, reason: 'no-project-name' }
|
|
86
|
+
|
|
87
|
+
const programs = collectProgramsPre(projectPath, opts.programs)
|
|
88
|
+
const r = renderSkillMarkdownPre(procedure, {
|
|
89
|
+
projectName,
|
|
90
|
+
projectPath,
|
|
91
|
+
programs,
|
|
92
|
+
exportedAt: opts.exportedAt || new Date().toISOString(),
|
|
93
|
+
})
|
|
94
|
+
if (!r.ok) return { ok: false, reason: r.reason }
|
|
95
|
+
|
|
96
|
+
// 自带标记 + 完整性自检(判据 = 用户 ⑪-1/⑪-3 的硬要求)
|
|
97
|
+
const content = r.content + '\n<!-- ' + SKILL_EXPORT_STAMP_V1 + ' -->\n'
|
|
98
|
+
const v = validateSkillMarkdownPre(content, { projectName, hasPrograms: programs.length > 0 })
|
|
99
|
+
if (!v.ok) return { ok: false, reason: 'notice-incomplete:' + v.problems.join('|') }
|
|
100
|
+
|
|
101
|
+
const dir = join(skillsRoot, r.dirName)
|
|
102
|
+
// 防误覆盖:目录已存在且**不是**本插件产物 ⇒ 拒绝(除非 force)
|
|
103
|
+
if (existsSync(dir) && !opts.force) {
|
|
104
|
+
const old = join(dir, 'SKILL.md')
|
|
105
|
+
const prev = readIfExists(old)
|
|
106
|
+
const isOurs = prev != null && prev.includes(SKILL_EXPORT_STAMP_V1)
|
|
107
|
+
const isOursByName = r.dirName.startsWith(SKILL_EXPORT_PREFIX_V1)
|
|
108
|
+
if (!isOurs && isOursByName) {
|
|
109
|
+
// 按名字是我们的,但内容不是 ⇒ 用户手写过,别动
|
|
110
|
+
if (prev != null) return { ok: false, reason: 'dir-occupied-by-foreign' }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
mkdirSync(dir, { recursive: true })
|
|
115
|
+
// 临时文件 + rename ⇒ 避免半截文件被 DSH 扫描到
|
|
116
|
+
const tmp = join(dir, '.SKILL.md.tmp-' + process.pid + '-' + Date.now())
|
|
117
|
+
const file = join(dir, 'SKILL.md')
|
|
118
|
+
writeFileSync(tmp, content, 'utf8')
|
|
119
|
+
renameSync(tmp, file)
|
|
120
|
+
|
|
121
|
+
return { ok: true, dir, file, bytes: Buffer.byteLength(content, 'utf8'), dirName: r.dirName }
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return { ok: false, reason: 'export-failed:' + String((e && e.message) || e) }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 列出已导出的技能(供诊断/GUI)。 */
|
|
128
|
+
export function listExportedSkillsPre(skillsRoot) {
|
|
129
|
+
const root = String(skillsRoot || '')
|
|
130
|
+
if (!root || !existsSync(root)) return []
|
|
131
|
+
const out = []
|
|
132
|
+
let entries = []
|
|
133
|
+
try { entries = readdirSync(root) } catch (_) { return [] }
|
|
134
|
+
for (const name of entries) {
|
|
135
|
+
if (!name.startsWith(SKILL_EXPORT_PREFIX_V1)) continue
|
|
136
|
+
const dir = join(root, name)
|
|
137
|
+
try { if (!statSync(dir).isDirectory()) continue } catch (_) { continue }
|
|
138
|
+
const prev = readIfExists(join(dir, 'SKILL.md'))
|
|
139
|
+
if (prev == null || !prev.includes(SKILL_EXPORT_STAMP_V1)) continue
|
|
140
|
+
const nameLine = /^name:\s*(.+)$/m.exec(prev)
|
|
141
|
+
const projLine = /适用项目\*\*:`([^`]+)`/.exec(prev)
|
|
142
|
+
out.push({
|
|
143
|
+
dirName: name,
|
|
144
|
+
dir,
|
|
145
|
+
name: nameLine ? nameLine[1].trim() : name,
|
|
146
|
+
project: projLine ? projLine[1] : '',
|
|
147
|
+
bytes: Buffer.byteLength(prev, 'utf8'),
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
return out
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { renderSkillMarkdownPre, validateSkillMarkdownPre } from './skill-export.js'
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M9-3 Skill 导出层(procedure → SKILL.md 目录束)。
|
|
3
|
+
*
|
|
4
|
+
* ★ 2026-09-19 用户拍板(三项):
|
|
5
|
+
* ⑪-1 形态:`SKILL.md` + **附上过程中用到的程序**(如 `.py`),且 SKILL.md 里
|
|
6
|
+
* 必须明写:这些程序**只是参考性的** —— 若当前做的事情与之前**根本不同**,
|
|
7
|
+
* 可以用来**迁移**,**不能直接运行**。
|
|
8
|
+
* ⑪-2 时机:**晋升为 `active` 后自动导出**。
|
|
9
|
+
* ⑪-3 目录:**用户级**(软件层面,可跨项目迁移);但导出物**必须标注适用于哪个项目**,
|
|
10
|
+
* 项目不一样时**只作参考,不能直接用**。
|
|
11
|
+
*
|
|
12
|
+
* 本模块是**纯渲染层**(无 IO、无状态),便于直接单测:
|
|
13
|
+
* - `renderSkillMarkdownPre(procedure, opts)` → SKILL.md 文本
|
|
14
|
+
* - `skillDirNamePre(procedure)` → 目录名(稳定、可预测)
|
|
15
|
+
* - `SKILL_USAGE_NOTICE_V1` → 使用约束条款(导出物必须原样包含)
|
|
16
|
+
*
|
|
17
|
+
* 设计纪律(与 procedure 引擎同源):
|
|
18
|
+
* ① **不新增状态源** —— 导出物是 procedure 的**派生物**,不是新的事实来源;
|
|
19
|
+
* ② **不自动执行任何导出的程序** —— 程序是**参考资料**,不是可调用入口;
|
|
20
|
+
* ③ 全部 fail-soft,但**返回可观察结果**(ok/reason),不静默。
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// ★ T7-b(2026-09-20):纯计算依赖,用于给「纯中文标题」生成稳定的 ASCII 目录名。
|
|
24
|
+
// node 内建模块,不引入外部依赖、不做 IO、不持有状态 —— 与本模块「纯渲染层」定位不冲突。
|
|
25
|
+
import { createHash } from 'node:crypto'
|
|
26
|
+
|
|
27
|
+
/** 导出物的使用约束条款 —— **必须原样出现在每个 SKILL.md 中**(用户 ⑪-1 硬要求)。 */
|
|
28
|
+
export const SKILL_USAGE_NOTICE_V1 = [
|
|
29
|
+
'> **⚠️ 使用约束(必读)**',
|
|
30
|
+
'>',
|
|
31
|
+
'> 本技能由 **dsh-auto-memory** 从一次真实工作过程**自动沉淀**而来,**不是通用最佳实践**。',
|
|
32
|
+
'>',
|
|
33
|
+
'> 1. **附带程序仅供参考**:本目录下的脚本(如 `.py`)是**当时那次工作用过的程序**,',
|
|
34
|
+
'> **不是可直接调用的工具**。它们可能依赖当时的路径、环境变量、数据格式或版本。',
|
|
35
|
+
'> 2. **场景根本不同时 → 只做迁移,不要直接运行**:若当前任务与下方「来源」描述的场景',
|
|
36
|
+
'> **有根本差异**,请把附带的程序**当作思路参考**,按当前场景**重写**,而不是直接执行。',
|
|
37
|
+
'> 3. **跨项目使用须先核对**:本技能的「适用项目」若与当前项目**不一致**,',
|
|
38
|
+
'> **一律只作参考**,不得直接套用其路径、命令与判据。',
|
|
39
|
+
'> 4. **高风险步骤需人工确认**:涉及删除、发布、付费、外部发送等动作,必须先向用户复述确认。',
|
|
40
|
+
].join('\n')
|
|
41
|
+
|
|
42
|
+
/** SKILL.md 中必须出现的约束锚点(供套件断言,防止被后续改动悄悄删掉)。 */
|
|
43
|
+
export const SKILL_NOTICE_ANCHORS_V1 = Object.freeze([
|
|
44
|
+
'使用约束(必读)',
|
|
45
|
+
'附带程序仅供参考',
|
|
46
|
+
'只做迁移,不要直接运行',
|
|
47
|
+
'跨项目使用须先核对',
|
|
48
|
+
'高风险步骤需人工确认',
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
const MAX_TITLE_LEN = 80
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 目录名 / `name:` 字段:`mem-skill-<slug>-<procId前12位>` —— 稳定、可预测、不冲突。
|
|
55
|
+
*
|
|
56
|
+
* ★ T7-b(2026-09-20 用户报「所有中文技能都叫 untitled」)修复:
|
|
57
|
+
* **根因**:原实现 `title.replace(/[^a-z0-9]+/g,'-')` 只保留 ASCII ⇒ **纯中文标题 slug 成空串**
|
|
58
|
+
* ⇒ 一律回落字面量 `'untitled'`。后果是每个中文技能都叫 `mem-skill-untitled-<id>`,
|
|
59
|
+
* 用户看到的技能列表名字毫无意义(实测:本机导出的
|
|
60
|
+
* `~/.dsh/skills/mem-skill-untitled-aa2153f33507/SKILL.md`)。
|
|
61
|
+
*
|
|
62
|
+
* **为什么不能用「保留中文」这个修法**:DSH 宿主对 skill name 是**硬校验**,不是建议 ——
|
|
63
|
+
* `SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/`(宿主 `index.js:17`),且
|
|
64
|
+
* `:481` 直接 `throw new Error('loaded skill has invalid name "…"')`。
|
|
65
|
+
* ⇒ 中文进 name 会让**技能加载直接失败**(比 untitled 更糟)。故必须产出 ASCII 安全名。
|
|
66
|
+
*
|
|
67
|
+
* **采用的修法**:slug 为空时,取标题的 **sha1 前 8 位十六进制**做 `t-<hash>` 前缀。
|
|
68
|
+
* - ASCII 安全:`t-1a2b3c4d` 完全符合上面的 SKILL_NAME 正则;
|
|
69
|
+
* - 稳定:同一 title ⇒ 同一 hash ⇒ 目录名不变(自动化重导出不会产生新目录);
|
|
70
|
+
* - 可区分:不同标题 hash 不同,不再出现「一堆 untitled」;
|
|
71
|
+
* - 中文原文**不丢**:仍完整出现在 SKILL.md 的 `# <title>` 与 `description` 里,
|
|
72
|
+
* 用户/模型在技能列表看到的是中文标题,name 只承担「唯一标识」职责。
|
|
73
|
+
*
|
|
74
|
+
* ⚠️ 哈希算法用 node 内建 `createHash('sha1')`(纯计算,不引入 IO/状态),
|
|
75
|
+
* 与 FNV-1a 那种"自造哈希"区分开 —— 本仓有过 sha256Hex 同名异义的教训(#86-1)。
|
|
76
|
+
*/
|
|
77
|
+
export function skillDirNamePre(procedure) {
|
|
78
|
+
const id = String((procedure && procedure.procedureId) || '')
|
|
79
|
+
const short = id.replace(/^proc_/, '').slice(0, 12)
|
|
80
|
+
const title = String((procedure && procedure.title) || '')
|
|
81
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40)
|
|
82
|
+
// 纯中文(或纯符号)标题:ASCII slug 为空 ⇒ 用标题哈希代替,绝不留 'untitled'
|
|
83
|
+
const stem = slug || ('t-' + createHash('sha1').update(title, 'utf8').digest('hex').slice(0, 8))
|
|
84
|
+
return 'mem-skill-' + stem + '-' + (short || 'noid')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 一句话摘要(取步骤首条或标题)——用于 SKILL.md 的 description。 */
|
|
88
|
+
/**
|
|
89
|
+
* 一句话摘要 —— 用于 SKILL.md 的 `description:` 字段。
|
|
90
|
+
*
|
|
91
|
+
* ★ T7-b(2026-09-20)修正取值优先级:**title 优先,steps[0] 兜底**(原来是反的)。
|
|
92
|
+
* 为什么反了不行:`description` 是模型在技能目录里看到的那句话,应当回答
|
|
93
|
+
* 「这个技能**是干什么的**」—— title 正是这件事;而 steps[0] 是**第一个动作**
|
|
94
|
+
* (实测导出的描述成了「备份:Copy-Item lib/index.js …」,完全看不出技能用途)。
|
|
95
|
+
* 仅当 title 缺失时才用 steps[0] 兜底(此时有总比没有好)。
|
|
96
|
+
*/
|
|
97
|
+
function summaryOfPre(p) {
|
|
98
|
+
const t = String(p.title || '').trim()
|
|
99
|
+
const step = Array.isArray(p.steps) && p.steps.length ? String(p.steps[0]).trim() : ''
|
|
100
|
+
const s = t || step
|
|
101
|
+
return s.length > MAX_TITLE_LEN ? s.slice(0, MAX_TITLE_LEN - 1) + '…' : s
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 渲染 SKILL.md。
|
|
106
|
+
*
|
|
107
|
+
* @param {object} procedure procedure 记录(stage 应为 active)
|
|
108
|
+
* @param {object} opts
|
|
109
|
+
* @param {string} opts.projectName 适用项目名(**必填**,⑪-3)
|
|
110
|
+
* @param {string} opts.projectPath 项目根绝对路径(用于溯源)
|
|
111
|
+
* @param {string[]} opts.programs 附带程序文件名列表(⑪-1,仅列名)
|
|
112
|
+
* @param {string} opts.exportedAt 导出时间(ISO 字符串;缺省由调用方给)
|
|
113
|
+
* @returns {{ok:true, fileName:string, dirName:string, content:string} | {ok:false, reason:string}}
|
|
114
|
+
*/
|
|
115
|
+
export function renderSkillMarkdownPre(procedure, opts = {}) {
|
|
116
|
+
const p = procedure || {}
|
|
117
|
+
if (!p.procedureId) return { ok: false, reason: 'no-procedure-id' }
|
|
118
|
+
if (!p.title) return { ok: false, reason: 'no-title' }
|
|
119
|
+
const projectName = String(opts.projectName || '').trim()
|
|
120
|
+
if (!projectName) return { ok: false, reason: 'no-project-name' } // ⑪-3 硬要求:必须标项目
|
|
121
|
+
const projectPath = String(opts.projectPath || '').trim()
|
|
122
|
+
const programs = Array.isArray(opts.programs) ? opts.programs.filter(Boolean).map(String) : []
|
|
123
|
+
const exportedAt = String(opts.exportedAt || '')
|
|
124
|
+
|
|
125
|
+
const L = []
|
|
126
|
+
L.push('---')
|
|
127
|
+
L.push('name: ' + skillDirNamePre(p))
|
|
128
|
+
L.push('description: ' + summaryOfPre(p).replace(/\r?\n/g, ' '))
|
|
129
|
+
L.push('---')
|
|
130
|
+
L.push('')
|
|
131
|
+
L.push('# ' + String(p.title).trim())
|
|
132
|
+
L.push('')
|
|
133
|
+
L.push(SKILL_USAGE_NOTICE_V1)
|
|
134
|
+
L.push('')
|
|
135
|
+
|
|
136
|
+
// ── 来源(⑪-3:必须标注适用于哪个项目)──
|
|
137
|
+
L.push('## 来源')
|
|
138
|
+
L.push('')
|
|
139
|
+
L.push('- **适用项目**:`' + projectName + '`' + (projectPath ? '(`' + projectPath + '`)' : ''))
|
|
140
|
+
L.push('- **沉淀自**:dsh-auto-memory 技能库(procedure `' + p.procedureId + '`)')
|
|
141
|
+
if (p.riskLevel) L.push('- **风险等级**:`' + p.riskLevel + '`' + (p.requiresApproval ? '(需人工批准)' : ''))
|
|
142
|
+
if (exportedAt) L.push('- **导出时间**:' + exportedAt)
|
|
143
|
+
L.push('')
|
|
144
|
+
L.push('> 若你的当前项目**不是** `' + projectName + '`,请**只作参考**,不要直接套用下方路径与命令。')
|
|
145
|
+
L.push('')
|
|
146
|
+
|
|
147
|
+
// ── 适用条件 ──
|
|
148
|
+
if (Array.isArray(p.preconditions) && p.preconditions.length) {
|
|
149
|
+
L.push('## 何时适用')
|
|
150
|
+
L.push('')
|
|
151
|
+
for (const x of p.preconditions) L.push('- ' + String(x))
|
|
152
|
+
L.push('')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── 步骤 ──
|
|
156
|
+
if (Array.isArray(p.steps) && p.steps.length) {
|
|
157
|
+
L.push('## 步骤')
|
|
158
|
+
L.push('')
|
|
159
|
+
p.steps.forEach((s, i) => L.push(String(i + 1) + '. ' + String(s)))
|
|
160
|
+
L.push('')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── 检查点 ──
|
|
164
|
+
if (Array.isArray(p.checks) && p.checks.length) {
|
|
165
|
+
L.push('## 检查点')
|
|
166
|
+
L.push('')
|
|
167
|
+
for (const x of p.checks) L.push('- [ ] ' + String(x))
|
|
168
|
+
L.push('')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── 成功判据 ──
|
|
172
|
+
if (Array.isArray(p.successCriteria) && p.successCriteria.length) {
|
|
173
|
+
L.push('## 成功判据')
|
|
174
|
+
L.push('')
|
|
175
|
+
for (const x of p.successCriteria) L.push('- ' + String(x))
|
|
176
|
+
L.push('')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── 回滚 ──
|
|
180
|
+
if (Array.isArray(p.rollback) && p.rollback.length) {
|
|
181
|
+
L.push('## 回滚')
|
|
182
|
+
L.push('')
|
|
183
|
+
for (const x of p.rollback) L.push('- ' + String(x))
|
|
184
|
+
L.push('')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── 附带程序(⑪-1)──
|
|
188
|
+
if (programs.length) {
|
|
189
|
+
L.push('## 附带程序(**参考性**)')
|
|
190
|
+
L.push('')
|
|
191
|
+
L.push('以下文件是**当时那次工作用过的程序**,随本技能一起导出:')
|
|
192
|
+
L.push('')
|
|
193
|
+
for (const f of programs) L.push('- `' + f + '`')
|
|
194
|
+
L.push('')
|
|
195
|
+
L.push('> **不要直接运行**。先读一遍,判断它与当前场景的差异;')
|
|
196
|
+
L.push('> 若场景根本不同,请**按当前场景迁移重写**;确需运行时,先向用户说明并确认。')
|
|
197
|
+
L.push('')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── 证据 ──
|
|
201
|
+
if (Array.isArray(p.sourceEpisodes) && p.sourceEpisodes.length) {
|
|
202
|
+
L.push('## 依据')
|
|
203
|
+
L.push('')
|
|
204
|
+
L.push('- 来源 episode 数:' + p.sourceEpisodes.length)
|
|
205
|
+
if (p.evidence && typeof p.evidence === 'object') {
|
|
206
|
+
const ev = p.evidence
|
|
207
|
+
const parts = []
|
|
208
|
+
if (Number.isFinite(ev.seen)) parts.push('seen=' + ev.seen)
|
|
209
|
+
if (Number.isFinite(ev.success)) parts.push('success=' + ev.success)
|
|
210
|
+
if (Number.isFinite(ev.correction)) parts.push('correction=' + ev.correction)
|
|
211
|
+
if (parts.length) L.push('- 证据统计:' + parts.join(' / '))
|
|
212
|
+
}
|
|
213
|
+
L.push('')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { ok: true, fileName: 'SKILL.md', dirName: skillDirNamePre(p), content: L.join('\n') + '\n' }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 导出完整性校验(供套件与调用方共用)。
|
|
221
|
+
* **判据是"用户 ⑪-1/⑪-3 的硬要求"本身**,不是实现细节。
|
|
222
|
+
*/
|
|
223
|
+
export function validateSkillMarkdownPre(content, expect = {}) {
|
|
224
|
+
const s = String(content == null ? '' : content)
|
|
225
|
+
const problems = []
|
|
226
|
+
if (!s.trim()) problems.push('empty')
|
|
227
|
+
for (const a of SKILL_NOTICE_ANCHORS_V1) if (!s.includes(a)) problems.push('missing-notice:' + a)
|
|
228
|
+
if (!/^---\r?\n[\s\S]*?\r?\n---/.test(s)) problems.push('no-frontmatter')
|
|
229
|
+
if (!/^name:\s*\S+/m.test(s)) problems.push('no-name')
|
|
230
|
+
if (!/^description:\s*\S+/m.test(s)) problems.push('no-description')
|
|
231
|
+
if (expect.projectName && !s.includes('`' + expect.projectName + '`')) problems.push('no-project-tag')
|
|
232
|
+
// ⑪-1:附带程序必须被运行约束**包裹**,而不是只列个名。
|
|
233
|
+
// ★ 判据必须锚定属于「程序块」的形态:
|
|
234
|
+
// 顶层约束条款里也出现「不要直接运行」四字(后接 `**:`),
|
|
235
|
+
// 只用该四字做判据会**同义反复**(永远为真)——套件 [5] 组已实跑抓到这一版缺陷。
|
|
236
|
+
// 程序块独有形态 = 加粗短语**后紧跟句号**:`**不要直接运行**。`
|
|
237
|
+
if (expect.hasPrograms && !/\*\*不要直接运行\*\*。/.test(s)) problems.push('programs-not-constrained')
|
|
238
|
+
return { ok: problems.length === 0, problems }
|
|
239
|
+
}
|
package/lib/storage-manage.js
CHANGED
|
@@ -116,6 +116,12 @@ export function createStorageManagerPre(opts = {}) {
|
|
|
116
116
|
/** 读取既有 sidecar(尽力而为):用于 rebuildSidecar 继承 epoch/version,避免无谓的 epoch 漂移。 */
|
|
117
117
|
function readSidecarPrev(file) {
|
|
118
118
|
try {
|
|
119
|
+
// ★ issue #55 修复(2026-09-18,2026-09-19 补回):函数体内**自行活读** docStore。
|
|
120
|
+
// 旧写法直接引用闭包里的 `docStore`,但该名只在 `repair`/`deleteMemory` 内部局部声明
|
|
121
|
+
// ⇒ 本函数作用域内**未定义** ⇒ 每次调用抛 ReferenceError ⇒ 被下方 `catch (_)` 吞掉
|
|
122
|
+
// ⇒ **恒返回 null** ⇒ `rebuildSidecar` 拿不到 prev ⇒ epoch 漂移、fresh 被翻成 stale。
|
|
123
|
+
// 必须走 `docStoreOf()` 工厂活读(getter 语义:memoryAnchorEnabled 后续生效也能拿到)。
|
|
124
|
+
const docStore = docStoreOf()
|
|
119
125
|
if (!docStore || typeof docStore.sidecarPath !== 'function') return null
|
|
120
126
|
const sp = docStore.sidecarPath(file)
|
|
121
127
|
if (!sp) return null
|