@ilaomianyang/dsh-memory-graph 0.10.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 +259 -0
- package/LICENSE +21 -0
- package/README.en.md +466 -0
- package/README.md +459 -0
- package/client.js +1953 -0
- package/cordis.patch.yml +13 -0
- package/docs/auto-settings.png +0 -0
- package/docs/distill-button.png +0 -0
- package/docs/graph-dark.png +0 -0
- package/docs/graph-light.png +0 -0
- package/docs/session-tab-cards.png +0 -0
- package/docs/session-tab-open.png +0 -0
- package/docs/session-tab.png +0 -0
- package/docs/vault-panel.png +0 -0
- package/eval/REPORT.md +437 -0
- package/eval/questions.json +277 -0
- package/eval/run.mjs +460 -0
- package/eval/seed.mjs +489 -0
- package/icon.svg +26 -0
- package/index.js +243 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +77 -0
- package/src/auto.js +194 -0
- package/src/capture.js +237 -0
- package/src/config.js +280 -0
- package/src/distillapi.js +30 -0
- package/src/distillrun.js +106 -0
- package/src/extract.js +660 -0
- package/src/graph.js +1582 -0
- package/src/graphdata.js +475 -0
- package/src/http.js +32 -0
- package/src/pageapi.js +40 -0
- package/src/privacy.js +78 -0
- package/src/schema.js +323 -0
- package/src/search.js +317 -0
- package/src/tools.js +534 -0
- package/src/vault.js +888 -0
- package/test/auto.mjs +295 -0
- package/test/capture.mjs +264 -0
- package/test/client.mjs +1035 -0
- package/test/distillapi.mjs +212 -0
- package/test/extract.mjs +660 -0
- package/test/graphdata.mjs +295 -0
- package/test/live-supersede.mjs +78 -0
- package/test/live.mjs +156 -0
- package/test/page.mjs +116 -0
- package/test/schema.mjs +256 -0
- package/test/smoke.mjs +428 -0
- package/test/tools.mjs +238 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "distill now" route.
|
|
3
|
+
*
|
|
4
|
+
* The panel's button is the one control in this plugin that spends model budget,
|
|
5
|
+
* so the things worth asserting are its edges: it runs the same distillation as
|
|
6
|
+
* the tool (nothing re-implemented), one press is one run, a second press while
|
|
7
|
+
* the first is working is refused instead of queued, and every outcome comes back
|
|
8
|
+
* as a report the panel can print — including the ones that are not failures.
|
|
9
|
+
*
|
|
10
|
+
* node test/distillapi.mjs
|
|
11
|
+
*/
|
|
12
|
+
import assert from 'node:assert/strict'
|
|
13
|
+
|
|
14
|
+
import { DISTILL_PATH, createDistillHandler } from '../src/distillapi.js'
|
|
15
|
+
import { createDistillRunner } from '../src/distillrun.js'
|
|
16
|
+
|
|
17
|
+
const results = []
|
|
18
|
+
function check(name, fn) {
|
|
19
|
+
return Promise.resolve()
|
|
20
|
+
.then(fn)
|
|
21
|
+
.then(
|
|
22
|
+
() => results.push(` ok ${name}`),
|
|
23
|
+
(error) => {
|
|
24
|
+
results.push(` FAIL ${name}: ${error.message}`)
|
|
25
|
+
process.exitCode = 1
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const body = async (response) => JSON.parse(await response.text())
|
|
31
|
+
|
|
32
|
+
const makeHandler = (overrides = {}) => {
|
|
33
|
+
const seen = []
|
|
34
|
+
const runner = createDistillRunner({
|
|
35
|
+
getVault: () => ({ name: 'vault' }),
|
|
36
|
+
getConfig: () => ({ extractor: { enabled: true } }),
|
|
37
|
+
getRedactor: () => ({ redact: (text) => text }),
|
|
38
|
+
getServices: () => ({ llm: 'LLM', selection: { provider: 'p', model: 'm' } }),
|
|
39
|
+
distill: async (input) => {
|
|
40
|
+
seen.push(input)
|
|
41
|
+
return { ok: true, episodes: 12, written: 3, cards: [{ id: 'card-1', kind: 'decision', title: '用 BM25' }] }
|
|
42
|
+
},
|
|
43
|
+
...overrides,
|
|
44
|
+
})
|
|
45
|
+
const handler = createDistillHandler({ getRunner: () => runner })
|
|
46
|
+
return { handler, seen, runner }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await check('the route sits on the authenticated /api channel like the graph one', () => {
|
|
50
|
+
assert.ok(DISTILL_PATH.startsWith('/api/'))
|
|
51
|
+
assert.ok(DISTILL_PATH.includes('distill'), 'it is the distill door, not the graph door')
|
|
52
|
+
assert.ok(!DISTILL_PATH.includes('?'), 'a query string would make the exact route never match')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
await check('it runs the same distillation the tool runs, with the live vault and model', async () => {
|
|
56
|
+
const { handler, seen } = makeHandler()
|
|
57
|
+
const response = await handler()
|
|
58
|
+
assert.equal(response.status, 200)
|
|
59
|
+
assert.equal(seen.length, 1)
|
|
60
|
+
assert.equal(seen[0].vault.name, 'vault')
|
|
61
|
+
assert.equal(seen[0].cfg.extractor.enabled, true)
|
|
62
|
+
assert.equal(seen[0].llm, 'LLM', 'the harness transport service is passed through')
|
|
63
|
+
assert.equal(seen[0].selection.provider, 'p', 'so the run uses the model selected in Settings')
|
|
64
|
+
assert.equal(typeof seen[0].redactor.redact, 'function', 'and the live privacy rules')
|
|
65
|
+
assert.equal(seen[0].dryRun, false, 'a button press is a real run, not a rehearsal')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
await check('the answer is a report the panel can print', async () => {
|
|
69
|
+
const { handler } = makeHandler()
|
|
70
|
+
const report = await body(await handler())
|
|
71
|
+
assert.equal(report.ok, true)
|
|
72
|
+
assert.equal(report.busy, false)
|
|
73
|
+
assert.equal(report.episodes, 12)
|
|
74
|
+
assert.equal(report.written, 3)
|
|
75
|
+
assert.deepEqual(report.cards, [{ id: 'card-1', kind: 'decision', title: '用 BM25', deduped: false }])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
await check('one press is one run: a second press while it works is refused', async () => {
|
|
79
|
+
let release
|
|
80
|
+
const gate = new Promise((resolve) => {
|
|
81
|
+
release = resolve
|
|
82
|
+
})
|
|
83
|
+
let runs = 0
|
|
84
|
+
const { handler } = makeHandler({
|
|
85
|
+
distill: async () => {
|
|
86
|
+
runs += 1
|
|
87
|
+
await gate
|
|
88
|
+
return { ok: true, episodes: 1, written: 1 }
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const first = handler()
|
|
93
|
+
// Let the first call reach its await, then press again.
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
95
|
+
const second = await body(await handler())
|
|
96
|
+
assert.equal(second.busy, true, 'the second caller is told, not queued')
|
|
97
|
+
assert.equal(second.ok, false)
|
|
98
|
+
|
|
99
|
+
release()
|
|
100
|
+
const after = await body(await first)
|
|
101
|
+
assert.equal(after.ok, true)
|
|
102
|
+
assert.equal(runs, 1, 'exactly one distillation spent budget')
|
|
103
|
+
|
|
104
|
+
const third = await body(await handler())
|
|
105
|
+
assert.equal(third.busy, false, 'and the lock is released for the next press')
|
|
106
|
+
assert.equal(runs, 2)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
await check('a refusal from the extractor is an answer, not a 500', async () => {
|
|
110
|
+
const { handler } = makeHandler({
|
|
111
|
+
distill: async () => ({ ok: false, reason: 'Extractor is disabled. Set config extractor.enabled = true.' }),
|
|
112
|
+
})
|
|
113
|
+
const response = await handler()
|
|
114
|
+
assert.equal(response.status, 200, 'the panel prints the line instead of an error banner')
|
|
115
|
+
const report = await body(response)
|
|
116
|
+
assert.equal(report.ok, false)
|
|
117
|
+
assert.ok(String(report.reason).includes('extractor.enabled'))
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
await check('an unexpected throw is a 500 with the message, and the lock still frees', async () => {
|
|
121
|
+
let calls = 0
|
|
122
|
+
const { handler } = makeHandler({
|
|
123
|
+
distill: async () => {
|
|
124
|
+
calls += 1
|
|
125
|
+
if (calls === 1) throw new Error('model route is gone')
|
|
126
|
+
return { ok: true, episodes: 0, written: 0, reason: 'nothing new' }
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
const failed = await handler()
|
|
130
|
+
assert.equal(failed.status, 500)
|
|
131
|
+
const report = await body(failed)
|
|
132
|
+
assert.equal(report.ok, false)
|
|
133
|
+
assert.ok(String(report.message).includes('model route is gone'))
|
|
134
|
+
|
|
135
|
+
const retried = await handler()
|
|
136
|
+
assert.equal(retried.status, 200, 'a crashed run does not wedge the button')
|
|
137
|
+
assert.equal((await body(retried)).ok, true)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
await check('a run that wrote cards refreshes the vault views, a dry one does not', async () => {
|
|
141
|
+
let refreshed = 0
|
|
142
|
+
const { handler } = makeHandler({
|
|
143
|
+
refreshViews: async () => {
|
|
144
|
+
refreshed += 1
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
await handler()
|
|
148
|
+
assert.equal(refreshed, 1, 'graph.html and README catch up without a restart')
|
|
149
|
+
|
|
150
|
+
const empty = makeHandler({
|
|
151
|
+
distill: async () => ({ ok: true, episodes: 0, written: 0, reason: 'Every episode has already been distilled.' }),
|
|
152
|
+
refreshViews: async () => {
|
|
153
|
+
refreshed += 1
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
await empty.handler()
|
|
157
|
+
assert.equal(refreshed, 1, 'nothing written, nothing to re-render')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
await check('a view that fails to render does not fail the run that wrote the cards', async () => {
|
|
161
|
+
const warnings = []
|
|
162
|
+
const { handler } = makeHandler({
|
|
163
|
+
refreshViews: async () => {
|
|
164
|
+
throw new Error('disk full')
|
|
165
|
+
},
|
|
166
|
+
log: (level, message) => warnings.push(`${level}: ${message}`),
|
|
167
|
+
})
|
|
168
|
+
const response = await handler()
|
|
169
|
+
assert.equal(response.status, 200)
|
|
170
|
+
assert.equal((await body(response)).written, 3)
|
|
171
|
+
assert.equal(warnings.length, 1)
|
|
172
|
+
assert.ok(warnings[0].includes('disk full'))
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
await check('the card list is capped so a huge run cannot bloat the panel', async () => {
|
|
176
|
+
const cards = Array.from({ length: 120 }, (_, i) => ({ id: `c-${i}`, kind: 'fact', title: `t${i}` }))
|
|
177
|
+
const { handler } = makeHandler({ distill: async () => ({ ok: true, episodes: 120, written: 120, cards }) })
|
|
178
|
+
const report = await body(await handler())
|
|
179
|
+
assert.equal(report.cards.length, 40)
|
|
180
|
+
assert.equal(report.written, 120, 'the count is still the truth')
|
|
181
|
+
})
|
|
182
|
+
await check('the runner tells its callers when it is busy, so the ticker can stand down', async () => {
|
|
183
|
+
let release
|
|
184
|
+
const gate = new Promise((resolve) => {
|
|
185
|
+
release = resolve
|
|
186
|
+
})
|
|
187
|
+
const { runner } = makeHandler({
|
|
188
|
+
distill: async () => {
|
|
189
|
+
await gate
|
|
190
|
+
return { ok: true, episodes: 1, written: 1 }
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
assert.equal(runner.busy(), false)
|
|
194
|
+
const running = runner.run({})
|
|
195
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
196
|
+
assert.equal(runner.busy(), true, 'a run in flight is visible without asking it to start another')
|
|
197
|
+
release()
|
|
198
|
+
await running
|
|
199
|
+
assert.equal(runner.busy(), false, 'and it clears when the run settles')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
await check('a route with no runner wired answers 500 instead of throwing at the browser', async () => {
|
|
203
|
+
const handler = createDistillHandler({ getRunner: () => null })
|
|
204
|
+
const response = await handler()
|
|
205
|
+
assert.equal(response.status, 500)
|
|
206
|
+
const report = await body(response)
|
|
207
|
+
assert.equal(report.ok, false)
|
|
208
|
+
assert.equal(report.reason, 'unavailable')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
console.log(results.join('\n'))
|
|
212
|
+
console.log(process.exitCode === 1 ? '\nSOME CHECKS FAILED' : `\nall ${results.length} checks passed`)
|