dsh-experience 1.0.14 → 1.1.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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,22 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## [1.1.0] — 2026-11-09 — 平台隔离与适用边界
5
+
6
+ ### 新增
7
+ - **平台标签**:`buildCandidate()` 写入 `platform: process.platform`;`platformConflicts()` 在召回与合并两条路径上拦截跨 OS 记录。
8
+ 起因:`& "<node.exe>"` 这类 win32 经验在 darwin 上不成立,却会被当通用知识注入。
9
+ - **README「适用边界」**:写明单用户、单机、按 OS 隔离、需人批准、不替代 AGENTS.md 五条边界。
10
+ - 新增文档徽章改为 GitHub Actions 工作流徽章(不再手写测试数字)。
11
+
12
+ ### 测试
13
+ - T82:捕获的记录带 `platform`;`platformOf` 对无标签记录返回空串。
14
+ - T83:异平台记录 `platformConflicts=true`,同平台与无标签均为 `false`。
15
+ - `selftest 93 pass / 0 fail`、`e2e 23 pass / 0 fail`。
16
+
17
+ ### 兼容
18
+ - 旧记录没有 `platform` 字段,按「与本机兼容」处理,不会被误判为异平台。
19
+
4
20
  ## [1.0.14] — 2026-11-09 — A/B 效率实测记录
5
21
 
6
22
  ### 记录
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **带生命周期治理与指纹防伪的 DSH 跨会话自学习插件** —— 同类任务第一次慢,第二次直接用上第一次的经验,但绝不把一次偶然的变通当成真理。
4
4
 
