@evomap/evolver-webui 2.0.0-beta.2 → 2.0.0-beta.22
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 +105 -52
- package/dist/diagnosticSanitize.d.ts +3 -0
- package/dist/diagnosticSanitize.js +788 -0
- package/dist/eventSnapshot.d.ts +12 -6
- package/dist/eventSnapshot.js +127 -21
- 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/jsoncScannerShim.d.ts +7 -0
- package/dist/jsoncScannerShim.js +10 -0
- package/dist/logDiagnostics.d.ts +18 -0
- package/dist/logDiagnostics.js +103 -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 +59 -1
- package/dist/server.js +260 -21
- package/package.json +10 -2
package/dist/eventSnapshot.d.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { events as ev } from '@evomap/evolver-core';
|
|
2
|
+
type Awaitable<T> = T | PromiseLike<T>;
|
|
3
|
+
export type EventSnapshotReader = (eventsPath: string) => Awaitable<readonly ev.ReportEvent[]>;
|
|
2
4
|
export interface EventSnapshotSource {
|
|
3
|
-
version(): string
|
|
4
|
-
read(): readonly ev.ReportEvent[]
|
|
5
|
+
version(): Awaitable<string>;
|
|
6
|
+
read(): Awaitable<readonly ev.ReportEvent[]>;
|
|
5
7
|
}
|
|
6
|
-
export declare function fileEventSnapshotSource(eventsPath: string): EventSnapshotSource;
|
|
7
|
-
/** Reuses a parsed
|
|
8
|
+
export declare function fileEventSnapshotSource(eventsPath: string, readEvents?: EventSnapshotReader): EventSnapshotSource;
|
|
9
|
+
/** Reuses a parsed event history only when the active file and archive segments stay stable across the read. */
|
|
8
10
|
export declare class EventSnapshotCache {
|
|
9
11
|
private readonly source;
|
|
10
12
|
private cached;
|
|
13
|
+
private inFlight;
|
|
11
14
|
constructor(source: EventSnapshotSource);
|
|
12
|
-
read(): readonly ev.ReportEvent[]
|
|
13
|
-
|
|
15
|
+
read(): Promise<readonly ev.ReportEvent[]>;
|
|
16
|
+
private readOnce;
|
|
17
|
+
private readVersioned;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
package/dist/eventSnapshot.js
CHANGED
|
@@ -1,51 +1,157 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { Worker } from 'node:worker_threads';
|
|
2
4
|
import { events as ev } from '@evomap/evolver-core';
|
|
5
|
+
const ARCHIVE_SEGMENT_PATTERN = /^root-events-\d{16}-\d{16}\.jsonl$/;
|
|
6
|
+
const MAX_VERSIONED_READ_ATTEMPTS = 2;
|
|
7
|
+
const EVENT_SNAPSHOT_WORKER_SOURCE = `
|
|
8
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const core = await import(workerData.coreModuleUrl);
|
|
12
|
+
const events = core.events.readEvents(workerData.eventsPath);
|
|
13
|
+
parentPort.postMessage({ ok: true, events });
|
|
14
|
+
} catch (error) {
|
|
15
|
+
parentPort.postMessage({
|
|
16
|
+
ok: false,
|
|
17
|
+
error: {
|
|
18
|
+
name: error instanceof Error ? error.name : 'Error',
|
|
19
|
+
message: error instanceof Error ? error.message : String(error),
|
|
20
|
+
code: error && typeof error === 'object' && 'code' in error ? error.code : undefined,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
const EVENT_SNAPSHOT_WORKER_URL = new URL(`data:text/javascript,${encodeURIComponent(EVENT_SNAPSHOT_WORKER_SOURCE)}`);
|
|
3
26
|
function isMissing(error) {
|
|
4
27
|
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
|
|
5
28
|
}
|
|
6
|
-
|
|
29
|
+
function statFingerprint(value) {
|
|
30
|
+
return `${value.dev}:${value.ino}:${value.size}:${value.mtimeNs}:${value.ctimeNs}`;
|
|
31
|
+
}
|
|
32
|
+
async function pathFingerprint(path) {
|
|
33
|
+
try {
|
|
34
|
+
return statFingerprint(await stat(path, { bigint: true }));
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (isMissing(error))
|
|
38
|
+
return 'missing';
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function archiveFingerprint(eventsPath) {
|
|
43
|
+
const archiveDir = ev.rootEventArchiveDir(eventsPath);
|
|
44
|
+
let entries;
|
|
45
|
+
try {
|
|
46
|
+
entries = (await readdir(archiveDir)).filter((entry) => ARCHIVE_SEGMENT_PATTERN.test(entry)).sort();
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (isMissing(error))
|
|
50
|
+
return 'missing';
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
const fingerprints = [];
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
fingerprints.push(`${entry}:${await pathFingerprint(join(archiveDir, entry))}`);
|
|
56
|
+
}
|
|
57
|
+
return `${await pathFingerprint(archiveDir)}:[${fingerprints.join(',')}]`;
|
|
58
|
+
}
|
|
59
|
+
async function fileSnapshotVersion(eventsPath) {
|
|
60
|
+
const [active, archive] = await Promise.all([
|
|
61
|
+
pathFingerprint(eventsPath),
|
|
62
|
+
archiveFingerprint(eventsPath),
|
|
63
|
+
]);
|
|
64
|
+
return `active=${active};archive=${archive}`;
|
|
65
|
+
}
|
|
66
|
+
function workerFailure(error) {
|
|
67
|
+
const restored = new Error(error.message);
|
|
68
|
+
restored.name = error.name;
|
|
69
|
+
if (error.code !== undefined)
|
|
70
|
+
Object.assign(restored, { code: error.code });
|
|
71
|
+
return restored;
|
|
72
|
+
}
|
|
73
|
+
function readEventsInWorker(eventsPath) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const worker = new Worker(EVENT_SNAPSHOT_WORKER_URL, {
|
|
76
|
+
workerData: {
|
|
77
|
+
eventsPath,
|
|
78
|
+
coreModuleUrl: new URL('../../evolver-core/dist/index.js', import.meta.url).href,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
let settled = false;
|
|
82
|
+
const settle = (action) => {
|
|
83
|
+
if (settled)
|
|
84
|
+
return;
|
|
85
|
+
settled = true;
|
|
86
|
+
action();
|
|
87
|
+
};
|
|
88
|
+
worker.once('message', (response) => {
|
|
89
|
+
settle(() => {
|
|
90
|
+
if (response.ok)
|
|
91
|
+
resolve(response.events);
|
|
92
|
+
else
|
|
93
|
+
reject(workerFailure(response.error));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
worker.once('error', (error) => settle(() => reject(error)));
|
|
97
|
+
worker.once('exit', (code) => {
|
|
98
|
+
settle(() => reject(new Error(code === 0
|
|
99
|
+
? 'event snapshot worker exited without a response'
|
|
100
|
+
: `event snapshot worker exited with code ${code}`)));
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export function fileEventSnapshotSource(eventsPath, readEvents = readEventsInWorker) {
|
|
7
105
|
return {
|
|
8
|
-
version: () =>
|
|
9
|
-
|
|
10
|
-
const stat = statSync(eventsPath, { bigint: true });
|
|
11
|
-
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
12
|
-
}
|
|
13
|
-
catch (error) {
|
|
14
|
-
if (isMissing(error))
|
|
15
|
-
return 'missing';
|
|
16
|
-
throw error;
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
read: () => ev.readEvents(eventsPath),
|
|
106
|
+
version: () => fileSnapshotVersion(eventsPath),
|
|
107
|
+
read: () => readEvents(eventsPath),
|
|
20
108
|
};
|
|
21
109
|
}
|
|
22
|
-
/** Reuses a parsed
|
|
110
|
+
/** Reuses a parsed event history only when the active file and archive segments stay stable across the read. */
|
|
23
111
|
export class EventSnapshotCache {
|
|
24
112
|
source;
|
|
25
113
|
cached;
|
|
114
|
+
inFlight;
|
|
26
115
|
constructor(source) {
|
|
27
116
|
this.source = source;
|
|
28
117
|
}
|
|
29
118
|
read() {
|
|
119
|
+
if (this.inFlight !== undefined)
|
|
120
|
+
return this.inFlight;
|
|
121
|
+
const pending = this.readOnce().finally(() => {
|
|
122
|
+
if (this.inFlight === pending)
|
|
123
|
+
this.inFlight = undefined;
|
|
124
|
+
});
|
|
125
|
+
this.inFlight = pending;
|
|
126
|
+
return pending;
|
|
127
|
+
}
|
|
128
|
+
async readOnce() {
|
|
30
129
|
let before;
|
|
31
130
|
try {
|
|
32
|
-
before = this.source.version();
|
|
131
|
+
before = await this.source.version();
|
|
33
132
|
}
|
|
34
133
|
catch {
|
|
35
|
-
return this.source.read();
|
|
134
|
+
return await this.source.read();
|
|
36
135
|
}
|
|
136
|
+
return await this.readVersioned(before, MAX_VERSIONED_READ_ATTEMPTS);
|
|
137
|
+
}
|
|
138
|
+
async readVersioned(before, attemptsRemaining) {
|
|
37
139
|
if (this.cached?.version === before)
|
|
38
140
|
return this.cached.events;
|
|
39
|
-
const events = this.source.read();
|
|
141
|
+
const events = await this.source.read();
|
|
40
142
|
let after;
|
|
41
143
|
try {
|
|
42
|
-
after = this.source.version();
|
|
144
|
+
after = await this.source.version();
|
|
43
145
|
}
|
|
44
146
|
catch {
|
|
45
147
|
return events;
|
|
46
148
|
}
|
|
47
|
-
if (before === after)
|
|
149
|
+
if (before === after) {
|
|
48
150
|
this.cached = { version: after, events };
|
|
49
|
-
|
|
151
|
+
return events;
|
|
152
|
+
}
|
|
153
|
+
if (attemptsRemaining === 1)
|
|
154
|
+
return events;
|
|
155
|
+
return await this.readVersioned(after, attemptsRemaining - 1);
|
|
50
156
|
}
|
|
51
157
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export declare const GITHUB_PR_TIMEOUT_MS = 15000;
|
|
2
|
+
export declare const GITHUB_PR_MAX_BUFFER_BYTES: number;
|
|
3
|
+
export declare const GITHUB_PR_CACHE_TTL_MS = 45000;
|
|
4
|
+
export declare const GITHUB_PR_MAX_ITEMS = 50;
|
|
5
|
+
export interface GithubPrCheckCounts {
|
|
6
|
+
total: number;
|
|
7
|
+
passed: number;
|
|
8
|
+
failed: number;
|
|
9
|
+
pending: number;
|
|
10
|
+
}
|
|
11
|
+
export interface GithubPrDiagnosticRow {
|
|
12
|
+
number: number;
|
|
13
|
+
title: string;
|
|
14
|
+
url: string;
|
|
15
|
+
state: 'OPEN' | 'CLOSED' | 'MERGED' | 'UNKNOWN';
|
|
16
|
+
isDraft: boolean;
|
|
17
|
+
head: string;
|
|
18
|
+
base: string;
|
|
19
|
+
updatedAt: string | null;
|
|
20
|
+
reviewDecision: 'APPROVED' | 'CHANGES_REQUESTED' | 'REVIEW_REQUIRED' | 'UNKNOWN' | null;
|
|
21
|
+
checks: GithubPrCheckCounts;
|
|
22
|
+
}
|
|
23
|
+
export interface GithubPrDiagnosticData {
|
|
24
|
+
prs: GithubPrDiagnosticRow[];
|
|
25
|
+
truncated: boolean;
|
|
26
|
+
refreshedAt: string;
|
|
27
|
+
}
|
|
28
|
+
export type GithubPrDiagnosticsResult = {
|
|
29
|
+
available: true;
|
|
30
|
+
data: GithubPrDiagnosticData;
|
|
31
|
+
} | {
|
|
32
|
+
available: false;
|
|
33
|
+
error: 'github_pr_unavailable' | 'github_pr_invalid_response';
|
|
34
|
+
};
|
|
35
|
+
export interface GithubPrRunnerOptions {
|
|
36
|
+
cwd?: string;
|
|
37
|
+
timeoutMs: number;
|
|
38
|
+
maxBufferBytes: number;
|
|
39
|
+
shell: false;
|
|
40
|
+
}
|
|
41
|
+
export interface GithubPrRunnerResult {
|
|
42
|
+
code: number;
|
|
43
|
+
stdout: string;
|
|
44
|
+
}
|
|
45
|
+
export type GithubPrRunner = (command: string, args: readonly string[], options: GithubPrRunnerOptions) => Promise<GithubPrRunnerResult>;
|
|
46
|
+
export interface GithubPrDiagnosticsProvider {
|
|
47
|
+
read(): Promise<GithubPrDiagnosticsResult>;
|
|
48
|
+
}
|
|
49
|
+
export interface GithubPrDiagnosticsProviderOptions {
|
|
50
|
+
cwd?: string;
|
|
51
|
+
runner?: GithubPrRunner;
|
|
52
|
+
now?: () => number;
|
|
53
|
+
ttlMs?: number;
|
|
54
|
+
maxItems?: number;
|
|
55
|
+
}
|
|
56
|
+
export declare const defaultGithubPrRunner: GithubPrRunner;
|
|
57
|
+
export declare function createGithubPrDiagnosticsProvider(options?: GithubPrDiagnosticsProviderOptions): GithubPrDiagnosticsProvider;
|
|
@@ -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,10 @@
|
|
|
1
|
+
// jsonc-parser's main entry is a UMD wrapper whose shadowed `require` leaves
|
|
2
|
+
// './impl/format' unresolved inside bun standalone binaries, and its ESM entry
|
|
3
|
+
// uses extensionless imports that Node ESM rejects. Depend on the scanner impl
|
|
4
|
+
// directly: it has no sibling dependencies, so bundlers can inline it safely.
|
|
5
|
+
// Use a default import because the impl is CommonJS and named CJS exports are
|
|
6
|
+
// not reliably detected by Node's ESM loader. The explicit JsoncScanner type
|
|
7
|
+
// keeps the emitted .d.ts self-contained so consumers never resolve the deep
|
|
8
|
+
// specifier.
|
|
9
|
+
import scannerImpl from 'jsonc-parser/lib/umd/impl/scanner.js';
|
|
10
|
+
export const createScanner = scannerImpl.createScanner;
|
|
@@ -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,103 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from 'node:fs';
|
|
2
|
+
import { hub } from '@evomap/evolver-core';
|
|
3
|
+
import { redactSensitiveHttpHeaders } from './diagnosticSanitize.js';
|
|
4
|
+
export const LOG_DIAGNOSTICS_MAX_BYTES = 128 * 1024;
|
|
5
|
+
export const LOG_DIAGNOSTICS_MAX_LINES = 200;
|
|
6
|
+
const LOG_DIAGNOSTICS_HARD_MAX_BYTES = 1024 * 1024;
|
|
7
|
+
const LOG_DIAGNOSTICS_HARD_MAX_LINES = 1_000;
|
|
8
|
+
const MAX_LINE_CHARS = 4_000;
|
|
9
|
+
function boundedInteger(value, fallback, maximum) {
|
|
10
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
11
|
+
return fallback;
|
|
12
|
+
return Math.max(1, Math.min(maximum, Math.floor(value)));
|
|
13
|
+
}
|
|
14
|
+
function redactSecrets(input) {
|
|
15
|
+
let text = replaceUnsafeControls(input);
|
|
16
|
+
text = text.replace(/-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)* PRIVATE KEY-----/gi, '[redacted private key]');
|
|
17
|
+
text = text.replace(/-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----[\s\S]*$/gi, '[redacted private key]');
|
|
18
|
+
text = text.replace(/\bBearer\s+[^\s,;"']+/gi, 'Bearer [redacted]');
|
|
19
|
+
text = text.replace(/\b(proxy[-_]?authorization|authorization)\b(\s*=\s*)(?!(?:"|')?\[redacted\](?:"|')?)(?:"[^"]*"|'[^']*'|[^\s,;"']+)/gi, '$1$2[redacted]');
|
|
20
|
+
text = text.replace(/\b(cookie|set[-_]?cookie)\b(\s*=\s*)(?!(?:"|')?\[redacted\](?:"|')?)(?:"[^"]*"|'[^']*'|[^\s,;"']+)/gi, '$1$2[redacted]');
|
|
21
|
+
text = text.replace(/\b(api[_ -]?key|token|access[_ -]?token|refresh[_ -]?token|password|passwd)\b(\s*[=:]\s*)(?!(?:"|')?\[redacted\](?:"|')?)(?:"[^"]*"|'[^']*'|[^\s,;"']+)/gi, '$1$2[redacted]');
|
|
22
|
+
text = text.replace(/([?&](?:api[_-]?key|token|access_token|password)=)[^&\s]+/gi, '$1[redacted]');
|
|
23
|
+
text = text.replace(/\b(id[_ -]?token|private[_ -]?key|node[_ -]?secret|account[_ -]?key|instrumentation[_ -]?key)\b(\s*[=:]\s*)[^\s,;]+/gi, '$1$2[redacted]');
|
|
24
|
+
return text
|
|
25
|
+
.split('[redacted]')
|
|
26
|
+
.map((segment) => hub.redactString(segment))
|
|
27
|
+
.join('[redacted]');
|
|
28
|
+
}
|
|
29
|
+
function replaceUnsafeControls(value) {
|
|
30
|
+
let out = '';
|
|
31
|
+
for (const char of value) {
|
|
32
|
+
const code = char.codePointAt(0) ?? 0;
|
|
33
|
+
out += code >= 0x20 || code === 0x09 || code === 0x0a || code === 0x0d ? char : ' ';
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function safeLine(line) {
|
|
38
|
+
const normalized = replaceUnsafeControls(line);
|
|
39
|
+
if (/^[A-Za-z0-9+/]{40,}={0,2}$/.test(normalized.trim()))
|
|
40
|
+
return '[redacted private key material]';
|
|
41
|
+
return normalized.slice(0, MAX_LINE_CHARS);
|
|
42
|
+
}
|
|
43
|
+
function redactTruncatedLeadingContinuations(value) {
|
|
44
|
+
return value.replace(/^(?:[ \t]+[^\r\n]*(?:\r?\n|$))+/, '[redacted truncated continuation]\n');
|
|
45
|
+
}
|
|
46
|
+
export function readLogDiagnostics(logFile, options = {}) {
|
|
47
|
+
const maxBytes = boundedInteger(options.maxBytes, LOG_DIAGNOSTICS_MAX_BYTES, LOG_DIAGNOSTICS_HARD_MAX_BYTES);
|
|
48
|
+
const maxLines = boundedInteger(options.maxLines, LOG_DIAGNOSTICS_MAX_LINES, LOG_DIAGNOSTICS_HARD_MAX_LINES);
|
|
49
|
+
let fd;
|
|
50
|
+
try {
|
|
51
|
+
const before = lstatSync(logFile);
|
|
52
|
+
if (before.isSymbolicLink() || !before.isFile())
|
|
53
|
+
return { available: false, error: 'log_unavailable' };
|
|
54
|
+
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
55
|
+
fd = openSync(logFile, constants.O_RDONLY | noFollow);
|
|
56
|
+
const stats = fstatSync(fd);
|
|
57
|
+
const current = lstatSync(logFile);
|
|
58
|
+
if (current.isSymbolicLink() || !current.isFile() || !stats.isFile()
|
|
59
|
+
|| current.dev !== stats.dev || current.ino !== stats.ino) {
|
|
60
|
+
return { available: false, error: 'log_unavailable' };
|
|
61
|
+
}
|
|
62
|
+
const size = stats.size;
|
|
63
|
+
const bytes = Math.min(size, maxBytes);
|
|
64
|
+
const start = Math.max(0, size - bytes);
|
|
65
|
+
const buffer = Buffer.alloc(bytes);
|
|
66
|
+
if (bytes > 0)
|
|
67
|
+
readSync(fd, buffer, 0, bytes, start);
|
|
68
|
+
let startsAtLineBoundary = start === 0;
|
|
69
|
+
if (start > 0) {
|
|
70
|
+
const previous = Buffer.allocUnsafe(1);
|
|
71
|
+
startsAtLineBoundary = readSync(fd, previous, 0, 1, start - 1) === 1 && previous[0] === 0x0a;
|
|
72
|
+
}
|
|
73
|
+
let text = buffer.toString('utf8');
|
|
74
|
+
if (start > 0) {
|
|
75
|
+
if (!startsAtLineBoundary) {
|
|
76
|
+
const firstNewline = text.indexOf('\n');
|
|
77
|
+
text = firstNewline >= 0 ? text.slice(firstNewline + 1) : '';
|
|
78
|
+
}
|
|
79
|
+
text = redactTruncatedLeadingContinuations(replaceUnsafeControls(redactSensitiveHttpHeaders(text)));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
text = replaceUnsafeControls(redactSensitiveHttpHeaders(text));
|
|
83
|
+
}
|
|
84
|
+
const redacted = redactSecrets(text);
|
|
85
|
+
const allLines = redacted.split(/\r?\n/).filter((line) => line.length > 0);
|
|
86
|
+
const lineTruncated = allLines.length > maxLines;
|
|
87
|
+
return {
|
|
88
|
+
available: true,
|
|
89
|
+
data: {
|
|
90
|
+
lines: allLines.slice(-maxLines).map(safeLine),
|
|
91
|
+
truncated: start > 0 || lineTruncated,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const code = error && typeof error === 'object' ? error.code : undefined;
|
|
97
|
+
return { available: false, error: code === 'ENOENT' ? 'log_not_found' : 'log_unavailable' };
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
if (fd !== undefined)
|
|
101
|
+
closeSync(fd);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -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;
|