@lensmcp/core 1.0.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/LICENSE +202 -0
- package/README.md +197 -0
- package/index.d.ts +12 -0
- package/index.d.ts.map +1 -0
- package/index.js +11 -0
- package/lib/common.d.ts +3 -0
- package/lib/common.d.ts.map +1 -0
- package/lib/common.js +1 -0
- package/lib/event-bus.d.ts +32 -0
- package/lib/event-bus.d.ts.map +1 -0
- package/lib/event-bus.js +53 -0
- package/lib/fingerprint.d.ts +21 -0
- package/lib/fingerprint.d.ts.map +1 -0
- package/lib/fingerprint.js +24 -0
- package/lib/graph-from-events.d.ts +24 -0
- package/lib/graph-from-events.d.ts.map +1 -0
- package/lib/graph-from-events.js +141 -0
- package/lib/graph-store.d.ts +48 -0
- package/lib/graph-store.d.ts.map +1 -0
- package/lib/graph-store.js +171 -0
- package/lib/patterns.d.ts +68 -0
- package/lib/patterns.d.ts.map +1 -0
- package/lib/patterns.js +163 -0
- package/lib/reducer-registry.d.ts +20 -0
- package/lib/reducer-registry.d.ts.map +1 -0
- package/lib/reducer-registry.js +28 -0
- package/lib/resource-store.d.ts +31 -0
- package/lib/resource-store.d.ts.map +1 -0
- package/lib/resource-store.js +71 -0
- package/lib/story.d.ts +51 -0
- package/lib/story.d.ts.map +1 -0
- package/lib/story.js +191 -0
- package/lib/thresholds.d.ts +27 -0
- package/lib/thresholds.d.ts.map +1 -0
- package/lib/thresholds.js +23 -0
- package/lib/ulid.d.ts +2 -0
- package/lib/ulid.d.ts.map +1 -0
- package/lib/ulid.js +32 -0
- package/package.json +45 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-store.d.ts","sourceRoot":"","sources":["../../src/lib/resource-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC;AAEpC,MAAM,MAAM,gBAAgB,GAAG,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;AAC5E,MAAM,MAAM,sBAAsB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;AAS7E;;;;GAIG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAkD;IAEvE,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,WAAW;IAWvD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAM/C,IAAI,IAAI,MAAM,EAAE;IAIhB;;;;OAIG;IACG,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB3C,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIzC,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB,GAAG,WAAW;CAWrE"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The resource store. URIs are registered once; reading is cheap; the
|
|
3
|
+
* `markDirty` call bumps the revision counter and notifies subscribers
|
|
4
|
+
* only when the produced JSON actually changes (diff-aware).
|
|
5
|
+
*/
|
|
6
|
+
export class ResourceStore {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.entries = new Map();
|
|
9
|
+
this.subs = new Map();
|
|
10
|
+
}
|
|
11
|
+
register(uri, produce) {
|
|
12
|
+
if (this.entries.has(uri)) {
|
|
13
|
+
throw new Error(`Resource already registered: ${uri}`);
|
|
14
|
+
}
|
|
15
|
+
this.entries.set(uri, { uri, produce, revision: 0 });
|
|
16
|
+
return () => {
|
|
17
|
+
this.entries.delete(uri);
|
|
18
|
+
this.subs.delete(uri);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async read(uri) {
|
|
22
|
+
const entry = this.entries.get(uri);
|
|
23
|
+
if (!entry)
|
|
24
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
25
|
+
return entry.produce();
|
|
26
|
+
}
|
|
27
|
+
list() {
|
|
28
|
+
return [...this.entries.keys()];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Recompute the JSON, bump the revision counter if it differs, and
|
|
32
|
+
* notify subscribers. No-op if the URI is unknown or the JSON didn't
|
|
33
|
+
* change.
|
|
34
|
+
*/
|
|
35
|
+
async markDirty(uri) {
|
|
36
|
+
const entry = this.entries.get(uri);
|
|
37
|
+
if (!entry)
|
|
38
|
+
return;
|
|
39
|
+
const next = await entry.produce();
|
|
40
|
+
const serialised = JSON.stringify(next);
|
|
41
|
+
if (serialised === entry.lastJson)
|
|
42
|
+
return;
|
|
43
|
+
entry.lastJson = serialised;
|
|
44
|
+
entry.revision += 1;
|
|
45
|
+
const handlers = this.subs.get(uri);
|
|
46
|
+
if (handlers) {
|
|
47
|
+
for (const h of handlers) {
|
|
48
|
+
try {
|
|
49
|
+
h(uri, entry.revision);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* swallow handler errors */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
revision(uri) {
|
|
58
|
+
return this.entries.get(uri)?.revision;
|
|
59
|
+
}
|
|
60
|
+
subscribe(uri, handler) {
|
|
61
|
+
let set = this.subs.get(uri);
|
|
62
|
+
if (!set) {
|
|
63
|
+
set = new Set();
|
|
64
|
+
this.subs.set(uri, set);
|
|
65
|
+
}
|
|
66
|
+
set.add(handler);
|
|
67
|
+
return () => {
|
|
68
|
+
set?.delete(handler);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
package/lib/story.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story compiler. Takes a set of `BaseEvent`s belonging to one
|
|
3
|
+
* `flowId`, sorts them by timestamp, and emits a `FlowStory` —
|
|
4
|
+
* a small declarative record of the phases the flow went through
|
|
5
|
+
* (user-action → loading-ui → backend-request → state-update →
|
|
6
|
+
* final-render). The output shape mirrors `02-data-model.md#story`.
|
|
7
|
+
*/
|
|
8
|
+
import type { BaseEvent } from '@lensmcp/protocol-types';
|
|
9
|
+
export type StoryPhaseKind = 'user-action' | 'loading-ui' | 'idle-ui' | 'route-transition' | 'state-update' | 'backend-request' | 'render' | 'final-render' | 'visual-violation';
|
|
10
|
+
export interface StoryPhase {
|
|
11
|
+
kind: StoryPhaseKind;
|
|
12
|
+
at: number;
|
|
13
|
+
/** Human-friendly label used in `story://` summaries. */
|
|
14
|
+
label: string;
|
|
15
|
+
/** Optional refs back to the underlying events. */
|
|
16
|
+
eventIds: string[];
|
|
17
|
+
details?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface FlowStory {
|
|
20
|
+
flowId: string;
|
|
21
|
+
origin: {
|
|
22
|
+
type?: string;
|
|
23
|
+
nodeId?: string;
|
|
24
|
+
at: number;
|
|
25
|
+
};
|
|
26
|
+
phases: StoryPhase[];
|
|
27
|
+
/** Final stable state summary (last render commit + last visual frame). */
|
|
28
|
+
finalState?: {
|
|
29
|
+
page?: string;
|
|
30
|
+
route?: string;
|
|
31
|
+
componentName?: string;
|
|
32
|
+
renderId?: string;
|
|
33
|
+
visualFrameId?: string;
|
|
34
|
+
};
|
|
35
|
+
startedAt: number;
|
|
36
|
+
endedAt: number;
|
|
37
|
+
eventCount: number;
|
|
38
|
+
}
|
|
39
|
+
export interface CompileStoryOptions {
|
|
40
|
+
/** Trim events outside the supplied flow id; defaults to scanning all. */
|
|
41
|
+
flowId?: string;
|
|
42
|
+
}
|
|
43
|
+
/** Group events by `context.flowId`. Events without a flowId are dropped. */
|
|
44
|
+
export declare function groupByFlow(events: readonly BaseEvent[]): Map<string, BaseEvent[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Compile a single flow's events into a `FlowStory`. The caller can
|
|
47
|
+
* either pre-filter events (preferred) or pass everything and supply
|
|
48
|
+
* `options.flowId`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function compileStory(events: readonly BaseEvent[], options?: CompileStoryOptions): FlowStory | undefined;
|
|
51
|
+
//# sourceMappingURL=story.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"story.d.ts","sourceRoot":"","sources":["../../src/lib/story.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD,MAAM,MAAM,cAAc,GACtB,aAAa,GACb,YAAY,GACZ,SAAS,GACT,kBAAkB,GAClB,cAAc,GACd,iBAAiB,GACjB,QAAQ,GACR,cAAc,GACd,kBAAkB,CAAC;AAEvB,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE;QACN,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,EAAE,EAAE,MAAM,CAAC;KACZ,CAAC;IACF,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,2EAA2E;IAC3E,UAAU,CAAC,EAAE;QACX,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAalF;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,SAAS,EAAE,EAC5B,OAAO,GAAE,mBAAwB,GAChC,SAAS,GAAG,SAAS,CA4EvB"}
|
package/lib/story.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/** Group events by `context.flowId`. Events without a flowId are dropped. */
|
|
2
|
+
export function groupByFlow(events) {
|
|
3
|
+
const out = new Map();
|
|
4
|
+
for (const e of events) {
|
|
5
|
+
const flowId = e.context.flowId;
|
|
6
|
+
if (!flowId)
|
|
7
|
+
continue;
|
|
8
|
+
let arr = out.get(flowId);
|
|
9
|
+
if (!arr) {
|
|
10
|
+
arr = [];
|
|
11
|
+
out.set(flowId, arr);
|
|
12
|
+
}
|
|
13
|
+
arr.push(e);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compile a single flow's events into a `FlowStory`. The caller can
|
|
19
|
+
* either pre-filter events (preferred) or pass everything and supply
|
|
20
|
+
* `options.flowId`.
|
|
21
|
+
*/
|
|
22
|
+
export function compileStory(events, options = {}) {
|
|
23
|
+
let pool;
|
|
24
|
+
let flowId;
|
|
25
|
+
if (options.flowId) {
|
|
26
|
+
flowId = options.flowId;
|
|
27
|
+
pool = events.filter((e) => e.context.flowId === flowId);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const first = events.find((e) => e.context.flowId);
|
|
31
|
+
if (!first)
|
|
32
|
+
return undefined;
|
|
33
|
+
flowId = first.context.flowId;
|
|
34
|
+
pool = events.filter((e) => e.context.flowId === flowId);
|
|
35
|
+
}
|
|
36
|
+
if (pool.length === 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
pool.sort((a, b) => a.timestamp - b.timestamp);
|
|
39
|
+
const origin = pool[0];
|
|
40
|
+
const phases = [];
|
|
41
|
+
let finalRender;
|
|
42
|
+
let finalVisualFrame;
|
|
43
|
+
for (const event of pool) {
|
|
44
|
+
const phase = classifyEvent(event);
|
|
45
|
+
if (!phase)
|
|
46
|
+
continue;
|
|
47
|
+
phases.push(phase);
|
|
48
|
+
if (phase.kind === 'render' || phase.kind === 'final-render') {
|
|
49
|
+
const raw = event.raw;
|
|
50
|
+
if (raw?.render) {
|
|
51
|
+
finalRender = {
|
|
52
|
+
id: raw.render.id,
|
|
53
|
+
page: raw.render.page,
|
|
54
|
+
route: raw.render.route,
|
|
55
|
+
componentName: raw.render.componentName,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (event.source === 'visual' && phase.kind === 'visual-violation') {
|
|
60
|
+
const raw = event.raw;
|
|
61
|
+
if (raw?.frame)
|
|
62
|
+
finalVisualFrame = raw.frame.id;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Promote the final `render` phase (if any) to `final-render`.
|
|
66
|
+
for (let i = phases.length - 1; i >= 0; i--) {
|
|
67
|
+
if (phases[i].kind === 'render') {
|
|
68
|
+
phases[i] = { ...phases[i], kind: 'final-render' };
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const startedAt = pool[0].timestamp;
|
|
73
|
+
const endedAt = pool[pool.length - 1].timestamp;
|
|
74
|
+
return {
|
|
75
|
+
flowId,
|
|
76
|
+
origin: {
|
|
77
|
+
type: origin.context.originType,
|
|
78
|
+
nodeId: origin.context.originNodeId,
|
|
79
|
+
at: origin.timestamp,
|
|
80
|
+
},
|
|
81
|
+
phases,
|
|
82
|
+
finalState: finalRender
|
|
83
|
+
? {
|
|
84
|
+
page: finalRender.page,
|
|
85
|
+
route: finalRender.route,
|
|
86
|
+
componentName: finalRender.componentName,
|
|
87
|
+
renderId: finalRender.id,
|
|
88
|
+
visualFrameId: finalVisualFrame,
|
|
89
|
+
}
|
|
90
|
+
: undefined,
|
|
91
|
+
startedAt,
|
|
92
|
+
endedAt,
|
|
93
|
+
eventCount: pool.length,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Classify a single event into a story phase. Returns undefined for
|
|
98
|
+
* events that don't carry phase semantics (we still keep them under
|
|
99
|
+
* `flow://flow/{flowId}.events` but don't surface them in the story).
|
|
100
|
+
*/
|
|
101
|
+
function classifyEvent(event) {
|
|
102
|
+
const raw = (event.raw ?? null);
|
|
103
|
+
const kind = raw?.kind;
|
|
104
|
+
// Flow origin events (synthetic user-action emitter)
|
|
105
|
+
if (kind === 'flow-origin' || (event.source === 'client-runtime' && kind === 'user-action')) {
|
|
106
|
+
const r = event.raw;
|
|
107
|
+
return {
|
|
108
|
+
kind: 'user-action',
|
|
109
|
+
at: event.timestamp,
|
|
110
|
+
label: r?.origin?.label ?? event.title,
|
|
111
|
+
eventIds: [event.id],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// React renders
|
|
115
|
+
if (event.source === 'react' && kind === 'render') {
|
|
116
|
+
const r = event.raw;
|
|
117
|
+
return {
|
|
118
|
+
kind: 'render',
|
|
119
|
+
at: event.timestamp,
|
|
120
|
+
label: `render ${r?.render?.componentName ?? '?'}`,
|
|
121
|
+
eventIds: [event.id],
|
|
122
|
+
details: r?.render
|
|
123
|
+
? { componentName: r.render.componentName, durationMs: r.render.actualDurationMs }
|
|
124
|
+
: undefined,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Slot transitions (loading / idle UI)
|
|
128
|
+
if (event.source === 'react' && kind === 'slot-content-changed') {
|
|
129
|
+
const r = event.raw;
|
|
130
|
+
const to = r?.slot?.toComponent ?? '?';
|
|
131
|
+
const isLoading = /spinner|loader|loading/i.test(to);
|
|
132
|
+
return {
|
|
133
|
+
kind: isLoading ? 'loading-ui' : 'idle-ui',
|
|
134
|
+
at: event.timestamp,
|
|
135
|
+
label: `slot ${r?.slot?.slotLogicalId ?? ''} → ${to}`,
|
|
136
|
+
eventIds: [event.id],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// Route transitions (client-runtime emits)
|
|
140
|
+
if (event.source === 'client-runtime' && kind === 'route') {
|
|
141
|
+
const r = event.raw;
|
|
142
|
+
return {
|
|
143
|
+
kind: 'route-transition',
|
|
144
|
+
at: event.timestamp,
|
|
145
|
+
label: `route ${r?.from ?? ''} → ${r?.url ?? ''}`,
|
|
146
|
+
eventIds: [event.id],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// Valtio state-update
|
|
150
|
+
if (event.source === 'valtio' && kind === 'state-update') {
|
|
151
|
+
const r = event.raw;
|
|
152
|
+
return {
|
|
153
|
+
kind: 'state-update',
|
|
154
|
+
at: event.timestamp,
|
|
155
|
+
label: `${r?.update?.storeId ?? '?'}.${r?.update?.path ?? '?'}`,
|
|
156
|
+
eventIds: [event.id],
|
|
157
|
+
details: r?.update,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// Backend (NestJS) server-request
|
|
161
|
+
if (event.source === 'nestjs' && kind === 'server-request') {
|
|
162
|
+
const r = event.raw;
|
|
163
|
+
return {
|
|
164
|
+
kind: 'backend-request',
|
|
165
|
+
at: event.timestamp,
|
|
166
|
+
label: `${r?.request?.method ?? '?'} ${r?.request?.route ?? '?'} → ${r?.request?.status ?? '?'}`,
|
|
167
|
+
eventIds: [event.id],
|
|
168
|
+
details: r?.request,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// Chrome fetch (browser side) — synthesised by client-runtime.
|
|
172
|
+
if (event.source === 'chrome' && event.category === 'network') {
|
|
173
|
+
return {
|
|
174
|
+
kind: 'backend-request',
|
|
175
|
+
at: event.timestamp,
|
|
176
|
+
label: event.title,
|
|
177
|
+
eventIds: [event.id],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// Visual violations
|
|
181
|
+
if (event.source === 'visual' && kind === 'visual-violation') {
|
|
182
|
+
const r = event.raw;
|
|
183
|
+
return {
|
|
184
|
+
kind: 'visual-violation',
|
|
185
|
+
at: event.timestamp,
|
|
186
|
+
label: `${r?.violation?.ruleType ?? 'rule'} ${r?.violation?.ruleId ?? ''}`,
|
|
187
|
+
eventIds: [event.id],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tunable thresholds that decide when a signal/channel fires. Defaults
|
|
3
|
+
* live here; a workspace overrides any subset via `nx.json#lensmcp.thresholds`,
|
|
4
|
+
* which the server loads at boot and threads into the reducers/detectors.
|
|
5
|
+
*
|
|
6
|
+
* Pure + isomorphic (no node APIs) so the detectors in `patterns.ts` and the
|
|
7
|
+
* reducers can both depend on it.
|
|
8
|
+
*/
|
|
9
|
+
export interface LensmcpThresholds {
|
|
10
|
+
/** DB calls inside one loop before `db-in-loop` fires. Default 5. */
|
|
11
|
+
dbInLoop: number;
|
|
12
|
+
/** Identical query repeats before `n+1-query` fires. Default 3. */
|
|
13
|
+
nPlusOne: number;
|
|
14
|
+
/** Renders of one component (per window) before `render-storm` fires. Default 5. */
|
|
15
|
+
renderStorm: number;
|
|
16
|
+
/** Server route durationMs at/above which it's "slow". Default 500. */
|
|
17
|
+
slowRouteMs: number;
|
|
18
|
+
/** React render durationMs at/above which it's "slow". Default 16. */
|
|
19
|
+
slowRenderMs: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_THRESHOLDS: LensmcpThresholds;
|
|
22
|
+
/**
|
|
23
|
+
* Merge partial overrides over the defaults, ignoring any non-finite /
|
|
24
|
+
* non-numeric values (so a malformed config can't disable detection).
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveThresholds(overrides?: Partial<LensmcpThresholds> | null): LensmcpThresholds;
|
|
27
|
+
//# sourceMappingURL=thresholds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thresholds.d.ts","sourceRoot":"","sources":["../../src/lib/thresholds.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,kBAAkB,EAAE,iBAMhC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,IAAI,GAC5C,iBAAiB,CAWnB"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const DEFAULT_THRESHOLDS = {
|
|
2
|
+
dbInLoop: 5,
|
|
3
|
+
nPlusOne: 3,
|
|
4
|
+
renderStorm: 5,
|
|
5
|
+
slowRouteMs: 500,
|
|
6
|
+
slowRenderMs: 16,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Merge partial overrides over the defaults, ignoring any non-finite /
|
|
10
|
+
* non-numeric values (so a malformed config can't disable detection).
|
|
11
|
+
*/
|
|
12
|
+
export function resolveThresholds(overrides) {
|
|
13
|
+
const out = { ...DEFAULT_THRESHOLDS };
|
|
14
|
+
if (overrides) {
|
|
15
|
+
for (const key of Object.keys(DEFAULT_THRESHOLDS)) {
|
|
16
|
+
const v = overrides[key];
|
|
17
|
+
if (typeof v === 'number' && Number.isFinite(v) && v >= 0) {
|
|
18
|
+
out[key] = v;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
package/lib/ulid.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ulid.d.ts","sourceRoot":"","sources":["../../src/lib/ulid.ts"],"names":[],"mappings":"AAiCA,wBAAgB,IAAI,CAAC,GAAG,GAAE,MAAmB,GAAG,MAAM,CAErD"}
|
package/lib/ulid.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny dependency-free ULID-like ID generator.
|
|
3
|
+
*
|
|
4
|
+
* Real ULIDs are 26 chars Crockford-Base32. Ours are 26 chars of the same
|
|
5
|
+
* alphabet for compatibility, with the 48-bit timestamp prefix and 80
|
|
6
|
+
* bits of `crypto.getRandomValues` randomness. Sortable by creation time.
|
|
7
|
+
*/
|
|
8
|
+
const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
9
|
+
const RANDOM_LEN = 16;
|
|
10
|
+
function encodeTime(now, len) {
|
|
11
|
+
let out = '';
|
|
12
|
+
let n = now;
|
|
13
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
14
|
+
const mod = n % 32;
|
|
15
|
+
out = ALPHABET[mod] + out;
|
|
16
|
+
n = (n - mod) / 32;
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
function encodeRandom(len) {
|
|
21
|
+
const bytes = new Uint8Array(len);
|
|
22
|
+
// Node 24 has global crypto; this is also fine in modern browsers.
|
|
23
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
24
|
+
let out = '';
|
|
25
|
+
for (let i = 0; i < len; i++) {
|
|
26
|
+
out += ALPHABET[(bytes[i] ?? 0) & 31];
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
export function ulid(now = Date.now()) {
|
|
31
|
+
return encodeTime(now, 10) + encodeRandom(RANDOM_LEN);
|
|
32
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lensmcp/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.js",
|
|
6
|
+
"module": "./index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./package.json": "./package.json",
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./index.d.ts",
|
|
12
|
+
"import": "./index.js",
|
|
13
|
+
"default": "./index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@lensmcp/protocol-types": "1.0.0",
|
|
18
|
+
"tslib": "^2.3.0"
|
|
19
|
+
},
|
|
20
|
+
"license": "Apache-2.0",
|
|
21
|
+
"homepage": "https://github.com/kiwiapps-ltd/lensmcp#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/kiwiapps-ltd/lensmcp/issues"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"description": "LensMCP core — event bus, resource store, graph store, and pattern detectors.",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"lensmcp",
|
|
34
|
+
"mcp",
|
|
35
|
+
"observability",
|
|
36
|
+
"ai-agents",
|
|
37
|
+
"claude",
|
|
38
|
+
"core"
|
|
39
|
+
],
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/kiwiapps-ltd/lensmcp.git",
|
|
43
|
+
"directory": "libs/core"
|
|
44
|
+
}
|
|
45
|
+
}
|