dsh-error-atlas 0.11.25 → 0.11.26
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 +9 -0
- package/README.md +21 -4
- package/index.js +129 -0
- package/package.json +1 -1
- package/test/run.mjs +102 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to **dsh-error-atlas**. Versions follow `0.11.x` while the p
|
|
|
4
4
|
is in the DSH marketplace pipeline; the panel's `V##` chip is the **client bundle** build
|
|
5
5
|
stamp, which only moves when `client.js` changes — a `V51 · 0.11.24` panel is expected.
|
|
6
6
|
|
|
7
|
+
## 0.11.26
|
|
8
|
+
|
|
9
|
+
- `atlas_impact` — the read-only tool that says whether the mechanism pays for itself:
|
|
10
|
+
how many lessons exist, how many can be judged (they name failures still recorded), how
|
|
11
|
+
many of those were followed by the same failure returning, and how much of the store the
|
|
12
|
+
lessons cover. Also exposed as `impact` in `/error-atlas/data` so the panel can show it.
|
|
13
|
+
- Real store at release: 59 lessons (49 model · 10 auto), **30 judgeable, 6 recurred (20%),
|
|
14
|
+
24 held (80%)**, 61% of signatures carry a lesson, 235 failures in the last 30 days.
|
|
15
|
+
|
|
7
16
|
## 0.11.25
|
|
8
17
|
|
|
9
18
|
- Client tests no longer depend on the checkout's line endings: the test seam matches the
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
*面板实拍:顶部是统计与操作,左侧是颜色图例,中间是 GPU 渲染的错误星系,底部是「记忆」开关、教训有效性榜单与最近事件表。标题旁的 `
|
|
9
|
+
*面板实拍:顶部是统计与操作,左侧是颜色图例,中间是 GPU 渲染的错误星系,底部是「记忆」开关、教训有效性榜单与最近事件表。标题旁的 `V##` 是客户端 build 号,用来确认页面跑的是哪一代代码(截图拍的是 `V33`,当前是 `V51`)。*
|
|
10
10
|
|
|
11
11
|
A local-first memory plugin for DeepSeek Harness: it records runtime failures and
|
|
12
12
|
coding-task retrospectives, injects the reusable lessons back into the prompt, and
|
|
@@ -28,7 +28,24 @@ DSH 报过的错、踩过的坑,默认会随会话结束消失。这个插件
|
|
|
28
28
|
|
|
29
29
|
- 一段全局提示词要求「编程任务收尾前写复盘」;
|
|
30
30
|
- 每次请求动态注入简报:**失效教训优先**(写完又复发的),再加几条最近的规则;没有教训时整段不出现,不产生固定开销;
|
|
31
|
-
- 只读工具 `atlas_lessons`,可按 `query` / `program` / `limit`
|
|
31
|
+
- 只读工具 `atlas_lessons`,可按 `query` / `program` / `limit` 查询全部教训;
|
|
32
|
+
- 只读工具 `atlas_impact`,回答「这套机制到底管不管用」:多少条教训可以被判定、其中多少条的失败在写完后又回来了,以及教训覆盖了多少种错误签名。
|
|
33
|
+
|
|
34
|
+
当前库的实测(59 条复盘 / 102 个签名):**30 条可判定,其中 6 条复发(20%),24 条成立(80%)**;61% 的签名已经有过教训。
|
|
35
|
+
|
|
36
|
+
## 它不是什么
|
|
37
|
+
|
|
38
|
+
生态里已经有做「记忆」和「复盘」的插件,先把边界说清楚,省得装完发现不是想要的:
|
|
39
|
+
|
|
40
|
+
| 已有的方向 | 代表 | 我们和它的分工 |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| **经验库**:任务后整理候选经验、审阅采纳、按项目召回、冲突处理、到期复核 | `dsh-dream` | 它管「经验文本的采纳流程」,我们管「失败证据的采集与度量」。它更成熟,可以一起装 |
|
|
43
|
+
| **云端失败案例检索**:带证据评级的历史案例 | `misakanet` | 它查别人的案例库(走公共 MCP 服务),我们只记**你这台机器上真实发生的事**,不联网、不上传 |
|
|
44
|
+
| **单仓库修复经验**:只从测试验证过的修复里积累 | `dsh-reforge` | 它要求「先修好、有测试证明」,覆盖 Python 单仓;我们记录任意工具/命令的失败,不要求已经修好 |
|
|
45
|
+
| **错误提醒 / 自动重试 / 诊断** | `dsh-pharos`、`bark-notify`、`dsh-client-auto-continue`、`dsh-plugin-debugging` | 那些是「处理与提示」,错误过去就过去了;我们做「沉淀」 |
|
|
46
|
+
| **服务端错误跟踪** | Sentry、GlitchTip 一类 | 那类要靠 instrumentation 上报到服务器、跨机器聚合;这里没有服务端、没有传输,数据只落在 `$DSH_HOME/error-atlas/records.json` |
|
|
47
|
+
|
|
48
|
+
一句话区别:**给每个失败一个身份(去重签名)、一个位置(会话岛 / 程序卫星)、一个判决(写完教训之后又犯了吗)**。前者是采集,中间是可视化,后者是唯一能证明「教训有用」的东西——这也是它和上面几个不重叠的部分。
|
|
32
49
|
|
|
33
50
|
## 噪声与本质
|
|
34
51
|
|
|
@@ -125,7 +142,7 @@ DSH 报过的错、踩过的坑,默认会随会话结束消失。这个插件
|
|
|
125
142
|
plugin_manager install_bundle target="dsh-error-atlas"
|
|
126
143
|
```
|
|
127
144
|
|
|
128
|
-
指定版本:`target="dsh-error-atlas@0.11.
|
|
145
|
+
指定版本:`target="dsh-error-atlas@0.11.25"`。
|
|
129
146
|
|
|
130
147
|
本地目录安装(开发用):
|
|
131
148
|
|
|
@@ -180,7 +197,7 @@ npm test # 157 项断言:宿主事件/路由/存储 + 客户端渲染
|
|
|
180
197
|
npm run smoke # 干净 DSH_HOME 下的冷启动冒烟:空库 → 写复盘 → 读回 → CLI 可读
|
|
181
198
|
```
|
|
182
199
|
|
|
183
|
-
客户端为零依赖手写 bundle(`window.__ModuleLoader__`),宿主为纯 ESM。面板标题旁的 `
|
|
200
|
+
客户端为零依赖手写 bundle(`window.__ModuleLoader__`),宿主为纯 ESM。面板标题旁的 `V51` 就是 BUILD 号,用来确认页面加载的是哪一代客户端代码。
|
|
184
201
|
|
|
185
202
|
## 已知边界
|
|
186
203
|
|
package/index.js
CHANGED
|
@@ -931,6 +931,61 @@ function createStore(config) {
|
|
|
931
931
|
return lines.join('\n')
|
|
932
932
|
}
|
|
933
933
|
|
|
934
|
+
// What the lessons bought, in numbers that are honest about being proxies: a lesson
|
|
935
|
+
// counts as "judged" only when it names failures that are still in the store, and only
|
|
936
|
+
// then can its signatures be watched afterwards. Everything else is bookkeeping.
|
|
937
|
+
function impactOf(days = 30) {
|
|
938
|
+
const span = Math.max(1, Math.min(365, Math.floor(days || 30)))
|
|
939
|
+
const windowMs = span * 24 * 60 * 60 * 1000
|
|
940
|
+
const since = Date.now() - windowMs
|
|
941
|
+
const recordById = new Map()
|
|
942
|
+
for (const entry of state.records.values()) recordById.set(entry.id, entry)
|
|
943
|
+
const ambient = ambientIds()
|
|
944
|
+
const linked = new Set()
|
|
945
|
+
let model = 0
|
|
946
|
+
let auto = 0
|
|
947
|
+
let judgeable = 0
|
|
948
|
+
let failed = 0
|
|
949
|
+
let firstLessonAt = Infinity
|
|
950
|
+
for (const retro of state.retros) {
|
|
951
|
+
if (retro.auto) auto += 1
|
|
952
|
+
else model += 1
|
|
953
|
+
if (!retro.auto && Number.isFinite(retro.at) && retro.at < firstLessonAt) firstLessonAt = retro.at
|
|
954
|
+
const ids = (retro.errorIds || []).filter((id) => recordById.has(id))
|
|
955
|
+
for (const id of ids) linked.add(id)
|
|
956
|
+
if (retro.auto || ids.length === 0) continue
|
|
957
|
+
judgeable += 1
|
|
958
|
+
const relapse = recurrenceOf(retro, recordById, ambient)
|
|
959
|
+
if (relapse && relapse.times > 0) failed += 1
|
|
960
|
+
}
|
|
961
|
+
let hits = 0
|
|
962
|
+
let hitsInWindow = 0
|
|
963
|
+
let afterFirstLesson = 0
|
|
964
|
+
for (const event of state.log) {
|
|
965
|
+
hits += 1
|
|
966
|
+
if (event.at >= since) hitsInWindow += 1
|
|
967
|
+
if (Number.isFinite(firstLessonAt) && event.at > firstLessonAt) afterFirstLesson += 1
|
|
968
|
+
}
|
|
969
|
+
const distinct = state.records.size
|
|
970
|
+
return {
|
|
971
|
+
days: span,
|
|
972
|
+
lessons: model + auto,
|
|
973
|
+
model,
|
|
974
|
+
auto,
|
|
975
|
+
judgeable,
|
|
976
|
+
failed,
|
|
977
|
+
held: judgeable - failed,
|
|
978
|
+
rate: judgeable > 0 ? Math.round((failed / judgeable) * 100) : 0,
|
|
979
|
+
distinct,
|
|
980
|
+
withLesson: linked.size,
|
|
981
|
+
coverage: distinct > 0 ? Math.round((linked.size / distinct) * 100) : 0,
|
|
982
|
+
hits,
|
|
983
|
+
hitsInWindow,
|
|
984
|
+
afterFirstLesson,
|
|
985
|
+
firstLessonAt: Number.isFinite(firstLessonAt) ? firstLessonAt : 0,
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
934
989
|
function snapshot() {
|
|
935
990
|
const all = [...state.records.values()].sort((a, b) => b.count - a.count || b.last - a.last)
|
|
936
991
|
const recordById = new Map()
|
|
@@ -1212,6 +1267,7 @@ function createStore(config) {
|
|
|
1212
1267
|
prefs: prefs(),
|
|
1213
1268
|
trend: trendOf(Date.now()),
|
|
1214
1269
|
lessons: lessonBoard(),
|
|
1270
|
+
impact: impactOf(30),
|
|
1215
1271
|
nodes,
|
|
1216
1272
|
links,
|
|
1217
1273
|
retros: state.retros.slice(0, config.maxRetros),
|
|
@@ -1233,6 +1289,7 @@ function createStore(config) {
|
|
|
1233
1289
|
applyPrefs,
|
|
1234
1290
|
lessonList,
|
|
1235
1291
|
lessonBriefing,
|
|
1292
|
+
impactOf,
|
|
1236
1293
|
}
|
|
1237
1294
|
}
|
|
1238
1295
|
|
|
@@ -1816,6 +1873,78 @@ export function apply(ctx, config = {}) {
|
|
|
1816
1873
|
}),
|
|
1817
1874
|
'error-atlas: lesson tool',
|
|
1818
1875
|
)
|
|
1876
|
+
|
|
1877
|
+
// Whether the mechanism pays for itself: how many lessons exist, how many can be
|
|
1878
|
+
// judged at all, and how many of those had their signatures come back anyway.
|
|
1879
|
+
scope.effect(
|
|
1880
|
+
() =>
|
|
1881
|
+
scope.tools.register({
|
|
1882
|
+
name: 'atlas_impact',
|
|
1883
|
+
description:
|
|
1884
|
+
'Report what the recorded lessons bought: how many exist, how many can be judged (they name failures that are still in the store), how many of those were followed by the same failure returning, and how much of the store the lessons cover. These are counts from the local store, not a controlled experiment.',
|
|
1885
|
+
parameters: {
|
|
1886
|
+
type: 'object',
|
|
1887
|
+
properties: {
|
|
1888
|
+
days: { type: 'number', description: 'Window for the failure counts, 1 to 365; defaults to 30.' },
|
|
1889
|
+
},
|
|
1890
|
+
additionalProperties: false,
|
|
1891
|
+
},
|
|
1892
|
+
output: {
|
|
1893
|
+
schema: {
|
|
1894
|
+
type: 'object',
|
|
1895
|
+
properties: {
|
|
1896
|
+
days: { type: 'number' },
|
|
1897
|
+
lessons: { type: 'number' },
|
|
1898
|
+
model: { type: 'number' },
|
|
1899
|
+
auto: { type: 'number' },
|
|
1900
|
+
judgeable: { type: 'number' },
|
|
1901
|
+
failed: { type: 'number' },
|
|
1902
|
+
held: { type: 'number' },
|
|
1903
|
+
rate: { type: 'number' },
|
|
1904
|
+
distinct: { type: 'number' },
|
|
1905
|
+
withLesson: { type: 'number' },
|
|
1906
|
+
coverage: { type: 'number' },
|
|
1907
|
+
hits: { type: 'number' },
|
|
1908
|
+
hitsInWindow: { type: 'number' },
|
|
1909
|
+
afterFirstLesson: { type: 'number' },
|
|
1910
|
+
firstLessonAt: { type: 'number' },
|
|
1911
|
+
},
|
|
1912
|
+
required: ['days', 'lessons', 'judgeable', 'failed', 'held', 'rate', 'coverage'],
|
|
1913
|
+
additionalProperties: false,
|
|
1914
|
+
},
|
|
1915
|
+
render(_args, value) {
|
|
1916
|
+
if (value.lessons === 0) return [{ type: 'text', text: 'no lessons recorded yet' }]
|
|
1917
|
+
if (value.judgeable === 0) {
|
|
1918
|
+
return [
|
|
1919
|
+
{
|
|
1920
|
+
type: 'text',
|
|
1921
|
+
text:
|
|
1922
|
+
value.lessons +
|
|
1923
|
+
' lesson(s), none judgeable yet: a lesson is judged only once it names failures that are still recorded',
|
|
1924
|
+
},
|
|
1925
|
+
]
|
|
1926
|
+
}
|
|
1927
|
+
return [
|
|
1928
|
+
{
|
|
1929
|
+
type: 'text',
|
|
1930
|
+
text: [
|
|
1931
|
+
'impact · ' + value.days + ' day window',
|
|
1932
|
+
'lessons: ' + value.lessons + ' (' + value.model + ' model · ' + value.auto + ' auto) · ' + value.judgeable + ' judgeable',
|
|
1933
|
+
'verdict: ' + value.failed + ' of ' + value.judgeable + ' judged lessons recurred (' + value.rate + '% failed · ' + (100 - value.rate) + '% held)',
|
|
1934
|
+
'coverage: ' + value.withLesson + ' of ' + value.distinct + ' signatures carry a lesson (' + value.coverage + '%)',
|
|
1935
|
+
'failures: ' + value.hitsInWindow + ' in window · ' + value.afterFirstLesson + ' since the first lesson',
|
|
1936
|
+
].join('\n'),
|
|
1937
|
+
},
|
|
1938
|
+
]
|
|
1939
|
+
},
|
|
1940
|
+
},
|
|
1941
|
+
execute(args) {
|
|
1942
|
+
const days = Number.isFinite(args.days) ? Math.max(1, Math.min(365, Math.floor(args.days))) : 30
|
|
1943
|
+
return Promise.resolve(store.impactOf(days))
|
|
1944
|
+
},
|
|
1945
|
+
}),
|
|
1946
|
+
'error-atlas: impact tool',
|
|
1947
|
+
)
|
|
1819
1948
|
})
|
|
1820
1949
|
|
|
1821
1950
|
ctx.inject(['systemPrompt'], (scope) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-error-atlas",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Records runtime errors and coding-task retrospectives, collapses noisy shell failures into command fingerprints, distils each lesson into one injected rule, and renders the whole thing as an interactive GPU knowledge graph (WebGL) with lesson links, recurrence flags, program satellites, session islands, orbit-any-node camera, search and activity trends.",
|
|
6
6
|
"icon": "icon.svg",
|
package/test/run.mjs
CHANGED
|
@@ -823,6 +823,107 @@ async function lessonSection() {
|
|
|
823
823
|
}
|
|
824
824
|
|
|
825
825
|
|
|
826
|
+
// ── impact ─────────────────────────────────────────────────────────────────
|
|
827
|
+
// The number the plugin exists for: of the lessons that can be judged at all, how many
|
|
828
|
+
// had their signatures come back anyway.
|
|
829
|
+
async function impactSection() {
|
|
830
|
+
section('impact')
|
|
831
|
+
const home = tempHome('impact')
|
|
832
|
+
const now = Date.now()
|
|
833
|
+
const hour = 60 * 60 * 1000
|
|
834
|
+
const retro = (id, at, errorIds, extra) =>
|
|
835
|
+
Object.assign(
|
|
836
|
+
{
|
|
837
|
+
id,
|
|
838
|
+
at,
|
|
839
|
+
auto: false,
|
|
840
|
+
title: 'task ' + id,
|
|
841
|
+
done: 'changed',
|
|
842
|
+
cause: 'c',
|
|
843
|
+
fix: 'f',
|
|
844
|
+
lesson: 'lesson ' + id,
|
|
845
|
+
tags: ['edit'],
|
|
846
|
+
tools: ['edit'],
|
|
847
|
+
files: ['C:/work/proj-a/src/a.ts'],
|
|
848
|
+
cwd: 'C:/work',
|
|
849
|
+
errors: [],
|
|
850
|
+
errorIds,
|
|
851
|
+
},
|
|
852
|
+
extra || {},
|
|
853
|
+
)
|
|
854
|
+
writeFileSync(
|
|
855
|
+
join(home, 'error-atlas', 'records.json'),
|
|
856
|
+
JSON.stringify({
|
|
857
|
+
version: 2,
|
|
858
|
+
updatedAt: now,
|
|
859
|
+
records: [
|
|
860
|
+
{
|
|
861
|
+
key: 'tool\u0000edit\u0000boom',
|
|
862
|
+
id: 'e1',
|
|
863
|
+
kind: 'tool',
|
|
864
|
+
name: 'edit',
|
|
865
|
+
message: 'boom',
|
|
866
|
+
count: 2,
|
|
867
|
+
first: now - 5 * hour,
|
|
868
|
+
last: now - 10 * 60 * 1000,
|
|
869
|
+
sessions: ['s'],
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
key: 'tool\u0000pwsh\u0000sunless',
|
|
873
|
+
id: 'e2',
|
|
874
|
+
kind: 'tool',
|
|
875
|
+
name: 'pwsh',
|
|
876
|
+
message: 'no lesson here',
|
|
877
|
+
count: 1,
|
|
878
|
+
first: now - 4 * hour,
|
|
879
|
+
last: now - 4 * hour,
|
|
880
|
+
sessions: ['s'],
|
|
881
|
+
},
|
|
882
|
+
],
|
|
883
|
+
log: [
|
|
884
|
+
{ at: now - 10 * 60 * 1000, kind: 'tool', name: 'edit', message: 'boom', sessionId: 's' },
|
|
885
|
+
{ at: now - 4 * hour, kind: 'tool', name: 'pwsh', message: 'no lesson here', sessionId: 's' },
|
|
886
|
+
{ at: now - 40 * 24 * hour, kind: 'tool', name: 'edit', message: 'ancient', sessionId: 's' },
|
|
887
|
+
],
|
|
888
|
+
retros: [
|
|
889
|
+
retro('r1', now - 3 * hour, ['e1']),
|
|
890
|
+
retro('r2', now - 2 * hour, []),
|
|
891
|
+
retro('r3', now - hour, [], { auto: true }),
|
|
892
|
+
],
|
|
893
|
+
}),
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
const host = await bootHost(home)
|
|
897
|
+
const tool = host.tools.find((item) => item.name === 'atlas_impact')
|
|
898
|
+
check('the impact tool is registered', Boolean(tool))
|
|
899
|
+
if (!tool) return
|
|
900
|
+
|
|
901
|
+
const impact = await tool.execute({})
|
|
902
|
+
eq('every retrospective is counted', impact.lessons, 3)
|
|
903
|
+
eq('model retrospectives are separated', impact.model, 2)
|
|
904
|
+
eq('auto skeletons are separated', impact.auto, 1)
|
|
905
|
+
eq('only lessons that name a live failure are judgeable', impact.judgeable, 1)
|
|
906
|
+
eq('a lesson whose failure came back counts as failed', impact.failed, 1)
|
|
907
|
+
eq('nothing was held against the only judged lesson', impact.held, 0)
|
|
908
|
+
eq('the failure rate is a percentage', impact.rate, 100)
|
|
909
|
+
eq('the store size is reported', impact.distinct, 2)
|
|
910
|
+
eq('coverage counts signatures with a lesson', impact.withLesson, 1)
|
|
911
|
+
eq('coverage is a percentage', impact.coverage, 50)
|
|
912
|
+
eq('the failure log is counted', impact.hits, 3)
|
|
913
|
+
eq('the window keeps the old failure out', impact.hitsInWindow, 2)
|
|
914
|
+
check('failures after the first lesson are counted', impact.afterFirstLesson >= 1, String(impact.afterFirstLesson))
|
|
915
|
+
eq('the default window is 30 days', impact.days, 30)
|
|
916
|
+
eq('a shorter window is honoured', (await tool.execute({ days: 1 })).days, 1)
|
|
917
|
+
eq('a silly window is clamped', (await tool.execute({ days: 9999 })).days, 365)
|
|
918
|
+
|
|
919
|
+
const text = tool.output.render({}, impact)[0].text
|
|
920
|
+
check('the rendered impact reads like a report', text.includes('judged lessons recurred'), text.slice(0, 120))
|
|
921
|
+
check('the rendered impact reports coverage', text.includes('carry a lesson'))
|
|
922
|
+
|
|
923
|
+
const payload = (await host.call('/error-atlas/data')).json
|
|
924
|
+
eq('the panel payload carries the same numbers', payload.impact ? payload.impact.judgeable : -1, 1)
|
|
925
|
+
}
|
|
926
|
+
|
|
826
927
|
// ── board ──────────────────────────────────────────────────────────────────
|
|
827
928
|
// The effectiveness leaderboard: a lesson library is only useful if the reader can see
|
|
828
929
|
// which kinds of lesson keep failing without opening every entry.
|
|
@@ -1034,7 +1135,7 @@ async function noiseSection() {
|
|
|
1034
1135
|
}
|
|
1035
1136
|
|
|
1036
1137
|
// ── run ────────────────────────────────────────────────────────────────────
|
|
1037
|
-
const sections = [storeSection, retroSection, prefsSection, trendSection, projectSection, exportSection, lessonSection, noiseSection, boardSection, clientSection, capSection, manifestSection]
|
|
1138
|
+
const sections = [storeSection, retroSection, prefsSection, trendSection, projectSection, exportSection, lessonSection, noiseSection, impactSection, boardSection, clientSection, capSection, manifestSection]
|
|
1038
1139
|
for (const run of sections) {
|
|
1039
1140
|
try {
|
|
1040
1141
|
await run()
|