@evomap/evolver-webui 2.0.0-beta.2 → 2.0.0-beta.4

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/dist/server.js CHANGED
@@ -3,6 +3,9 @@ import { randomBytes, timingSafeEqual } from 'node:crypto';
3
3
  import { events as ev, assetstore, mailbox as mb, ops } from '@evomap/evolver-core';
4
4
  import { CONSOLE_HTML } from './console.js';
5
5
  import { EventSnapshotCache, fileEventSnapshotSource } from './eventSnapshot.js';
6
+ import { listLineageAssets, loadAssetLineage } from './assetLineage.js';
7
+ import { eventListRelations } from './observabilityRelations.js';
8
+ import { redactDiagnosticText, sanitizeDiagnosticValue } from './diagnosticSanitize.js';
6
9
  const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
7
10
  const DASHBOARD_COOKIE = 'evolver_dashboard';
8
11
  const BROWSER_BLOCKED_PORTS = new Set([
@@ -15,6 +18,41 @@ const BROWSER_BLOCKED_PORTS = new Set([
15
18
  /** The empty value summary (zero entries) — the shape /api/value returns when no provider is wired, so the card
16
19
  * always gets a valid ValueSummary to render. Derived from core's aggregator to stay shape-identical. */
17
20
  const EMPTY_VALUE_SUMMARY = ops.valueSummary([]);
21
+ const MEMORY_GRAPH_REASON_PATTERN = /scoped memory-graph outcome ([+-]\d+\.\d{3}) \(boost=([+-]?\d+\.\d{2})\)/;
22
+ const MEMORY_GRAPH_RECOVERY_STATES = new Set(['healthy', 'degraded', 'recovered', 'empty']);
23
+ function boundedMemoryGraphCount(value) {
24
+ const count = Number(value);
25
+ return Number.isFinite(count) && count > 0 ? Math.min(1_000_000, Math.floor(count)) : 0;
26
+ }
27
+ function sanitizeMemoryGraphReason(value) {
28
+ if (typeof value !== 'string')
29
+ return undefined;
30
+ const match = MEMORY_GRAPH_REASON_PATTERN.exec(value);
31
+ if (!match?.[1] || !match[2])
32
+ return undefined;
33
+ const outcome = Number(match[1]);
34
+ const boost = Number(match[2]);
35
+ if (!Number.isFinite(outcome) || !Number.isFinite(boost) || Math.abs(outcome) > 1 || Math.abs(boost) > 1)
36
+ return undefined;
37
+ return `scoped memory-graph outcome ${match[1]} (boost=${match[2]})`;
38
+ }
39
+ function sanitizeMemoryGraphStatus(value) {
40
+ const raw = value;
41
+ const recovery = MEMORY_GRAPH_RECOVERY_STATES.has(raw['recovery'])
42
+ ? raw['recovery']
43
+ : 'degraded';
44
+ const selectionReason = sanitizeMemoryGraphReason(raw['selectionReason']);
45
+ return {
46
+ recovery,
47
+ compactedRecords: boundedMemoryGraphCount(raw['compactedRecords']),
48
+ activeRecords: boundedMemoryGraphCount(raw['activeRecords']),
49
+ corruptLines: boundedMemoryGraphCount(raw['corruptLines']),
50
+ oversizedLines: boundedMemoryGraphCount(raw['oversizedLines']),
51
+ oversizedFiles: boundedMemoryGraphCount(raw['oversizedFiles']),
52
+ archives: boundedMemoryGraphCount(raw['archives']),
53
+ ...(selectionReason ? { selectionReason } : {}),
54
+ };
55
+ }
18
56
  /** Constant-time token compare (avoids leaking the token via timing). */
19
57
  function tokenEq(a, b) {
20
58
  const ba = Buffer.from(a), bb = Buffer.from(b);
@@ -35,6 +73,13 @@ function positiveIntParam(value) {
35
73
  const n = Number(value);
36
74
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
37
75
  }
76
+ function requireGet(req, res) {
77
+ if (req.method === 'GET')
78
+ return true;
79
+ res.writeHead(405, { allow: 'GET', 'content-type': 'application/json' });
80
+ res.end(JSON.stringify({ error: 'method_not_allowed' }));
81
+ return false;
82
+ }
38
83
  const HUMAN_ACTIONS = {
39
84
  observe: 'actor.human.observe', nudge: 'actor.human.nudge', intervene: 'actor.human.intervene', teach: 'actor.human.teach',
40
85
  };
@@ -51,6 +96,7 @@ export class WebUIServer {
51
96
  actorId;
52
97
  /** Review ledger backing the human-review queue. Undefined when no LocalJsonlProvider store is available. */
53
98
  review;
99
+ provenance;
54
100
  /** Token guarding /api/*; supplied by the browser via Bearer, with ?token= retained for compatibility. */
55
101
  token;
56
102
  launchTicket;
@@ -67,6 +113,7 @@ export class WebUIServer {
67
113
  this.ingestor = deps.ingestor ?? new ev.Ingestor({ path: deps.eventsPath });
68
114
  // Co-locate the review ledger with the store so the queue reads the same review.jsonl the CLI writes.
69
115
  this.review = deps.review ?? (deps.store instanceof assetstore.LocalJsonlProvider ? new assetstore.ReviewLedger(deps.store.baseDir) : undefined);
116
+ this.provenance = deps.provenance ?? (deps.store instanceof assetstore.LocalJsonlProvider ? new assetstore.ProvenanceStore(deps.store.baseDir) : undefined);
70
117
  this.server = createServer((req, res) => { void this.handle(req, res); });
71
118
  }
72
119
  async listen(port = 0) {
@@ -148,27 +195,76 @@ export class WebUIServer {
148
195
  }
149
196
  }
150
197
  if (p === '/api/status')
151
- return this.json(res, ev.statusReport(this.eventSnapshots.read()));
198
+ return this.json(res, ev.statusReport(await this.eventSnapshots.read()));
152
199
  if (p === '/api/cycles')
153
- return this.json(res, ev.listCycles(this.eventSnapshots.read()));
154
- if (p === '/api/cycle')
155
- return this.json(res, ev.showCycle(this.eventSnapshots.read(), url.searchParams.get('id') ?? ''));
200
+ return this.json(res, ev.listCycles(await this.eventSnapshots.read()));
201
+ if (p === '/api/cycle') {
202
+ const cycle = ev.showCycle(await this.eventSnapshots.read(), url.searchParams.get('id') ?? '');
203
+ const safeTimeline = cycle.timeline.map((event) => ({
204
+ ...event,
205
+ title: redactDiagnosticText(event.title),
206
+ ...(event.why ? { why: redactDiagnosticText(event.why) } : {}),
207
+ ...(event.payload ? { payload: sanitizeDiagnosticValue(event.payload) } : {}),
208
+ }));
209
+ const relationEvents = cycle.timeline.map((event) => ({
210
+ seq: event.seq, type: event.type, ts: event.ts, payload: event.payload,
211
+ human: { title: event.title, ...(event.why ? { why: event.why } : {}) },
212
+ }));
213
+ return this.json(res, { ...cycle, timeline: safeTimeline, relations: eventListRelations(relationEvents) });
214
+ }
156
215
  if (p === '/api/narrative') {
157
- return this.json(res, ev.buildNarrativeSnapshot(this.eventSnapshots.read(), { limit: positiveIntParam(url.searchParams.get('limit')) }));
216
+ return this.json(res, ev.buildNarrativeSnapshot(await this.eventSnapshots.read(), { limit: positiveIntParam(url.searchParams.get('limit')) }));
158
217
  }
159
218
  if (p === '/api/triggers')
160
- return this.json(res, ev.listTriggers(this.eventSnapshots.read()));
219
+ return this.json(res, ev.listTriggers(await this.eventSnapshots.read()));
161
220
  if (p === '/api/daily-summary') {
162
221
  const day = url.searchParams.get('day') ?? new Date(this.now()).toISOString().slice(0, 10);
163
- return this.json(res, ev.dailySummary(this.eventSnapshots.read(), day));
222
+ return this.json(res, ev.dailySummary(await this.eventSnapshots.read(), day));
164
223
  }
165
224
  if (p === '/api/value') {
166
225
  // Thin pass-through: the provider (composition layer) owns prices + traces; the server only scopes the
167
226
  // window and serializes. No provider wired → an empty summary so the card renders "no savings yet".
168
227
  const window = ops.windowFromSpec(url.searchParams.get('window') ?? undefined, this.now());
169
- const summary = this.deps.valueSummary ? this.deps.valueSummary(window, this.eventSnapshots.read()) : EMPTY_VALUE_SUMMARY;
228
+ const summary = this.deps.valueSummary ? this.deps.valueSummary(window, await this.eventSnapshots.read()) : EMPTY_VALUE_SUMMARY;
170
229
  return this.json(res, summary);
171
230
  }
231
+ if (p === '/api/personality') {
232
+ if (!requireGet(req, res))
233
+ return;
234
+ return this.json(res, this.deps.personalityDiagnostics
235
+ ? await this.deps.personalityDiagnostics()
236
+ : { available: false, error: 'personality_unavailable' });
237
+ }
238
+ if (p === '/api/memory-graph') {
239
+ if (!requireGet(req, res))
240
+ return;
241
+ if (!this.deps.memoryGraphStatus)
242
+ return this.json(res, { available: false });
243
+ try {
244
+ const status = sanitizeMemoryGraphStatus(this.deps.memoryGraphStatus());
245
+ return this.json(res, { available: true, ...status });
246
+ }
247
+ catch {
248
+ return this.send(res, 503, 'application/json', JSON.stringify({
249
+ available: false,
250
+ error: 'memory_graph_unavailable',
251
+ }));
252
+ }
253
+ }
254
+ if (p === '/api/logs') {
255
+ if (!requireGet(req, res))
256
+ return;
257
+ return this.json(res, this.deps.logDiagnostics
258
+ ? await this.deps.logDiagnostics()
259
+ : { available: false, error: 'logs_unavailable' });
260
+ }
261
+ if (p === '/api/github-prs') {
262
+ if (!requireGet(req, res))
263
+ return;
264
+ return this.json(res, this.deps.githubPrDiagnostics
265
+ ? await this.deps.githubPrDiagnostics()
266
+ : { available: false, error: 'github_prs_unavailable' });
267
+ }
172
268
  if (p === '/api/retention') {
173
269
  if (!this.deps.retentionReport)
174
270
  return this.json(res, { available: false });
@@ -197,6 +293,31 @@ export class WebUIServer {
197
293
  const kind = url.searchParams.get('kind');
198
294
  return this.json(res, (await this.deps.store.list(kind ?? undefined, 100)).map((a) => ({ asset_id: a.asset_id, type: a.type, summary: a.summary })));
199
295
  }
296
+ if (p === '/api/asset-lineage/assets') {
297
+ if (!requireGet(req, res))
298
+ return;
299
+ return this.json(res, await listLineageAssets(this.deps.store, {
300
+ page: positiveIntParam(url.searchParams.get('page')),
301
+ pageSize: positiveIntParam(url.searchParams.get('pageSize')),
302
+ }));
303
+ }
304
+ if (p === '/api/asset-lineage') {
305
+ if (!requireGet(req, res))
306
+ return;
307
+ return this.json(res, await loadAssetLineage({
308
+ store: this.deps.store,
309
+ events: () => this.eventSnapshots.read(),
310
+ review: this.review,
311
+ provenance: this.provenance,
312
+ }, url.searchParams.get('id') ?? '', {
313
+ page: positiveIntParam(url.searchParams.get('page')),
314
+ pageSize: positiveIntParam(url.searchParams.get('pageSize')),
315
+ capsulePage: positiveIntParam(url.searchParams.get('capsulePage')),
316
+ capsulePageSize: positiveIntParam(url.searchParams.get('capsulePageSize')),
317
+ eventPage: positiveIntParam(url.searchParams.get('eventPage')),
318
+ eventPageSize: positiveIntParam(url.searchParams.get('eventPageSize')),
319
+ }));
320
+ }
200
321
  if (p === '/api/review' && req.method === 'POST') {
201
322
  // Approve/reject a quarantined draft from the console: the SAME gate the CLI uses — flip the ReviewLedger
202
323
  // state + record an audited actor.human.review.* event. Loopback + bearer already guard the route; actor is
@@ -236,7 +357,7 @@ export class WebUIServer {
236
357
  // record) are appended as context from a bounded list. Pending (quarantined) sorts first. Read-only here.
237
358
  // Match the CLI's semantics exactly: only the unattended distill-observer counts as "auto-drafted"
238
359
  // (manual `ingest --distill` / skill2gep emit gene.distilled with a different source and are NOT auto).
239
- const autoDrafted = new Set(this.eventSnapshots.read()
360
+ const autoDrafted = new Set((await this.eventSnapshots.read())
240
361
  .filter((e) => e.type === 'gene.distilled' && e.payload?.['source'] === 'distill-observer')
241
362
  .map((e) => String(e.payload?.['assetId'] ?? ''))
242
363
  .filter(Boolean));
@@ -271,8 +392,8 @@ export class WebUIServer {
271
392
  }
272
393
  return this.send(res, 404, 'application/json', JSON.stringify({ error: 'not found' }));
273
394
  }
274
- catch (e) {
275
- return this.send(res, 500, 'application/json', JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
395
+ catch {
396
+ return this.send(res, 500, 'application/json', JSON.stringify({ error: 'dashboard_request_failed' }));
276
397
  }
277
398
  }
278
399
  readJson(req) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver-webui",
3
- "version": "2.0.0-beta.2",
3
+ "version": "2.0.0-beta.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "保活/可视化 WebUI",
@@ -13,7 +13,11 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "@evomap/evolver-core": "2.0.0-beta.2"
16
+ "@evomap/evolver-core": "2.0.0-beta.4"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/EvoMap/evolver.git"
17
21
  },
18
22
  "publishConfig": {
19
23
  "access": "public",