@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.
@@ -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,57 @@
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
+ };
19
+ type MaybePromise<T> = T | Promise<T>;
20
+ export interface WorkflowRunSummary {
21
+ runId: string;
22
+ workflowId: string;
23
+ status: string;
24
+ currentStep: string | null;
25
+ createdAt: string;
26
+ updatedAt: string;
27
+ completedAt: string | null;
28
+ }
29
+ export interface WorkflowHistoryEntry {
30
+ sequence: number;
31
+ timestamp: string;
32
+ type: string;
33
+ status?: string | null;
34
+ stepId?: string | null;
35
+ executionId?: string | null;
36
+ gateId?: string | null;
37
+ attempt?: number | null;
38
+ actorId?: string | null;
39
+ errorClass?: string | null;
40
+ }
41
+ /** Operator-safe workflow projection. Filesystem paths and raw durable state remain owned by the composition layer. */
42
+ export interface WorkflowProvider {
43
+ listRuns(): MaybePromise<readonly WorkflowRunSummary[]>;
44
+ getRun(runId: string): MaybePromise<WorkflowRunSummary | null>;
45
+ getHistory(runId: string): MaybePromise<readonly WorkflowHistoryEntry[] | null>;
46
+ }
3
47
  export interface WebUIServerDeps {
4
48
  eventsPath: string;
5
49
  ingestor?: ev.Ingestor;
6
50
  store?: assetstore.AssetStoreProvider;
7
51
  /** 人审队列用的 review ledger(#117 人审门)。缺省由 LocalJsonlProvider store 的 baseDir 推导。 */
8
52
  review?: assetstore.ReviewLedger;
53
+ /** Optional provenance sidecar; inferred for a LocalJsonlProvider when absent. */
54
+ provenance?: assetstore.ProvenanceStore;
9
55
  mailbox?: mb.MailboxStore;
10
56
  now?: () => number;
11
57
  host?: string;
@@ -22,8 +68,16 @@ export interface WebUIServerDeps {
22
68
  valueSummary?: (window: ops.SummaryWindow, events: readonly ev.ReportEvent[]) => ops.ValueSummary;
23
69
  /** Shared core retention report provider. Kept injectable so WebUI never owns filesystem policy or paths. */
24
70
  retentionReport?: () => ev.RetentionReport;
71
+ /** Read-only, already scoped MemoryGraph operator status. Re-read for every request; WebUI only exposes a sanitized allowlist. */
72
+ memoryGraphStatus?: () => MemoryGraphStatus;
25
73
  /** Injectable file/source seam for versioned root-event snapshots. */
26
74
  eventSource?: EventSnapshotSource;
75
+ /** Optional bounded diagnostics providers. Each source degrades independently. */
76
+ personalityDiagnostics?: () => unknown | Promise<unknown>;
77
+ logDiagnostics?: () => unknown | Promise<unknown>;
78
+ githubPrDiagnostics?: () => unknown | Promise<unknown>;
79
+ /** Durable workflow visibility provider. Only safe summaries/history metadata may cross this boundary. */
80
+ workflow?: WorkflowProvider;
27
81
  }
28
82
  /**
29
83
  * WebUI 控台(M7): 可观测 + 保活. node:http + 自包含 HTML, 仅绑 loopback.
@@ -38,6 +92,7 @@ export declare class WebUIServer {
38
92
  private readonly actorId;
39
93
  /** Review ledger backing the human-review queue. Undefined when no LocalJsonlProvider store is available. */
40
94
  private readonly review;
95
+ private readonly provenance;
41
96
  /** Token guarding /api/*; supplied by the browser via Bearer, with ?token= retained for compatibility. */
42
97
  readonly token: string;
43
98
  readonly launchTicket: string;
@@ -50,5 +105,8 @@ export declare class WebUIServer {
50
105
  private handle;
51
106
  private readJson;
52
107
  private json;
108
+ private apiError;
109
+ private methodNotAllowed;
53
110
  private send;
54
- }
111
+ }
112
+ export {};