5
- [![tests](https://img.shields.io/badge/tests-114%20passing-brightgreen)](tools/selftest.mjs)
5
+ [![tests](https://github.com/Aatzer0/dsh-experience/actions/workflows/test.yml/badge.svg)](https://github.com/Aatzer0/dsh-experience/actions/workflows/test.yml)
6
6
  [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
7
  [![runtime](https://img.shields.io/badge/runtime-DeepSeek%20Harness-purple)](https://github.com/Aatzer0/dsh-experience)
8
8
 
@@ -29,6 +29,8 @@ Agent 每次会话都从零开始:读同样的配置文件、踩同样的坑
29
29
  | **零性能损耗** | 捕获是 O(≤40 次调用) 的纯字符串运算,实测 `avgCaptureMs: 1`;召回在本地做词覆盖打分,无网络、无向量库 | `experience_debug` |
30
30
  | **可观测** | 六层诊断逐层 PASS/FAIL,直接回答「为什么这次没命中」 | `/memory debug` |
31
31
 
32
+ - **平台隔离** —— 每条经验记录它是在哪个 OS 上学到的;`win32` 的「PATH 里没有 node」不会被拿到 `darwin` 上当事实用,跨平台既不合并也不召回。
33
+
32
34
  ## 为什么经验必须由你批准
33
35
 
34
36
  自动捕获的经验只会进入 `candidate` 状态,**永远不会被注入**。只有你执行 `/memory approve <id>` 之后,它才变成 `verified` 并进入召回池。Agent 侧没有任何写入状态的入口——`experience_record` 只能新建候选,`experience_review` 是只读的,「批准」这个动作只存在于 `/memory` 命令里。
@@ -69,7 +71,7 @@ flowchart LR
69
71
  ### 方式一:从 npm 安装(推荐)
70
72
 
71
73
  ```text
72
- plugin_manager install_bundle target=dsh-experience@1.0.5
74
+ plugin_manager install_bundle target=dsh-experience
73
75
  ```
74
76
 
75
77
  安装后 `plugin_manager list_plugins` 应出现 `dsh-experience` 且 `fiberPhase: active`。
@@ -130,6 +132,16 @@ plugin_manager install_bundle target=link:<PATH-TO-dsh-experience>
130
132
  `#error`/`#node`/`#module` 同时命中再加确定性加分。实测同一批失败查询上,正确条目的分数从 0.07 提升到 0.36–0.50,
131
133
  并在清理流水账后稳定排第 1。
132
134
 
135
+ ## 适用边界
136
+
137
+ 这个插件是给**一个人一台机器**的长期协作场景设计的,不是团队共享服务:
138
+
139
+ - **单用户**:经验库在 `~/.dsh/experience/`,没有账号体系、没有并发写保护、没有服务端。
140
+ - **单机**:网络位置、绝对路径、工具链安装方式都随机器而变;跨机器共享需要你自己想办法(同步文件、或用 `/memory teach` 重建)。
141
+ - **按 OS 隔离**:v1.1 起每条记录带 `platform` 标签,跨平台不合并、不召回。
142
+ - **需要人批准**:自动捕获只到 `candidate`,注入池里永远只有你亲手批过的 `verified`。
143
+ - **不替代 AGENTS.md**:经验是"上次怎么做的",规则是"必须怎么做"。两者冲突时以规则为准。
144
+
133
145
  ## 已知限制
134
146
 
135
147
  - 召回仅在每轮第 1 步生效;
@@ -142,7 +154,7 @@ plugin_manager install_bundle target=link:<PATH-TO-dsh-experience>
142
154
 
143
155
  ```bash
144
156
  node tools/selftest.mjs # 91 pass / 0 fail(纯函数 + 闸门 + 衰减)
145
- node tools/e2e.mjs # 19 pass / 0 fail(假 Cordis context 驱动真实 apply())
157
+ node tools/e2e.mjs # 23 pass / 0 fail(假 Cordis context 驱动真实 apply())
146
158
  ```
147
159
 
148
160
  零依赖、零构建步骤:修改 `index.js` / `lib/*.js` 后重载插件即可。贡献流程见 [CONTRIBUTING.md](CONTRIBUTING.md)。
package/index.js CHANGED
@@ -273,6 +273,7 @@ export function apply(ctx) {
273
273
 
274
274
  function recallable(experience) {
275
275
  if (!experience) return false
276
+ if (C.platformConflicts(experience)) return false
276
277
  if (experience.decay && experience.decay.state === 'dormant' && !D.isPinned(experience)) return false
277
278
  if (experience.status === 'verified') return true
278
279
  return CONFIG.allowAutoVerified === true && experience.status === 'auto-verified'
@@ -611,10 +612,19 @@ export function apply(ctx) {
611
612
  return true
612
613
  }
613
614
 
615
+ // The same recipe on another OS is a different recipe.
616
+ let platformVeto = null
617
+ const platformBlocked = (existing) => {
618
+ if (!C.platformConflicts(existing)) return false
619
+ if (!platformVeto) platformVeto = { id: existing.id, was: C.platformOf(existing), now: process.platform }
620
+ return true
621
+ }
622
+
614
623
  let fpBest = null
615
624
  for (const existing of store.experiences) {
616
625
  if (!mergeable.includes(existing.status)) continue
617
626
  if (!sameScope(existing)) continue
627
+ if (platformBlocked(existing)) continue
618
628
  if (intentBlocked(existing)) continue
619
629
  if (!C.fingerprintsMatch(existing, draft)) continue
620
630
  if (!fpBest) fpBest = existing
@@ -625,6 +635,7 @@ export function apply(ctx) {
625
635
  if (!mergeable.includes(existing.status)) continue
626
636
  if (!sameScope(existing)) continue
627
637
  if (existing.taskType && draft.taskType && existing.taskType !== draft.taskType) continue
638
+ if (platformBlocked(existing)) continue
628
639
  if (intentBlocked(existing)) continue
629
640
  const score = scoreExperience(existing, queryTokens, index)
630
641
  if (score >= CONFIG.mergeThreshold && (!best || score > best.score)) best = { existing, score }
@@ -675,7 +686,7 @@ export function apply(ctx) {
675
686
  noteEvidence(created, trace, 'success', Boolean(draft.fingerprint && draft.fingerprint.green), runSteps)
676
687
  outcome.ok = true
677
688
  outcome.id = id
678
- outcome.reason = `新建 ${id}(scope ${(draft.scope || []).join(',') || '未知'};指纹 ${((draft.fingerprint && draft.fingerprint.targets) || []).join(',') || '无'}${draft.fingerprint && draft.fingerprint.green ? ' 全绿' : ''}${intentVeto ? `;意图不同(${intentVeto.a} vs ${intentVeto.b}),不并入 ${intentVeto.id}` : ''}${typeVeto ? `;产物类型不同(${typeVeto.kinds[0].join(',') || '无'} vs ${typeVeto.kinds[1].join(',') || '无'}),不并入 ${typeVeto.id}(相似度 ${typeVeto.score})` : ''})`
689
+ outcome.reason = `新建 ${id}(scope ${(draft.scope || []).join(',') || '未知'};指纹 ${((draft.fingerprint && draft.fingerprint.targets) || []).join(',') || '无'}${draft.fingerprint && draft.fingerprint.green ? ' 全绿' : ''}${platformVeto ? `;平台不同(${platformVeto.was} vs ${platformVeto.now}),不并入 ${platformVeto.id}` : ''}${intentVeto ? `;意图不同(${intentVeto.a} vs ${intentVeto.b}),不并入 ${intentVeto.id}` : ''}${typeVeto ? `;产物类型不同(${typeVeto.kinds[0].join(',') || '无'} vs ${typeVeto.kinds[1].join(',') || '无'}),不并入 ${typeVeto.id}(相似度 ${typeVeto.score})` : ''})`
679
690
  outcome.ms = Math.round(performance.now() - started)
680
691
  persist()
681
692
  return outcome
package/lib/capture.js CHANGED
@@ -659,6 +659,7 @@ export function buildCandidate(trace, info, config) {
659
659
 
660
660
  const record = {
661
661
  status: 'candidate',
662
+ platform: process.platform,
662
663
  // Scope detection needs the raw paths; every rendered field below is normalized.
663
664
  scope: mentionedDirs(
664
665
  [
@@ -692,6 +693,21 @@ export function buildCandidate(trace, info, config) {
692
693
  return record
693
694
  }
694
695
 
696
+ export function platformOf(record) {
697
+ const value = record && record.platform
698
+ return typeof value === 'string' ? value : ''
699
+ }
700
+
701
+ /**
702
+ * "PATH has no node, use the bundled node.exe" is a fact on win32 and a lie on
703
+ * darwin. Unknown platform never vetoes: records written before v1.1 carry no
704
+ * tag, and every one of them was learned on this machine.
705
+ */
706
+ export function platformConflicts(record) {
707
+ const tag = platformOf(record)
708
+ return Boolean(tag && tag !== process.platform)
709
+ }
710
+
695
711
  export function mergeInto(existing, draft) {
696
712
  existing.occurrences = (existing.occurrences || 1) + 1
697
713
  existing.lastSeenAt = new Date().toISOString()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-experience",
3
- "version": "1.0.14",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1184,6 +1184,20 @@ test('T81 a run recipe is always titled by its script', () => {
1184
1184
  assert.equal(draft.title, '跑 run')
1185
1185
  })
1186
1186
 
1187
+ test('T82 a captured recipe remembers the OS that taught it', () => {
1188
+ const draft = C.buildCandidate({ query: 'add pad', turn: 1 }, { sessionId: 's', cwd: 'D:/work/acme' }, C.DEFAULT_CONFIG)
1189
+ assert.equal(draft.platform, process.platform)
1190
+ assert.equal(C.platformOf({ platform: 'linux' }), 'linux')
1191
+ assert.equal(C.platformOf({}), '')
1192
+ })
1193
+
1194
+ test('T83 a recipe from another OS is never merged and never recalled', () => {
1195
+ const foreign = { platform: process.platform === 'win32' ? 'linux' : 'win32' }
1196
+ assert.equal(C.platformConflicts(foreign), true)
1197
+ assert.equal(C.platformConflicts({ platform: process.platform }), false)
1198
+ assert.equal(C.platformConflicts({}), false)
1199
+ })
1200
+
1187
1201
  // ---------------------------------------------------------------- summary
1188
1202
 
1189
1203
  const total = pass + failures.length