@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/assetLineage.d.ts +70 -0
- package/dist/assetLineage.js +256 -0
- package/dist/console.d.ts +1 -1
- package/dist/console.js +95 -52
- package/dist/diagnosticSanitize.d.ts +2 -0
- package/dist/diagnosticSanitize.js +49 -0
- package/dist/eventSnapshot.d.ts +11 -6
- package/dist/eventSnapshot.js +117 -19
- package/dist/githubPrDiagnostics.d.ts +57 -0
- package/dist/githubPrDiagnostics.js +164 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/logDiagnostics.d.ts +18 -0
- package/dist/logDiagnostics.js +81 -0
- package/dist/observabilityRelations.d.ts +22 -0
- package/dist/observabilityRelations.js +93 -0
- package/dist/personalityDiagnostics.d.ts +41 -0
- package/dist/personalityDiagnostics.js +101 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.js +132 -11
- package/package.json +6 -2
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { redactDiagnosticText } from './diagnosticSanitize.js';
|
|
3
|
+
export const GITHUB_PR_TIMEOUT_MS = 15_000;
|
|
4
|
+
export const GITHUB_PR_MAX_BUFFER_BYTES = 512 * 1024;
|
|
5
|
+
export const GITHUB_PR_CACHE_TTL_MS = 45_000;
|
|
6
|
+
export const GITHUB_PR_MAX_ITEMS = 50;
|
|
7
|
+
const GH_FIELDS = 'number,title,url,state,isDraft,headRefName,baseRefName,updatedAt,reviewDecision,statusCheckRollup';
|
|
8
|
+
export const defaultGithubPrRunner = async (command, args, options) => new Promise((resolve) => {
|
|
9
|
+
execFile(command, [...args], {
|
|
10
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
11
|
+
encoding: 'utf8',
|
|
12
|
+
timeout: options.timeoutMs,
|
|
13
|
+
maxBuffer: options.maxBufferBytes,
|
|
14
|
+
shell: options.shell,
|
|
15
|
+
windowsHide: true,
|
|
16
|
+
}, (error, stdout) => {
|
|
17
|
+
const code = error && typeof error.code === 'number' ? error.code : error ? 1 : 0;
|
|
18
|
+
resolve({ code, stdout: typeof stdout === 'string' ? stdout : '' });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
function boundedInteger(value, fallback, maximum) {
|
|
22
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
23
|
+
return fallback;
|
|
24
|
+
return Math.max(1, Math.min(maximum, Math.floor(value)));
|
|
25
|
+
}
|
|
26
|
+
function safeText(value, maxChars) {
|
|
27
|
+
return redactDiagnosticText(value, maxChars);
|
|
28
|
+
}
|
|
29
|
+
function safeGithubUrl(value) {
|
|
30
|
+
if (typeof value !== 'string' || value.length > 2_000)
|
|
31
|
+
return null;
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(value);
|
|
34
|
+
const hostname = url.hostname.toLowerCase();
|
|
35
|
+
if (url.protocol !== 'https:' || url.port || (hostname !== 'github.com' && !hostname.endsWith('.github.com')))
|
|
36
|
+
return null;
|
|
37
|
+
url.username = '';
|
|
38
|
+
url.password = '';
|
|
39
|
+
url.hash = '';
|
|
40
|
+
url.search = '';
|
|
41
|
+
return url.toString().slice(0, 2_000);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function safeState(value) {
|
|
48
|
+
const state = String(value ?? '').toUpperCase();
|
|
49
|
+
return state === 'OPEN' || state === 'CLOSED' || state === 'MERGED' ? state : 'UNKNOWN';
|
|
50
|
+
}
|
|
51
|
+
function safeReviewDecision(value) {
|
|
52
|
+
if (value === null || value === undefined || value === '')
|
|
53
|
+
return null;
|
|
54
|
+
const decision = String(value).toUpperCase();
|
|
55
|
+
return decision === 'APPROVED' || decision === 'CHANGES_REQUESTED' || decision === 'REVIEW_REQUIRED'
|
|
56
|
+
? decision
|
|
57
|
+
: 'UNKNOWN';
|
|
58
|
+
}
|
|
59
|
+
function safeTimestamp(value) {
|
|
60
|
+
if (typeof value !== 'string')
|
|
61
|
+
return null;
|
|
62
|
+
const text = safeText(value, 64);
|
|
63
|
+
return Number.isNaN(Date.parse(text)) ? null : text;
|
|
64
|
+
}
|
|
65
|
+
function checkCounts(value) {
|
|
66
|
+
const counts = { total: 0, passed: 0, failed: 0, pending: 0 };
|
|
67
|
+
if (!Array.isArray(value))
|
|
68
|
+
return counts;
|
|
69
|
+
for (const raw of value.slice(0, 200)) {
|
|
70
|
+
const record = raw && typeof raw === 'object' ? raw : {};
|
|
71
|
+
const status = String(record['conclusion'] ?? record['state'] ?? record['status'] ?? '').toUpperCase();
|
|
72
|
+
counts.total += 1;
|
|
73
|
+
if (['SUCCESS', 'NEUTRAL', 'SKIPPED'].includes(status))
|
|
74
|
+
counts.passed += 1;
|
|
75
|
+
else if (['FAILURE', 'ERROR', 'CANCELLED', 'TIMED_OUT', 'ACTION_REQUIRED', 'STARTUP_FAILURE'].includes(status))
|
|
76
|
+
counts.failed += 1;
|
|
77
|
+
else
|
|
78
|
+
counts.pending += 1;
|
|
79
|
+
}
|
|
80
|
+
return counts;
|
|
81
|
+
}
|
|
82
|
+
function parsePr(raw) {
|
|
83
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
84
|
+
return null;
|
|
85
|
+
const record = raw;
|
|
86
|
+
const number = typeof record['number'] === 'number' ? Math.floor(record['number']) : Number.NaN;
|
|
87
|
+
const url = safeGithubUrl(record['url']);
|
|
88
|
+
if (!Number.isSafeInteger(number) || number <= 0 || !url)
|
|
89
|
+
return null;
|
|
90
|
+
return {
|
|
91
|
+
number,
|
|
92
|
+
title: safeText(record['title'], 300),
|
|
93
|
+
url,
|
|
94
|
+
state: safeState(record['state']),
|
|
95
|
+
isDraft: record['isDraft'] === true,
|
|
96
|
+
head: safeText(record['headRefName'], 200),
|
|
97
|
+
base: safeText(record['baseRefName'], 200),
|
|
98
|
+
updatedAt: safeTimestamp(record['updatedAt']),
|
|
99
|
+
reviewDecision: safeReviewDecision(record['reviewDecision']),
|
|
100
|
+
checks: checkCounts(record['statusCheckRollup']),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function loadGithubPrDiagnostics(runner, options) {
|
|
104
|
+
try {
|
|
105
|
+
const result = await runner('gh', [
|
|
106
|
+
'pr', 'list', '--state', 'all', '--limit', String(options.maxItems), '--json', GH_FIELDS,
|
|
107
|
+
], {
|
|
108
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
109
|
+
timeoutMs: GITHUB_PR_TIMEOUT_MS,
|
|
110
|
+
maxBufferBytes: GITHUB_PR_MAX_BUFFER_BYTES,
|
|
111
|
+
shell: false,
|
|
112
|
+
});
|
|
113
|
+
if (result.code !== 0)
|
|
114
|
+
return { available: false, error: 'github_pr_unavailable' };
|
|
115
|
+
let raw;
|
|
116
|
+
try {
|
|
117
|
+
raw = JSON.parse(result.stdout || '[]');
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return { available: false, error: 'github_pr_invalid_response' };
|
|
121
|
+
}
|
|
122
|
+
if (!Array.isArray(raw))
|
|
123
|
+
return { available: false, error: 'github_pr_invalid_response' };
|
|
124
|
+
const selected = raw.slice(0, options.maxItems);
|
|
125
|
+
const prs = selected.map(parsePr).filter((row) => row !== null);
|
|
126
|
+
return {
|
|
127
|
+
available: true,
|
|
128
|
+
data: {
|
|
129
|
+
prs,
|
|
130
|
+
truncated: raw.length > selected.length || prs.length < selected.length,
|
|
131
|
+
refreshedAt: new Date(options.now()).toISOString(),
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { available: false, error: 'github_pr_unavailable' };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function createGithubPrDiagnosticsProvider(options = {}) {
|
|
140
|
+
const runner = options.runner ?? defaultGithubPrRunner;
|
|
141
|
+
const now = options.now ?? Date.now;
|
|
142
|
+
const requestedTtl = boundedInteger(options.ttlMs, GITHUB_PR_CACHE_TTL_MS, 60_000);
|
|
143
|
+
const ttlMs = Math.max(30_000, requestedTtl);
|
|
144
|
+
const maxItems = boundedInteger(options.maxItems, GITHUB_PR_MAX_ITEMS, GITHUB_PR_MAX_ITEMS);
|
|
145
|
+
let cached = null;
|
|
146
|
+
let inflight = null;
|
|
147
|
+
return {
|
|
148
|
+
read() {
|
|
149
|
+
const current = now();
|
|
150
|
+
if (cached && current - cached.at < ttlMs)
|
|
151
|
+
return Promise.resolve(cached.value);
|
|
152
|
+
if (inflight)
|
|
153
|
+
return inflight;
|
|
154
|
+
inflight = loadGithubPrDiagnostics(runner, { cwd: options.cwd, now, maxItems })
|
|
155
|
+
.then((value) => {
|
|
156
|
+
if (value.available)
|
|
157
|
+
cached = { at: now(), value };
|
|
158
|
+
return value;
|
|
159
|
+
})
|
|
160
|
+
.finally(() => { inflight = null; });
|
|
161
|
+
return inflight;
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export declare const PACKAGE = "@evomap/evolver-webui";
|
|
2
2
|
export * from './server.js';
|
|
3
|
-
export { CONSOLE_HTML } from './console.js';
|
|
3
|
+
export { CONSOLE_HTML } from './console.js';
|
|
4
|
+
export * from './personalityDiagnostics.js';
|
|
5
|
+
export * from './logDiagnostics.js';
|
|
6
|
+
export * from './githubPrDiagnostics.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export const PACKAGE = '@evomap/evolver-webui';
|
|
2
2
|
export * from './server.js';
|
|
3
|
-
export { CONSOLE_HTML } from './console.js';
|
|
3
|
+
export { CONSOLE_HTML } from './console.js';
|
|
4
|
+
export * from './personalityDiagnostics.js';
|
|
5
|
+
export * from './logDiagnostics.js';
|
|
6
|
+
export * from './githubPrDiagnostics.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const LOG_DIAGNOSTICS_MAX_BYTES: number;
|
|
2
|
+
export declare const LOG_DIAGNOSTICS_MAX_LINES = 200;
|
|
3
|
+
export interface LogDiagnosticsData {
|
|
4
|
+
lines: string[];
|
|
5
|
+
truncated: boolean;
|
|
6
|
+
}
|
|
7
|
+
export type LogDiagnosticsResult = {
|
|
8
|
+
available: true;
|
|
9
|
+
data: LogDiagnosticsData;
|
|
10
|
+
} | {
|
|
11
|
+
available: false;
|
|
12
|
+
error: 'log_not_found' | 'log_unavailable';
|
|
13
|
+
};
|
|
14
|
+
export interface LogDiagnosticsOptions {
|
|
15
|
+
maxBytes?: number;
|
|
16
|
+
maxLines?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function readLogDiagnostics(logFile: string, options?: LogDiagnosticsOptions): LogDiagnosticsResult;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from 'node:fs';
|
|
2
|
+
export const LOG_DIAGNOSTICS_MAX_BYTES = 128 * 1024;
|
|
3
|
+
export const LOG_DIAGNOSTICS_MAX_LINES = 200;
|
|
4
|
+
const LOG_DIAGNOSTICS_HARD_MAX_BYTES = 1024 * 1024;
|
|
5
|
+
const LOG_DIAGNOSTICS_HARD_MAX_LINES = 1_000;
|
|
6
|
+
const MAX_LINE_CHARS = 4_000;
|
|
7
|
+
function boundedInteger(value, fallback, maximum) {
|
|
8
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
9
|
+
return fallback;
|
|
10
|
+
return Math.max(1, Math.min(maximum, Math.floor(value)));
|
|
11
|
+
}
|
|
12
|
+
function redactSecrets(input) {
|
|
13
|
+
let text = input;
|
|
14
|
+
text = text.replace(/-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)* PRIVATE KEY-----/gi, '[redacted private key]');
|
|
15
|
+
text = text.replace(/-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----[\s\S]*$/gi, '[redacted private key]');
|
|
16
|
+
text = text.replace(/\bBearer\s+[^\s,;"']+/gi, 'Bearer [redacted]');
|
|
17
|
+
text = text.replace(/\b(authorization|proxy-authorization|api[_ -]?key|token|access[_ -]?token|refresh[_ -]?token|password|passwd|cookie|set-cookie)\b(\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi, '$1$2[redacted]');
|
|
18
|
+
text = text.replace(/([?&](?:api[_-]?key|token|access_token|password)=)[^&\s]+/gi, '$1[redacted]');
|
|
19
|
+
return text;
|
|
20
|
+
}
|
|
21
|
+
function replaceUnsafeControls(value) {
|
|
22
|
+
let out = '';
|
|
23
|
+
for (const char of value) {
|
|
24
|
+
const code = char.codePointAt(0) ?? 0;
|
|
25
|
+
out += code >= 0x20 || code === 0x09 || code === 0x0a || code === 0x0d ? char : ' ';
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function safeLine(line) {
|
|
30
|
+
const normalized = replaceUnsafeControls(line);
|
|
31
|
+
if (/^[A-Za-z0-9+/]{40,}={0,2}$/.test(normalized.trim()))
|
|
32
|
+
return '[redacted private key material]';
|
|
33
|
+
return normalized.slice(0, MAX_LINE_CHARS);
|
|
34
|
+
}
|
|
35
|
+
export function readLogDiagnostics(logFile, options = {}) {
|
|
36
|
+
const maxBytes = boundedInteger(options.maxBytes, LOG_DIAGNOSTICS_MAX_BYTES, LOG_DIAGNOSTICS_HARD_MAX_BYTES);
|
|
37
|
+
const maxLines = boundedInteger(options.maxLines, LOG_DIAGNOSTICS_MAX_LINES, LOG_DIAGNOSTICS_HARD_MAX_LINES);
|
|
38
|
+
let fd;
|
|
39
|
+
try {
|
|
40
|
+
const before = lstatSync(logFile);
|
|
41
|
+
if (before.isSymbolicLink() || !before.isFile())
|
|
42
|
+
return { available: false, error: 'log_unavailable' };
|
|
43
|
+
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
44
|
+
fd = openSync(logFile, constants.O_RDONLY | noFollow);
|
|
45
|
+
const stats = fstatSync(fd);
|
|
46
|
+
const current = lstatSync(logFile);
|
|
47
|
+
if (current.isSymbolicLink() || !current.isFile() || !stats.isFile()
|
|
48
|
+
|| current.dev !== stats.dev || current.ino !== stats.ino) {
|
|
49
|
+
return { available: false, error: 'log_unavailable' };
|
|
50
|
+
}
|
|
51
|
+
const size = stats.size;
|
|
52
|
+
const bytes = Math.min(size, maxBytes);
|
|
53
|
+
const start = Math.max(0, size - bytes);
|
|
54
|
+
const buffer = Buffer.alloc(bytes);
|
|
55
|
+
if (bytes > 0)
|
|
56
|
+
readSync(fd, buffer, 0, bytes, start);
|
|
57
|
+
let text = buffer.toString('utf8');
|
|
58
|
+
if (start > 0) {
|
|
59
|
+
const firstNewline = text.indexOf('\n');
|
|
60
|
+
text = firstNewline >= 0 ? text.slice(firstNewline + 1) : '';
|
|
61
|
+
}
|
|
62
|
+
const redacted = redactSecrets(text);
|
|
63
|
+
const allLines = redacted.split(/\r?\n/).filter((line) => line.length > 0);
|
|
64
|
+
const lineTruncated = allLines.length > maxLines;
|
|
65
|
+
return {
|
|
66
|
+
available: true,
|
|
67
|
+
data: {
|
|
68
|
+
lines: allLines.slice(-maxLines).map(safeLine),
|
|
69
|
+
truncated: start > 0 || lineTruncated,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const code = error && typeof error === 'object' ? error.code : undefined;
|
|
75
|
+
return { available: false, error: code === 'ENOENT' ? 'log_not_found' : 'log_unavailable' };
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
if (fd !== undefined)
|
|
79
|
+
closeSync(fd);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { events as ev } from '@evomap/evolver-core';
|
|
2
|
+
export interface AssetRelation {
|
|
3
|
+
id: string;
|
|
4
|
+
assetId: string;
|
|
5
|
+
type: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TrajectoryRelation {
|
|
8
|
+
traceId: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PullRequestRelation {
|
|
12
|
+
number: number | null;
|
|
13
|
+
url: string;
|
|
14
|
+
repo: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ObservabilityRelations {
|
|
17
|
+
assets: AssetRelation[];
|
|
18
|
+
trajectories: TrajectoryRelation[];
|
|
19
|
+
pullRequests: PullRequestRelation[];
|
|
20
|
+
}
|
|
21
|
+
export declare function eventRelations(event: ev.ReportEvent): ObservabilityRelations;
|
|
22
|
+
export declare function eventListRelations(events: readonly ev.ReportEvent[]): ObservabilityRelations;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { redactDiagnosticText } from './diagnosticSanitize.js';
|
|
2
|
+
const MAX_ID = 160;
|
|
3
|
+
const MAX_RELATIONS = 50;
|
|
4
|
+
const OPAQUE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,159}$/;
|
|
5
|
+
function safeOpaqueId(value) {
|
|
6
|
+
if (typeof value !== 'string')
|
|
7
|
+
return '';
|
|
8
|
+
const id = value.trim();
|
|
9
|
+
return OPAQUE_ID_RE.test(id) ? id : '';
|
|
10
|
+
}
|
|
11
|
+
function safeDisplayText(value) {
|
|
12
|
+
if (typeof value !== 'string')
|
|
13
|
+
return '';
|
|
14
|
+
return redactDiagnosticText(value, MAX_ID);
|
|
15
|
+
}
|
|
16
|
+
function safePrUrl(value) {
|
|
17
|
+
if (typeof value !== 'string')
|
|
18
|
+
return '';
|
|
19
|
+
const raw = value.replace(/[\r\n\t]/g, '').trim();
|
|
20
|
+
if (!raw)
|
|
21
|
+
return '';
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(raw);
|
|
24
|
+
if (url.protocol !== 'https:')
|
|
25
|
+
return '';
|
|
26
|
+
const host = url.hostname.toLowerCase();
|
|
27
|
+
if ((host !== 'github.com' && !host.endsWith('.github.com')) || url.port)
|
|
28
|
+
return '';
|
|
29
|
+
url.username = '';
|
|
30
|
+
url.password = '';
|
|
31
|
+
url.search = '';
|
|
32
|
+
url.hash = '';
|
|
33
|
+
return url.toString().slice(0, MAX_ID);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function positivePrNumber(value) {
|
|
40
|
+
const number = typeof value === 'number' ? value : Number(value);
|
|
41
|
+
return Number.isSafeInteger(number) && number > 0 ? number : null;
|
|
42
|
+
}
|
|
43
|
+
function addAsset(out, value, type) {
|
|
44
|
+
const id = safeOpaqueId(value);
|
|
45
|
+
if (!id || out.size >= MAX_RELATIONS)
|
|
46
|
+
return;
|
|
47
|
+
const key = `${type}:${id}`;
|
|
48
|
+
if (!out.has(key))
|
|
49
|
+
out.set(key, { id, assetId: id, type });
|
|
50
|
+
}
|
|
51
|
+
function addAssetArray(out, value, type) {
|
|
52
|
+
if (!Array.isArray(value))
|
|
53
|
+
return;
|
|
54
|
+
for (const item of value)
|
|
55
|
+
addAsset(out, item, type);
|
|
56
|
+
}
|
|
57
|
+
function relationsFromPayload(payload) {
|
|
58
|
+
const assets = new Map();
|
|
59
|
+
addAsset(assets, payload['assetId'] ?? payload['asset_id'], 'asset');
|
|
60
|
+
addAsset(assets, payload['geneId'] ?? payload['gene'], 'gene');
|
|
61
|
+
addAsset(assets, payload['capsuleId'] ?? payload['capsule_id'], 'capsule');
|
|
62
|
+
addAssetArray(assets, payload['assetIds'] ?? payload['asset_ids'], 'asset');
|
|
63
|
+
addAssetArray(assets, payload['genesUsed'] ?? payload['genes_used'] ?? payload['genes'], 'gene');
|
|
64
|
+
const traceId = safeOpaqueId(payload['traceId'] ?? payload['trace_id'] ?? payload['trajectoryId'] ?? payload['trajectory_id']);
|
|
65
|
+
const sessionId = safeOpaqueId(payload['sessionId'] ?? payload['session_id']);
|
|
66
|
+
const trajectories = traceId || sessionId ? [{ traceId, sessionId }] : [];
|
|
67
|
+
const url = safePrUrl(payload['pullRequestUrl'] ?? payload['pull_request_url'] ?? payload['prUrl'] ?? payload['pr_url'] ?? payload['githubPrUrl']);
|
|
68
|
+
const number = positivePrNumber(payload['pullRequestNumber'] ?? payload['pull_request_number'] ?? payload['prNumber'] ?? payload['pr_number'] ?? payload['githubPrNumber']);
|
|
69
|
+
const repo = safeDisplayText(payload['repo'] ?? payload['repository'] ?? payload['githubRepo']);
|
|
70
|
+
const pullRequests = url || number !== null ? [{ number, url, repo }] : [];
|
|
71
|
+
return { assets: [...assets.values()], trajectories, pullRequests };
|
|
72
|
+
}
|
|
73
|
+
function dedupe(items, key) {
|
|
74
|
+
const out = new Map();
|
|
75
|
+
for (const item of items) {
|
|
76
|
+
const itemKey = key(item);
|
|
77
|
+
if (!itemKey || out.has(itemKey) || out.size >= MAX_RELATIONS)
|
|
78
|
+
continue;
|
|
79
|
+
out.set(itemKey, item);
|
|
80
|
+
}
|
|
81
|
+
return [...out.values()];
|
|
82
|
+
}
|
|
83
|
+
export function eventRelations(event) {
|
|
84
|
+
return relationsFromPayload((event.payload ?? {}));
|
|
85
|
+
}
|
|
86
|
+
export function eventListRelations(events) {
|
|
87
|
+
const relations = events.map(eventRelations);
|
|
88
|
+
return {
|
|
89
|
+
assets: dedupe(relations.flatMap((entry) => entry.assets), (item) => `${item.type}:${item.assetId}`),
|
|
90
|
+
trajectories: dedupe(relations.flatMap((entry) => entry.trajectories), (item) => `${item.traceId}:${item.sessionId}`),
|
|
91
|
+
pullRequests: dedupe(relations.flatMap((entry) => entry.pullRequests), (item) => `${item.url}:${item.repo}:${item.number ?? ''}`),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type personality as PersonalityNamespace } from '@evomap/evolver-core';
|
|
2
|
+
export declare const PERSONALITY_DIAGNOSTICS_MAX_STATS = 40;
|
|
3
|
+
export declare const PERSONALITY_DIAGNOSTICS_MAX_HISTORY = 60;
|
|
4
|
+
export type PersonalityAxisValues = Pick<PersonalityNamespace.PersonalityState, 'rigor' | 'creativity' | 'verbosity' | 'risk_tolerance' | 'obedience'>;
|
|
5
|
+
export interface PersonalityDiagnosticStat {
|
|
6
|
+
key: string;
|
|
7
|
+
success: number;
|
|
8
|
+
fail: number;
|
|
9
|
+
avgScore: number;
|
|
10
|
+
n: number;
|
|
11
|
+
updatedAt: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface PersonalityDiagnosticHistoryEntry {
|
|
14
|
+
at: string;
|
|
15
|
+
key: string;
|
|
16
|
+
outcome: string;
|
|
17
|
+
score: number | null;
|
|
18
|
+
}
|
|
19
|
+
export interface PersonalityDiagnosticData {
|
|
20
|
+
current: PersonalityAxisValues;
|
|
21
|
+
updatedAt: string | null;
|
|
22
|
+
stats: PersonalityDiagnosticStat[];
|
|
23
|
+
history: PersonalityDiagnosticHistoryEntry[];
|
|
24
|
+
truncated: {
|
|
25
|
+
stats: boolean;
|
|
26
|
+
history: boolean;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export type PersonalityDiagnosticsResult = {
|
|
30
|
+
available: true;
|
|
31
|
+
data: PersonalityDiagnosticData;
|
|
32
|
+
} | {
|
|
33
|
+
available: false;
|
|
34
|
+
error: 'personality_unavailable';
|
|
35
|
+
};
|
|
36
|
+
export type PersonalityDiagnosticsReader = () => unknown | Promise<unknown>;
|
|
37
|
+
export interface PersonalityDiagnosticsOptions {
|
|
38
|
+
maxStats?: number;
|
|
39
|
+
maxHistory?: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function readPersonalityDiagnostics(reader: PersonalityDiagnosticsReader | undefined, options?: PersonalityDiagnosticsOptions): Promise<PersonalityDiagnosticsResult>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { personality } from '@evomap/evolver-core';
|
|
2
|
+
import { redactDiagnosticText } from './diagnosticSanitize.js';
|
|
3
|
+
export const PERSONALITY_DIAGNOSTICS_MAX_STATS = 40;
|
|
4
|
+
export const PERSONALITY_DIAGNOSTICS_MAX_HISTORY = 60;
|
|
5
|
+
const MAX_TEXT_CHARS = 240;
|
|
6
|
+
function boundedInteger(value, fallback, maximum) {
|
|
7
|
+
if (!Number.isFinite(value))
|
|
8
|
+
return fallback;
|
|
9
|
+
return Math.max(1, Math.min(maximum, Math.floor(value)));
|
|
10
|
+
}
|
|
11
|
+
function finiteNumber(value, fallback = 0) {
|
|
12
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
13
|
+
}
|
|
14
|
+
function nonnegativeInteger(value) {
|
|
15
|
+
return Math.max(0, Math.floor(finiteNumber(value)));
|
|
16
|
+
}
|
|
17
|
+
function replaceControlCharacters(value) {
|
|
18
|
+
let out = '';
|
|
19
|
+
for (const char of value) {
|
|
20
|
+
const code = char.codePointAt(0) ?? 0;
|
|
21
|
+
out += code < 0x20 || code === 0x7f ? ' ' : char;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function boundedText(value, maxChars = MAX_TEXT_CHARS) {
|
|
26
|
+
return replaceControlCharacters(String(value ?? '')).slice(0, maxChars);
|
|
27
|
+
}
|
|
28
|
+
function nullableTimestamp(value) {
|
|
29
|
+
if (typeof value !== 'string')
|
|
30
|
+
return null;
|
|
31
|
+
const text = boundedText(value, 64);
|
|
32
|
+
return Number.isNaN(Date.parse(text)) ? null : text;
|
|
33
|
+
}
|
|
34
|
+
function axisValue(value) {
|
|
35
|
+
return Math.max(0, Math.min(1, finiteNumber(value)));
|
|
36
|
+
}
|
|
37
|
+
function currentAxes(value) {
|
|
38
|
+
const record = value && typeof value === 'object' ? value : {};
|
|
39
|
+
return {
|
|
40
|
+
rigor: axisValue(record['rigor']),
|
|
41
|
+
creativity: axisValue(record['creativity']),
|
|
42
|
+
verbosity: axisValue(record['verbosity']),
|
|
43
|
+
risk_tolerance: axisValue(record['risk_tolerance']),
|
|
44
|
+
obedience: axisValue(record['obedience']),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function statRows(value) {
|
|
48
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
49
|
+
return [];
|
|
50
|
+
return Object.entries(value).map(([key, raw]) => {
|
|
51
|
+
const record = raw && typeof raw === 'object' ? raw : {};
|
|
52
|
+
return {
|
|
53
|
+
key: redactDiagnosticText(key, MAX_TEXT_CHARS),
|
|
54
|
+
success: nonnegativeInteger(record['success']),
|
|
55
|
+
fail: nonnegativeInteger(record['fail']),
|
|
56
|
+
avgScore: axisValue(record['avgScore']),
|
|
57
|
+
n: nonnegativeInteger(record['n']),
|
|
58
|
+
updatedAt: nullableTimestamp(record['updatedAt']),
|
|
59
|
+
};
|
|
60
|
+
}).sort((left, right) => (right.updatedAt ?? '').localeCompare(left.updatedAt ?? ''));
|
|
61
|
+
}
|
|
62
|
+
function historyRows(value) {
|
|
63
|
+
if (!Array.isArray(value))
|
|
64
|
+
return [];
|
|
65
|
+
return value.map((raw) => {
|
|
66
|
+
const record = raw && typeof raw === 'object' ? raw : {};
|
|
67
|
+
return {
|
|
68
|
+
at: nullableTimestamp(record['at']) ?? '',
|
|
69
|
+
key: redactDiagnosticText(record['key'], MAX_TEXT_CHARS),
|
|
70
|
+
outcome: redactDiagnosticText(record['outcome'], MAX_TEXT_CHARS),
|
|
71
|
+
score: record['score'] === null ? null : axisValue(record['score']),
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export async function readPersonalityDiagnostics(reader, options = {}) {
|
|
76
|
+
if (!reader)
|
|
77
|
+
return { available: false, error: 'personality_unavailable' };
|
|
78
|
+
try {
|
|
79
|
+
const raw = await reader();
|
|
80
|
+
const parsed = personality.personalityModel.safeParse(raw);
|
|
81
|
+
if (!parsed.success)
|
|
82
|
+
return { available: false, error: 'personality_unavailable' };
|
|
83
|
+
const maxStats = boundedInteger(options.maxStats, PERSONALITY_DIAGNOSTICS_MAX_STATS, PERSONALITY_DIAGNOSTICS_MAX_STATS);
|
|
84
|
+
const maxHistory = boundedInteger(options.maxHistory, PERSONALITY_DIAGNOSTICS_MAX_HISTORY, PERSONALITY_DIAGNOSTICS_MAX_HISTORY);
|
|
85
|
+
const stats = statRows(parsed.data.stats);
|
|
86
|
+
const history = historyRows(parsed.data.history);
|
|
87
|
+
return {
|
|
88
|
+
available: true,
|
|
89
|
+
data: {
|
|
90
|
+
current: currentAxes(parsed.data.current),
|
|
91
|
+
updatedAt: nullableTimestamp(parsed.data.updatedAt),
|
|
92
|
+
stats: stats.slice(0, maxStats),
|
|
93
|
+
history: history.slice(-maxHistory).reverse(),
|
|
94
|
+
truncated: { stats: stats.length > maxStats, history: history.length > maxHistory },
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { available: false, error: 'personality_unavailable' };
|
|
100
|
+
}
|
|
101
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import { events as ev, assetstore, mailbox as mb, ops } from '@evomap/evolver-core';
|
|
2
2
|
import { type EventSnapshotSource } from './eventSnapshot.js';
|
|
3
|
+
export interface MemoryGraphStatus {
|
|
4
|
+
recovery: 'healthy' | 'degraded' | 'recovered' | 'empty';
|
|
5
|
+
compactedRecords: number;
|
|
6
|
+
activeRecords: number;
|
|
7
|
+
corruptLines: number;
|
|
8
|
+
oversizedLines: number;
|
|
9
|
+
oversizedFiles: number;
|
|
10
|
+
archives: number;
|
|
11
|
+
selectionReason?: string;
|
|
12
|
+
}
|
|
13
|
+
export type MemoryGraphStatusResponse = ({
|
|
14
|
+
available: true;
|
|
15
|
+
} & MemoryGraphStatus) | {
|
|
16
|
+
available: false;
|
|
17
|
+
error?: 'memory_graph_unavailable';
|
|
18
|
+
};
|
|
3
19
|
export interface WebUIServerDeps {
|
|
4
20
|
eventsPath: string;
|
|
5
21
|
ingestor?: ev.Ingestor;
|
|
6
22
|
store?: assetstore.AssetStoreProvider;
|
|
7
23
|
/** 人审队列用的 review ledger(#117 人审门)。缺省由 LocalJsonlProvider store 的 baseDir 推导。 */
|
|
8
24
|
review?: assetstore.ReviewLedger;
|
|
25
|
+
/** Optional provenance sidecar; inferred for a LocalJsonlProvider when absent. */
|
|
26
|
+
provenance?: assetstore.ProvenanceStore;
|
|
9
27
|
mailbox?: mb.MailboxStore;
|
|
10
28
|
now?: () => number;
|
|
11
29
|
host?: string;
|
|
@@ -22,8 +40,14 @@ export interface WebUIServerDeps {
|
|
|
22
40
|
valueSummary?: (window: ops.SummaryWindow, events: readonly ev.ReportEvent[]) => ops.ValueSummary;
|
|
23
41
|
/** Shared core retention report provider. Kept injectable so WebUI never owns filesystem policy or paths. */
|
|
24
42
|
retentionReport?: () => ev.RetentionReport;
|
|
43
|
+
/** Read-only, already scoped MemoryGraph operator status. Re-read for every request; WebUI only exposes a sanitized allowlist. */
|
|
44
|
+
memoryGraphStatus?: () => MemoryGraphStatus;
|
|
25
45
|
/** Injectable file/source seam for versioned root-event snapshots. */
|
|
26
46
|
eventSource?: EventSnapshotSource;
|
|
47
|
+
/** Optional bounded diagnostics providers. Each source degrades independently. */
|
|
48
|
+
personalityDiagnostics?: () => unknown | Promise<unknown>;
|
|
49
|
+
logDiagnostics?: () => unknown | Promise<unknown>;
|
|
50
|
+
githubPrDiagnostics?: () => unknown | Promise<unknown>;
|
|
27
51
|
}
|
|
28
52
|
/**
|
|
29
53
|
* WebUI 控台(M7): 可观测 + 保活. node:http + 自包含 HTML, 仅绑 loopback.
|
|
@@ -38,6 +62,7 @@ export declare class WebUIServer {
|
|
|
38
62
|
private readonly actorId;
|
|
39
63
|
/** Review ledger backing the human-review queue. Undefined when no LocalJsonlProvider store is available. */
|
|
40
64
|
private readonly review;
|
|
65
|
+
private readonly provenance;
|
|
41
66
|
/** Token guarding /api/*; supplied by the browser via Bearer, with ?token= retained for compatibility. */
|
|
42
67
|
readonly token: string;
|
|
43
68
|
readonly launchTicket: string;
|