margin-ts 0.6.1 → 0.6.2

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,65 @@
1
+ /**
2
+ * Next.js API route adapter for margin.
3
+ *
4
+ * One-liner health tracking for any API route:
5
+ *
6
+ * // pages/api/users.ts (Pages Router)
7
+ * import { withMargin } from 'margin-ts/adapters/nextjs';
8
+ * export default withMargin(handler);
9
+ *
10
+ * // app/api/users/route.ts (App Router)
11
+ * import { withMarginApp } from 'margin-ts/adapters/nextjs';
12
+ * export const GET = withMarginApp(handler);
13
+ *
14
+ * // Health endpoint
15
+ * // pages/api/margin/health.ts
16
+ * import { marginHealthHandler } from 'margin-ts/adapters/nextjs';
17
+ * export default marginHealthHandler();
18
+ *
19
+ * Tracks latency, error rate, and request count per route.
20
+ * Zero dependencies beyond margin-ts core.
21
+ */
22
+ import { Thresholds } from '../index.js';
23
+ export interface NextjsThresholds {
24
+ p50Latency: Thresholds;
25
+ p99Latency: Thresholds;
26
+ errorRate: Thresholds;
27
+ }
28
+ export declare const DEFAULT_NEXTJS_THRESHOLDS: NextjsThresholds;
29
+ export interface WithMarginOptions {
30
+ route?: string;
31
+ thresholds?: NextjsThresholds;
32
+ }
33
+ /**
34
+ * Wrap a Pages Router API handler with margin health tracking.
35
+ *
36
+ * export default withMargin(handler);
37
+ * export default withMargin(handler, { route: '/api/users' });
38
+ */
39
+ export declare function withMargin(handler: (req: any, res: any) => any, options?: WithMarginOptions): (req: any, res: any) => any;
40
+ /**
41
+ * Wrap an App Router handler with margin health tracking.
42
+ *
43
+ * export const GET = withMarginApp(handler);
44
+ * export const POST = withMarginApp(handler, { route: '/api/users' });
45
+ */
46
+ export declare function withMarginApp(handler: (req: any) => any, options?: WithMarginOptions): (req: any) => any;
47
+ /**
48
+ * Pages Router: health endpoint handler.
49
+ *
50
+ * // pages/api/margin/health.ts
51
+ * import { marginHealthHandler } from 'margin-ts/adapters/nextjs';
52
+ * export default marginHealthHandler();
53
+ */
54
+ export declare function marginHealthHandler(thresholds?: NextjsThresholds): (req: any, res: any) => void;
55
+ /**
56
+ * App Router: health endpoint handler.
57
+ *
58
+ * // app/api/margin/health/route.ts
59
+ * import { marginHealthAppHandler } from 'margin-ts/adapters/nextjs';
60
+ * export const GET = marginHealthAppHandler();
61
+ */
62
+ export declare function marginHealthAppHandler(thresholds?: NextjsThresholds): (req: any) => any;
63
+ /** Clear all tracked routes (for testing). */
64
+ export declare function resetRoutes(): void;
65
+ //# sourceMappingURL=nextjs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nextjs.d.ts","sourceRoot":"","sources":["../../src/adapters/nextjs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAGL,UAAU,EAOX,MAAM,aAAa,CAAC;AAwCrB,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,UAAU,CAAC;IACvB,UAAU,EAAE,UAAU,CAAC;IACvB,SAAS,EAAE,UAAU,CAAC;CACvB;AAED,eAAO,MAAM,yBAAyB,EAAE,gBAIvC,CAAC;AA8CF,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,GAAG,EACpC,OAAO,GAAE,iBAAsB,GAC9B,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,GAAG,CAiB7B;AAMD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,EAC1B,OAAO,GAAE,iBAAsB,GAC9B,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,CAiBnB;AAMD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,CAAC,EAAE,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAkC/F;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,CAAC,EAAE,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,CAqCvF;AAED,8CAA8C;AAC9C,wBAAgB,WAAW,IAAI,IAAI,CAElC"}
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ /**
3
+ * Next.js API route adapter for margin.
4
+ *
5
+ * One-liner health tracking for any API route:
6
+ *
7
+ * // pages/api/users.ts (Pages Router)
8
+ * import { withMargin } from 'margin-ts/adapters/nextjs';
9
+ * export default withMargin(handler);
10
+ *
11
+ * // app/api/users/route.ts (App Router)
12
+ * import { withMarginApp } from 'margin-ts/adapters/nextjs';
13
+ * export const GET = withMarginApp(handler);
14
+ *
15
+ * // Health endpoint
16
+ * // pages/api/margin/health.ts
17
+ * import { marginHealthHandler } from 'margin-ts/adapters/nextjs';
18
+ * export default marginHealthHandler();
19
+ *
20
+ * Tracks latency, error rate, and request count per route.
21
+ * Zero dependencies beyond margin-ts core.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.DEFAULT_NEXTJS_THRESHOLDS = void 0;
25
+ exports.withMargin = withMargin;
26
+ exports.withMarginApp = withMarginApp;
27
+ exports.marginHealthHandler = marginHealthHandler;
28
+ exports.marginHealthAppHandler = marginHealthAppHandler;
29
+ exports.resetRoutes = resetRoutes;
30
+ const index_js_1 = require("../index.js");
31
+ const _routes = new Map();
32
+ const _MAX_WINDOW = 200;
33
+ function getOrCreateRoute(route) {
34
+ if (!_routes.has(route)) {
35
+ _routes.set(route, {
36
+ totalRequests: 0,
37
+ totalErrors: 0,
38
+ latencies: [],
39
+ maxWindow: _MAX_WINDOW,
40
+ });
41
+ }
42
+ return _routes.get(route);
43
+ }
44
+ function record(route, latencyMs, isError) {
45
+ const stats = getOrCreateRoute(route);
46
+ stats.totalRequests++;
47
+ if (isError)
48
+ stats.totalErrors++;
49
+ stats.latencies.push(latencyMs);
50
+ if (stats.latencies.length > stats.maxWindow)
51
+ stats.latencies.shift();
52
+ }
53
+ exports.DEFAULT_NEXTJS_THRESHOLDS = {
54
+ p50Latency: (0, index_js_1.createThresholds)(100, 500, false),
55
+ p99Latency: (0, index_js_1.createThresholds)(500, 2000, false),
56
+ errorRate: (0, index_js_1.createThresholds)(0.01, 0.10, false),
57
+ };
58
+ // -----------------------------------------------------------------------
59
+ // Classification
60
+ // -----------------------------------------------------------------------
61
+ function percentile(sorted, p) {
62
+ if (sorted.length === 0)
63
+ return 0;
64
+ const idx = Math.ceil(sorted.length * p) - 1;
65
+ return sorted[Math.max(0, Math.min(idx, sorted.length - 1))];
66
+ }
67
+ function classifyRoute(route, stats, thresholds) {
68
+ if (stats.latencies.length < 3)
69
+ return null;
70
+ const sorted = [...stats.latencies].sort((a, b) => a - b);
71
+ const p50 = percentile(sorted, 0.5);
72
+ const p99 = percentile(sorted, 0.99);
73
+ const errorRate = stats.totalRequests > 0 ? stats.totalErrors / stats.totalRequests : 0;
74
+ const now = new Date();
75
+ const observations = [
76
+ {
77
+ name: `${route}:p50`, health: (0, index_js_1.classify)(p50, index_js_1.Confidence.HIGH, thresholds.p50Latency),
78
+ value: p50, baseline: thresholds.p50Latency.intact, confidence: index_js_1.Confidence.HIGH,
79
+ higherIsBetter: false, provenance: [], measuredAt: now,
80
+ },
81
+ {
82
+ name: `${route}:p99`, health: (0, index_js_1.classify)(p99, index_js_1.Confidence.HIGH, thresholds.p99Latency),
83
+ value: p99, baseline: thresholds.p99Latency.intact, confidence: index_js_1.Confidence.HIGH,
84
+ higherIsBetter: false, provenance: [], measuredAt: now,
85
+ },
86
+ {
87
+ name: `${route}:errors`, health: (0, index_js_1.classify)(errorRate, index_js_1.Confidence.HIGH, thresholds.errorRate),
88
+ value: errorRate, baseline: 0.001, confidence: index_js_1.Confidence.HIGH,
89
+ higherIsBetter: false, provenance: [], measuredAt: now,
90
+ },
91
+ ];
92
+ return (0, index_js_1.createExpression)(observations, [], route);
93
+ }
94
+ /**
95
+ * Wrap a Pages Router API handler with margin health tracking.
96
+ *
97
+ * export default withMargin(handler);
98
+ * export default withMargin(handler, { route: '/api/users' });
99
+ */
100
+ function withMargin(handler, options = {}) {
101
+ return async (req, res) => {
102
+ const route = options.route || req.url || '/api/unknown';
103
+ const start = Date.now();
104
+ try {
105
+ const result = await handler(req, res);
106
+ const latency = Date.now() - start;
107
+ const status = res.statusCode || 200;
108
+ record(route, latency, status >= 500);
109
+ return result;
110
+ }
111
+ catch (err) {
112
+ const latency = Date.now() - start;
113
+ record(route, latency, true);
114
+ throw err;
115
+ }
116
+ };
117
+ }
118
+ // -----------------------------------------------------------------------
119
+ // App Router wrapper
120
+ // -----------------------------------------------------------------------
121
+ /**
122
+ * Wrap an App Router handler with margin health tracking.
123
+ *
124
+ * export const GET = withMarginApp(handler);
125
+ * export const POST = withMarginApp(handler, { route: '/api/users' });
126
+ */
127
+ function withMarginApp(handler, options = {}) {
128
+ return async (req) => {
129
+ const route = options.route || new URL(req.url || '/', 'http://localhost').pathname;
130
+ const start = Date.now();
131
+ try {
132
+ const response = await handler(req);
133
+ const latency = Date.now() - start;
134
+ const status = response?.status || 200;
135
+ record(route, latency, status >= 500);
136
+ return response;
137
+ }
138
+ catch (err) {
139
+ const latency = Date.now() - start;
140
+ record(route, latency, true);
141
+ throw err;
142
+ }
143
+ };
144
+ }
145
+ // -----------------------------------------------------------------------
146
+ // Health endpoint
147
+ // -----------------------------------------------------------------------
148
+ /**
149
+ * Pages Router: health endpoint handler.
150
+ *
151
+ * // pages/api/margin/health.ts
152
+ * import { marginHealthHandler } from 'margin-ts/adapters/nextjs';
153
+ * export default marginHealthHandler();
154
+ */
155
+ function marginHealthHandler(thresholds) {
156
+ const t = thresholds || exports.DEFAULT_NEXTJS_THRESHOLDS;
157
+ return (_req, res) => {
158
+ const routeHealth = {};
159
+ let worstOverall = index_js_1.Health.INTACT;
160
+ const SEVERITY = {
161
+ [index_js_1.Health.INTACT]: 0, [index_js_1.Health.RECOVERING]: 1, [index_js_1.Health.DEGRADED]: 2,
162
+ [index_js_1.Health.ABLATED]: 3, [index_js_1.Health.OOD]: 4,
163
+ };
164
+ for (const [route, stats] of _routes) {
165
+ const expr = classifyRoute(route, stats, t);
166
+ if (expr) {
167
+ routeHealth[route] = {
168
+ expression: (0, index_js_1.expressionToString)(expr),
169
+ ...(0, index_js_1.expressionToDict)(expr),
170
+ requests: stats.totalRequests,
171
+ errors: stats.totalErrors,
172
+ };
173
+ for (const obs of expr.observations) {
174
+ if (SEVERITY[obs.health] > SEVERITY[worstOverall]) {
175
+ worstOverall = obs.health;
176
+ }
177
+ }
178
+ }
179
+ }
180
+ res.status(200).json({
181
+ status: worstOverall,
182
+ routes: routeHealth,
183
+ totalRoutes: _routes.size,
184
+ });
185
+ };
186
+ }
187
+ /**
188
+ * App Router: health endpoint handler.
189
+ *
190
+ * // app/api/margin/health/route.ts
191
+ * import { marginHealthAppHandler } from 'margin-ts/adapters/nextjs';
192
+ * export const GET = marginHealthAppHandler();
193
+ */
194
+ function marginHealthAppHandler(thresholds) {
195
+ const t = thresholds || exports.DEFAULT_NEXTJS_THRESHOLDS;
196
+ return (_req) => {
197
+ const routeHealth = {};
198
+ let worstOverall = index_js_1.Health.INTACT;
199
+ const SEVERITY = {
200
+ [index_js_1.Health.INTACT]: 0, [index_js_1.Health.RECOVERING]: 1, [index_js_1.Health.DEGRADED]: 2,
201
+ [index_js_1.Health.ABLATED]: 3, [index_js_1.Health.OOD]: 4,
202
+ };
203
+ for (const [route, stats] of _routes) {
204
+ const expr = classifyRoute(route, stats, t);
205
+ if (expr) {
206
+ routeHealth[route] = {
207
+ expression: (0, index_js_1.expressionToString)(expr),
208
+ ...(0, index_js_1.expressionToDict)(expr),
209
+ requests: stats.totalRequests,
210
+ errors: stats.totalErrors,
211
+ };
212
+ for (const obs of expr.observations) {
213
+ if (SEVERITY[obs.health] > SEVERITY[worstOverall]) {
214
+ worstOverall = obs.health;
215
+ }
216
+ }
217
+ }
218
+ }
219
+ return new Response(JSON.stringify({
220
+ status: worstOverall,
221
+ routes: routeHealth,
222
+ totalRoutes: _routes.size,
223
+ }), {
224
+ status: 200,
225
+ headers: { 'Content-Type': 'application/json' },
226
+ });
227
+ };
228
+ }
229
+ /** Clear all tracked routes (for testing). */
230
+ function resetRoutes() {
231
+ _routes.clear();
232
+ }
233
+ //# sourceMappingURL=nextjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nextjs.js","sourceRoot":"","sources":["../../src/adapters/nextjs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Streaming health updates via Server-Sent Events (SSE).
3
+ *
4
+ * Push health/drift/anomaly updates to browser dashboards in real-time.
5
+ *
6
+ * Express:
7
+ * app.get('/margin/stream', marginSSE(monitor));
8
+ *
9
+ * Next.js Pages Router:
10
+ * export default marginSSEHandler(monitor);
11
+ *
12
+ * Browser client:
13
+ * const source = new EventSource('/margin/stream');
14
+ * source.onmessage = (e) => {
15
+ * const health = JSON.parse(e.data);
16
+ * updateDashboard(health);
17
+ * };
18
+ *
19
+ * Zero dependencies. Works with any framework that supports
20
+ * writable response streams (Express, Fastify, Next.js, raw Node http).
21
+ */
22
+ import { Expression, Parser, DriftClassification, AnomalyClassification } from '../index.js';
23
+ export declare class StreamingMonitor {
24
+ private parser;
25
+ private windows;
26
+ private _expression;
27
+ private _step;
28
+ private maxWindow;
29
+ private minAnomalyRef;
30
+ constructor(parser: Parser, options?: {
31
+ window?: number;
32
+ minAnomalyRef?: number;
33
+ });
34
+ get expression(): Expression | null;
35
+ get step(): number;
36
+ update(values: Record<string, number>, now?: Date): Expression;
37
+ drift(component: string): DriftClassification | null;
38
+ anomaly(component: string): AnomalyClassification | null;
39
+ status(): Record<string, unknown>;
40
+ reset(): void;
41
+ }
42
+ /**
43
+ * Broadcast a status update to all connected SSE clients.
44
+ * Call this after each monitor.update().
45
+ */
46
+ export declare function broadcast(monitor: StreamingMonitor): void;
47
+ /**
48
+ * Express/Fastify route handler for SSE streaming.
49
+ *
50
+ * app.get('/margin/stream', marginSSE(monitor));
51
+ *
52
+ * Each connected browser receives real-time health updates.
53
+ * Call broadcast(monitor) after each monitor.update() to push.
54
+ */
55
+ export declare function marginSSE(monitor: StreamingMonitor): (req: any, res: any) => void;
56
+ /**
57
+ * Next.js Pages Router SSE handler.
58
+ *
59
+ * // pages/api/margin/stream.ts
60
+ * export default marginSSEHandler(monitor);
61
+ */
62
+ export declare function marginSSEHandler(monitor: StreamingMonitor): (req: any, res: any) => void;
63
+ /**
64
+ * Create a polling endpoint (for environments that don't support SSE).
65
+ * Returns the current status as JSON on each request.
66
+ *
67
+ * app.get('/margin/poll', marginPoll(monitor));
68
+ */
69
+ export declare function marginPoll(monitor: StreamingMonitor): (req: any, res: any) => void;
70
+ /** Number of connected SSE clients. */
71
+ export declare function connectedClients(): number;
72
+ /** Disconnect all SSE clients (for testing). */
73
+ export declare function resetClients(): void;
74
+ //# sourceMappingURL=stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/adapters/stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAIL,UAAU,EACV,MAAM,EAKN,mBAAmB,EAInB,qBAAqB,EAEtB,MAAM,aAAa,CAAC;AAYrB,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAA2C;IAC1D,OAAO,CAAC,WAAW,CAA2B;IAC9C,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,aAAa,CAAS;gBAElB,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAO;IAMrF,IAAI,UAAU,IAAI,UAAU,GAAG,IAAI,CAA6B;IAChE,IAAI,IAAI,IAAI,MAAM,CAAuB;IAEzC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,UAAU;IAwB9D,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAMpD,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI;IAQxD,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAoBjC,KAAK,IAAI,IAAI;CAKd;AAeD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAWzD;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAoBjF;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAExF;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAIlF;AAED,uCAAuC;AACvC,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,gDAAgD;AAChD,wBAAgB,YAAY,IAAI,IAAI,CAMnC"}
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ /**
3
+ * Streaming health updates via Server-Sent Events (SSE).
4
+ *
5
+ * Push health/drift/anomaly updates to browser dashboards in real-time.
6
+ *
7
+ * Express:
8
+ * app.get('/margin/stream', marginSSE(monitor));
9
+ *
10
+ * Next.js Pages Router:
11
+ * export default marginSSEHandler(monitor);
12
+ *
13
+ * Browser client:
14
+ * const source = new EventSource('/margin/stream');
15
+ * source.onmessage = (e) => {
16
+ * const health = JSON.parse(e.data);
17
+ * updateDashboard(health);
18
+ * };
19
+ *
20
+ * Zero dependencies. Works with any framework that supports
21
+ * writable response streams (Express, Fastify, Next.js, raw Node http).
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.StreamingMonitor = void 0;
25
+ exports.broadcast = broadcast;
26
+ exports.marginSSE = marginSSE;
27
+ exports.marginSSEHandler = marginSSEHandler;
28
+ exports.marginPoll = marginPoll;
29
+ exports.connectedClients = connectedClients;
30
+ exports.resetClients = resetClients;
31
+ const index_js_1 = require("../index.js");
32
+ class StreamingMonitor {
33
+ parser;
34
+ windows = new Map();
35
+ _expression = null;
36
+ _step = 0;
37
+ maxWindow;
38
+ minAnomalyRef;
39
+ constructor(parser, options = {}) {
40
+ this.parser = parser;
41
+ this.maxWindow = options.window ?? 100;
42
+ this.minAnomalyRef = options.minAnomalyRef ?? 10;
43
+ }
44
+ get expression() { return this._expression; }
45
+ get step() { return this._step; }
46
+ update(values, now) {
47
+ now = now ?? new Date();
48
+ this._expression = this.parser.parse(values, { step: this._step });
49
+ this._step++;
50
+ // Update per-component windows
51
+ for (const obs of this._expression.observations) {
52
+ if (!this.windows.has(obs.name)) {
53
+ this.windows.set(obs.name, { values: [], observations: [], maxWindow: this.maxWindow });
54
+ }
55
+ const w = this.windows.get(obs.name);
56
+ w.values.push(obs.value);
57
+ // Stamp the observation with time for drift
58
+ const stamped = { ...obs, measuredAt: now };
59
+ w.observations.push(stamped);
60
+ if (w.values.length > this.maxWindow) {
61
+ w.values.shift();
62
+ w.observations.shift();
63
+ }
64
+ }
65
+ return this._expression;
66
+ }
67
+ drift(component) {
68
+ const w = this.windows.get(component);
69
+ if (!w || w.observations.length < 3)
70
+ return null;
71
+ return (0, index_js_1.classifyDrift)(w.observations);
72
+ }
73
+ anomaly(component) {
74
+ const w = this.windows.get(component);
75
+ if (!w || w.values.length < this.minAnomalyRef + 1)
76
+ return null;
77
+ const ref = w.values.slice(0, -1);
78
+ const current = w.values[w.values.length - 1];
79
+ return (0, index_js_1.classifyAnomaly)(current, ref, { component });
80
+ }
81
+ status() {
82
+ const drift = {};
83
+ const anomaly = {};
84
+ for (const [name] of this.windows) {
85
+ const dc = this.drift(name);
86
+ if (dc)
87
+ drift[name] = { state: dc.state, direction: dc.direction, rate: dc.rate };
88
+ const ac = this.anomaly(name);
89
+ if (ac)
90
+ anomaly[name] = { state: ac.state, zScore: ac.zScore };
91
+ }
92
+ return {
93
+ step: this._step,
94
+ expression: this._expression ? (0, index_js_1.expressionToString)(this._expression) : null,
95
+ health: this._expression ? (0, index_js_1.expressionToDict)(this._expression) : null,
96
+ drift,
97
+ anomaly,
98
+ };
99
+ }
100
+ reset() {
101
+ this.windows.clear();
102
+ this._expression = null;
103
+ this._step = 0;
104
+ }
105
+ }
106
+ exports.StreamingMonitor = StreamingMonitor;
107
+ let _clients = [];
108
+ let _clientIdCounter = 0;
109
+ /**
110
+ * Broadcast a status update to all connected SSE clients.
111
+ * Call this after each monitor.update().
112
+ */
113
+ function broadcast(monitor) {
114
+ const data = JSON.stringify(monitor.status());
115
+ const message = `data: ${data}\n\n`;
116
+ _clients = _clients.filter(client => {
117
+ try {
118
+ client.res.write(message);
119
+ return true;
120
+ }
121
+ catch {
122
+ return false; // client disconnected
123
+ }
124
+ });
125
+ }
126
+ /**
127
+ * Express/Fastify route handler for SSE streaming.
128
+ *
129
+ * app.get('/margin/stream', marginSSE(monitor));
130
+ *
131
+ * Each connected browser receives real-time health updates.
132
+ * Call broadcast(monitor) after each monitor.update() to push.
133
+ */
134
+ function marginSSE(monitor) {
135
+ return (req, res) => {
136
+ res.writeHead(200, {
137
+ 'Content-Type': 'text/event-stream',
138
+ 'Cache-Control': 'no-cache',
139
+ 'Connection': 'keep-alive',
140
+ 'Access-Control-Allow-Origin': '*',
141
+ });
142
+ // Send current state immediately
143
+ const initial = JSON.stringify(monitor.status());
144
+ res.write(`data: ${initial}\n\n`);
145
+ const client = { id: String(++_clientIdCounter), res };
146
+ _clients.push(client);
147
+ req.on('close', () => {
148
+ _clients = _clients.filter(c => c.id !== client.id);
149
+ });
150
+ };
151
+ }
152
+ /**
153
+ * Next.js Pages Router SSE handler.
154
+ *
155
+ * // pages/api/margin/stream.ts
156
+ * export default marginSSEHandler(monitor);
157
+ */
158
+ function marginSSEHandler(monitor) {
159
+ return marginSSE(monitor);
160
+ }
161
+ /**
162
+ * Create a polling endpoint (for environments that don't support SSE).
163
+ * Returns the current status as JSON on each request.
164
+ *
165
+ * app.get('/margin/poll', marginPoll(monitor));
166
+ */
167
+ function marginPoll(monitor) {
168
+ return (_req, res) => {
169
+ res.json(monitor.status());
170
+ };
171
+ }
172
+ /** Number of connected SSE clients. */
173
+ function connectedClients() {
174
+ return _clients.length;
175
+ }
176
+ /** Disconnect all SSE clients (for testing). */
177
+ function resetClients() {
178
+ for (const client of _clients) {
179
+ try {
180
+ client.res.end();
181
+ }
182
+ catch { }
183
+ }
184
+ _clients = [];
185
+ _clientIdCounter = 0;
186
+ }
187
+ //# sourceMappingURL=stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/adapters/stream.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -13,4 +13,6 @@ export { DriftState, DriftDirection, DriftClassification, classifyDrift, } from
13
13
  export { AnomalyState, ANOMALY_SEVERITY, AnomalyClassification, classifyAnomaly, } from './anomaly.js';
14
14
  export { marginMiddleware, marginHealthRoute, DEFAULT_THRESHOLDS as EXPRESS_THRESHOLDS, type MarginMiddlewareOptions, type MarginState, type EndpointThresholds, } from './adapters/express.js';
15
15
  export { classifySuite, suiteHealthString, DEFAULT_SUITE_THRESHOLDS, type SuiteResults, type SuiteThresholds, } from './adapters/vitest.js';
16
+ export { withMargin, withMarginApp, marginHealthHandler, marginHealthAppHandler, resetRoutes, DEFAULT_NEXTJS_THRESHOLDS, type WithMarginOptions, type NextjsThresholds, } from './adapters/nextjs.js';
17
+ export { StreamingMonitor, broadcast, marginSSE, marginSSEHandler, marginPoll, connectedClients, resetClients, } from './adapters/stream.js';
16
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEzG,OAAO,EACL,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,EAC9C,QAAQ,EAAE,SAAS,EAAE,QAAQ,GAC9B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EACvC,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,EAC3E,kBAAkB,EAClB,gBAAgB,EAAE,QAAQ,EAAE,QAAQ,EAAE,kBAAkB,EAAE,gBAAgB,EAC1E,MAAM,GACP,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,UAAU,EAAE,cAAc,EAAE,mBAAmB,EAC/C,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,YAAY,EAAE,gBAAgB,EAAE,qBAAqB,EACrD,eAAe,GAChB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,gBAAgB,EAAE,iBAAiB,EACnC,kBAAkB,IAAI,kBAAkB,EACxC,KAAK,uBAAuB,EAAE,KAAK,WAAW,EAAE,KAAK,kBAAkB,GACxE,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,aAAa,EAAE,iBAAiB,EAChC,wBAAwB,EACxB,KAAK,YAAY,EAAE,KAAK,eAAe,GACxC,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEzG,OAAO,EACL,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,EAC9C,QAAQ,EAAE,SAAS,EAAE,QAAQ,GAC9B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EACvC,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,EAC3E,kBAAkB,EAClB,gBAAgB,EAAE,QAAQ,EAAE,QAAQ,EAAE,kBAAkB,EAAE,gBAAgB,EAC1E,MAAM,GACP,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,UAAU,EAAE,cAAc,EAAE,mBAAmB,EAC/C,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,YAAY,EAAE,gBAAgB,EAAE,qBAAqB,EACrD,eAAe,GAChB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,gBAAgB,EAAE,iBAAiB,EACnC,kBAAkB,IAAI,kBAAkB,EACxC,KAAK,uBAAuB,EAAE,KAAK,WAAW,EAAE,KAAK,kBAAkB,GACxE,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,aAAa,EAAE,iBAAiB,EAChC,wBAAwB,EACxB,KAAK,YAAY,EAAE,KAAK,eAAe,GACxC,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,UAAU,EAAE,aAAa,EACzB,mBAAmB,EAAE,sBAAsB,EAC3C,WAAW,EACX,yBAAyB,EACzB,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,GAC9C,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,gBAAgB,EAChB,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,UAAU,EAClD,gBAAgB,EAAE,YAAY,GAC/B,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * Copyright (c) 2026 Cope Labs LLC. MIT License.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.DEFAULT_SUITE_THRESHOLDS = exports.suiteHealthString = exports.classifySuite = exports.EXPRESS_THRESHOLDS = exports.marginHealthRoute = exports.marginMiddleware = exports.classifyAnomaly = exports.ANOMALY_SEVERITY = exports.AnomalyState = exports.classifyDrift = exports.DriftDirection = exports.DriftState = exports.Parser = exports.expressionToDict = exports.expressionToString = exports.degraded = exports.healthOf = exports.createExpression = exports.correctionIsActive = exports.observationFromDict = exports.observationToDict = exports.observationToAtom = exports.observationSigma = exports.Op = exports.classify = exports.isAblated = exports.isIntact = exports.createThresholds = exports.SEVERITY = exports.Health = exports.minConfidence = exports.confidenceLt = exports.confidenceGte = exports.confidenceRank = exports.Confidence = void 0;
11
+ exports.resetClients = exports.connectedClients = exports.marginPoll = exports.marginSSEHandler = exports.marginSSE = exports.broadcast = exports.StreamingMonitor = exports.DEFAULT_NEXTJS_THRESHOLDS = exports.resetRoutes = exports.marginHealthAppHandler = exports.marginHealthHandler = exports.withMarginApp = exports.withMargin = exports.DEFAULT_SUITE_THRESHOLDS = exports.suiteHealthString = exports.classifySuite = exports.EXPRESS_THRESHOLDS = exports.marginHealthRoute = exports.marginMiddleware = exports.classifyAnomaly = exports.ANOMALY_SEVERITY = exports.AnomalyState = exports.classifyDrift = exports.DriftDirection = exports.DriftState = exports.Parser = exports.expressionToDict = exports.expressionToString = exports.degraded = exports.healthOf = exports.createExpression = exports.correctionIsActive = exports.observationFromDict = exports.observationToDict = exports.observationToAtom = exports.observationSigma = exports.Op = exports.classify = exports.isAblated = exports.isIntact = exports.createThresholds = exports.SEVERITY = exports.Health = exports.minConfidence = exports.confidenceLt = exports.confidenceGte = exports.confidenceRank = exports.Confidence = void 0;
12
12
  var confidence_js_1 = require("./confidence.js");
13
13
  Object.defineProperty(exports, "Confidence", { enumerable: true, get: function () { return confidence_js_1.Confidence; } });
14
14
  Object.defineProperty(exports, "confidenceRank", { enumerable: true, get: function () { return confidence_js_1.confidenceRank; } });
@@ -52,4 +52,19 @@ var vitest_js_1 = require("./adapters/vitest.js");
52
52
  Object.defineProperty(exports, "classifySuite", { enumerable: true, get: function () { return vitest_js_1.classifySuite; } });
53
53
  Object.defineProperty(exports, "suiteHealthString", { enumerable: true, get: function () { return vitest_js_1.suiteHealthString; } });
54
54
  Object.defineProperty(exports, "DEFAULT_SUITE_THRESHOLDS", { enumerable: true, get: function () { return vitest_js_1.DEFAULT_SUITE_THRESHOLDS; } });
55
+ var nextjs_js_1 = require("./adapters/nextjs.js");
56
+ Object.defineProperty(exports, "withMargin", { enumerable: true, get: function () { return nextjs_js_1.withMargin; } });
57
+ Object.defineProperty(exports, "withMarginApp", { enumerable: true, get: function () { return nextjs_js_1.withMarginApp; } });
58
+ Object.defineProperty(exports, "marginHealthHandler", { enumerable: true, get: function () { return nextjs_js_1.marginHealthHandler; } });
59
+ Object.defineProperty(exports, "marginHealthAppHandler", { enumerable: true, get: function () { return nextjs_js_1.marginHealthAppHandler; } });
60
+ Object.defineProperty(exports, "resetRoutes", { enumerable: true, get: function () { return nextjs_js_1.resetRoutes; } });
61
+ Object.defineProperty(exports, "DEFAULT_NEXTJS_THRESHOLDS", { enumerable: true, get: function () { return nextjs_js_1.DEFAULT_NEXTJS_THRESHOLDS; } });
62
+ var stream_js_1 = require("./adapters/stream.js");
63
+ Object.defineProperty(exports, "StreamingMonitor", { enumerable: true, get: function () { return stream_js_1.StreamingMonitor; } });
64
+ Object.defineProperty(exports, "broadcast", { enumerable: true, get: function () { return stream_js_1.broadcast; } });
65
+ Object.defineProperty(exports, "marginSSE", { enumerable: true, get: function () { return stream_js_1.marginSSE; } });
66
+ Object.defineProperty(exports, "marginSSEHandler", { enumerable: true, get: function () { return stream_js_1.marginSSEHandler; } });
67
+ Object.defineProperty(exports, "marginPoll", { enumerable: true, get: function () { return stream_js_1.marginPoll; } });
68
+ Object.defineProperty(exports, "connectedClients", { enumerable: true, get: function () { return stream_js_1.connectedClients; } });
69
+ Object.defineProperty(exports, "resetClients", { enumerable: true, get: function () { return stream_js_1.resetClients; } });
55
70
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,iDAAyG;AAAhG,2GAAA,UAAU,OAAA;AAAE,+GAAA,cAAc,OAAA;AAAE,8GAAA,aAAa,OAAA;AAAE,6GAAA,YAAY,OAAA;AAAE,8GAAA,aAAa,OAAA;AAE/E,yCAGqB;AAFnB,mGAAA,MAAM,OAAA;AAAE,qGAAA,QAAQ,OAAA;AAAc,6GAAA,gBAAgB,OAAA;AAC9C,qGAAA,QAAQ,OAAA;AAAE,sGAAA,SAAS,OAAA;AAAE,qGAAA,QAAQ,OAAA;AAG/B,mDAM0B;AALxB,oGAAA,EAAE,OAAA;AACF,kHAAA,gBAAgB,OAAA;AAAE,mHAAA,iBAAiB,OAAA;AAAE,mHAAA,iBAAiB,OAAA;AAAE,qHAAA,mBAAmB,OAAA;AAC3E,oHAAA,kBAAkB,OAAA;AAClB,kHAAA,gBAAgB,OAAA;AAAE,0GAAA,QAAQ,OAAA;AAAE,0GAAA,QAAQ,OAAA;AAAE,oHAAA,kBAAkB,OAAA;AAAE,kHAAA,gBAAgB,OAAA;AAC1E,wGAAA,MAAM,OAAA;AAGR,uCAGoB;AAFlB,sGAAA,UAAU,OAAA;AAAE,0GAAA,cAAc,OAAA;AAC1B,yGAAA,aAAa,OAAA;AAGf,2CAGsB;AAFpB,0GAAA,YAAY,OAAA;AAAE,8GAAA,gBAAgB,OAAA;AAC9B,6GAAA,eAAe,OAAA;AAGjB,WAAW;AACX,oDAI+B;AAH7B,8GAAA,gBAAgB,OAAA;AAAE,+GAAA,iBAAiB,OAAA;AACnC,gHAAA,kBAAkB,OAAsB;AAI1C,kDAI8B;AAH5B,0GAAA,aAAa,OAAA;AAAE,8GAAA,iBAAiB,OAAA;AAChC,qHAAA,wBAAwB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,iDAAyG;AAAhG,2GAAA,UAAU,OAAA;AAAE,+GAAA,cAAc,OAAA;AAAE,8GAAA,aAAa,OAAA;AAAE,6GAAA,YAAY,OAAA;AAAE,8GAAA,aAAa,OAAA;AAE/E,yCAGqB;AAFnB,mGAAA,MAAM,OAAA;AAAE,qGAAA,QAAQ,OAAA;AAAc,6GAAA,gBAAgB,OAAA;AAC9C,qGAAA,QAAQ,OAAA;AAAE,sGAAA,SAAS,OAAA;AAAE,qGAAA,QAAQ,OAAA;AAG/B,mDAM0B;AALxB,oGAAA,EAAE,OAAA;AACF,kHAAA,gBAAgB,OAAA;AAAE,mHAAA,iBAAiB,OAAA;AAAE,mHAAA,iBAAiB,OAAA;AAAE,qHAAA,mBAAmB,OAAA;AAC3E,oHAAA,kBAAkB,OAAA;AAClB,kHAAA,gBAAgB,OAAA;AAAE,0GAAA,QAAQ,OAAA;AAAE,0GAAA,QAAQ,OAAA;AAAE,oHAAA,kBAAkB,OAAA;AAAE,kHAAA,gBAAgB,OAAA;AAC1E,wGAAA,MAAM,OAAA;AAGR,uCAGoB;AAFlB,sGAAA,UAAU,OAAA;AAAE,0GAAA,cAAc,OAAA;AAC1B,yGAAA,aAAa,OAAA;AAGf,2CAGsB;AAFpB,0GAAA,YAAY,OAAA;AAAE,8GAAA,gBAAgB,OAAA;AAC9B,6GAAA,eAAe,OAAA;AAGjB,WAAW;AACX,oDAI+B;AAH7B,8GAAA,gBAAgB,OAAA;AAAE,+GAAA,iBAAiB,OAAA;AACnC,gHAAA,kBAAkB,OAAsB;AAI1C,kDAI8B;AAH5B,0GAAA,aAAa,OAAA;AAAE,8GAAA,iBAAiB,OAAA;AAChC,qHAAA,wBAAwB,OAAA;AAI1B,kDAM8B;AAL5B,uGAAA,UAAU,OAAA;AAAE,0GAAA,aAAa,OAAA;AACzB,gHAAA,mBAAmB,OAAA;AAAE,mHAAA,sBAAsB,OAAA;AAC3C,wGAAA,WAAW,OAAA;AACX,sHAAA,yBAAyB,OAAA;AAI3B,kDAI8B;AAH5B,6GAAA,gBAAgB,OAAA;AAChB,sGAAA,SAAS,OAAA;AAAE,sGAAA,SAAS,OAAA;AAAE,6GAAA,gBAAgB,OAAA;AAAE,uGAAA,UAAU,OAAA;AAClD,6GAAA,gBAAgB,OAAA;AAAE,yGAAA,YAAY,OAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "margin-ts",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Typed health classification for systems that measure things. Zero dependencies.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Next.js API route adapter for margin.
3
+ *
4
+ * One-liner health tracking for any API route:
5
+ *
6
+ * // pages/api/users.ts (Pages Router)
7
+ * import { withMargin } from 'margin-ts/adapters/nextjs';
8
+ * export default withMargin(handler);
9
+ *
10
+ * // app/api/users/route.ts (App Router)
11
+ * import { withMarginApp } from 'margin-ts/adapters/nextjs';
12
+ * export const GET = withMarginApp(handler);
13
+ *
14
+ * // Health endpoint
15
+ * // pages/api/margin/health.ts
16
+ * import { marginHealthHandler } from 'margin-ts/adapters/nextjs';
17
+ * export default marginHealthHandler();
18
+ *
19
+ * Tracks latency, error rate, and request count per route.
20
+ * Zero dependencies beyond margin-ts core.
21
+ */
22
+
23
+ import {
24
+ Confidence,
25
+ Health,
26
+ Thresholds,
27
+ createThresholds,
28
+ classify,
29
+ Observation,
30
+ createExpression,
31
+ expressionToString,
32
+ expressionToDict,
33
+ } from '../index.js';
34
+
35
+ // -----------------------------------------------------------------------
36
+ // Shared state — singleton across all wrapped routes
37
+ // -----------------------------------------------------------------------
38
+
39
+ interface RouteStats {
40
+ totalRequests: number;
41
+ totalErrors: number;
42
+ latencies: number[];
43
+ maxWindow: number;
44
+ }
45
+
46
+ const _routes = new Map<string, RouteStats>();
47
+ const _MAX_WINDOW = 200;
48
+
49
+ function getOrCreateRoute(route: string): RouteStats {
50
+ if (!_routes.has(route)) {
51
+ _routes.set(route, {
52
+ totalRequests: 0,
53
+ totalErrors: 0,
54
+ latencies: [],
55
+ maxWindow: _MAX_WINDOW,
56
+ });
57
+ }
58
+ return _routes.get(route)!;
59
+ }
60
+
61
+ function record(route: string, latencyMs: number, isError: boolean): void {
62
+ const stats = getOrCreateRoute(route);
63
+ stats.totalRequests++;
64
+ if (isError) stats.totalErrors++;
65
+ stats.latencies.push(latencyMs);
66
+ if (stats.latencies.length > stats.maxWindow) stats.latencies.shift();
67
+ }
68
+
69
+ // -----------------------------------------------------------------------
70
+ // Thresholds
71
+ // -----------------------------------------------------------------------
72
+
73
+ export interface NextjsThresholds {
74
+ p50Latency: Thresholds;
75
+ p99Latency: Thresholds;
76
+ errorRate: Thresholds;
77
+ }
78
+
79
+ export const DEFAULT_NEXTJS_THRESHOLDS: NextjsThresholds = {
80
+ p50Latency: createThresholds(100, 500, false),
81
+ p99Latency: createThresholds(500, 2000, false),
82
+ errorRate: createThresholds(0.01, 0.10, false),
83
+ };
84
+
85
+ // -----------------------------------------------------------------------
86
+ // Classification
87
+ // -----------------------------------------------------------------------
88
+
89
+ function percentile(sorted: number[], p: number): number {
90
+ if (sorted.length === 0) return 0;
91
+ const idx = Math.ceil(sorted.length * p) - 1;
92
+ return sorted[Math.max(0, Math.min(idx, sorted.length - 1))];
93
+ }
94
+
95
+ function classifyRoute(route: string, stats: RouteStats, thresholds: NextjsThresholds) {
96
+ if (stats.latencies.length < 3) return null;
97
+
98
+ const sorted = [...stats.latencies].sort((a, b) => a - b);
99
+ const p50 = percentile(sorted, 0.5);
100
+ const p99 = percentile(sorted, 0.99);
101
+ const errorRate = stats.totalRequests > 0 ? stats.totalErrors / stats.totalRequests : 0;
102
+ const now = new Date();
103
+
104
+ const observations: Observation[] = [
105
+ {
106
+ name: `${route}:p50`, health: classify(p50, Confidence.HIGH, thresholds.p50Latency),
107
+ value: p50, baseline: thresholds.p50Latency.intact, confidence: Confidence.HIGH,
108
+ higherIsBetter: false, provenance: [], measuredAt: now,
109
+ },
110
+ {
111
+ name: `${route}:p99`, health: classify(p99, Confidence.HIGH, thresholds.p99Latency),
112
+ value: p99, baseline: thresholds.p99Latency.intact, confidence: Confidence.HIGH,
113
+ higherIsBetter: false, provenance: [], measuredAt: now,
114
+ },
115
+ {
116
+ name: `${route}:errors`, health: classify(errorRate, Confidence.HIGH, thresholds.errorRate),
117
+ value: errorRate, baseline: 0.001, confidence: Confidence.HIGH,
118
+ higherIsBetter: false, provenance: [], measuredAt: now,
119
+ },
120
+ ];
121
+
122
+ return createExpression(observations, [], route);
123
+ }
124
+
125
+ // -----------------------------------------------------------------------
126
+ // Pages Router wrapper
127
+ // -----------------------------------------------------------------------
128
+
129
+ export interface WithMarginOptions {
130
+ route?: string;
131
+ thresholds?: NextjsThresholds;
132
+ }
133
+
134
+ /**
135
+ * Wrap a Pages Router API handler with margin health tracking.
136
+ *
137
+ * export default withMargin(handler);
138
+ * export default withMargin(handler, { route: '/api/users' });
139
+ */
140
+ export function withMargin(
141
+ handler: (req: any, res: any) => any,
142
+ options: WithMarginOptions = {},
143
+ ): (req: any, res: any) => any {
144
+ return async (req: any, res: any) => {
145
+ const route = options.route || req.url || '/api/unknown';
146
+ const start = Date.now();
147
+
148
+ try {
149
+ const result = await handler(req, res);
150
+ const latency = Date.now() - start;
151
+ const status = res.statusCode || 200;
152
+ record(route, latency, status >= 500);
153
+ return result;
154
+ } catch (err) {
155
+ const latency = Date.now() - start;
156
+ record(route, latency, true);
157
+ throw err;
158
+ }
159
+ };
160
+ }
161
+
162
+ // -----------------------------------------------------------------------
163
+ // App Router wrapper
164
+ // -----------------------------------------------------------------------
165
+
166
+ /**
167
+ * Wrap an App Router handler with margin health tracking.
168
+ *
169
+ * export const GET = withMarginApp(handler);
170
+ * export const POST = withMarginApp(handler, { route: '/api/users' });
171
+ */
172
+ export function withMarginApp(
173
+ handler: (req: any) => any,
174
+ options: WithMarginOptions = {},
175
+ ): (req: any) => any {
176
+ return async (req: any) => {
177
+ const route = options.route || new URL(req.url || '/', 'http://localhost').pathname;
178
+ const start = Date.now();
179
+
180
+ try {
181
+ const response = await handler(req);
182
+ const latency = Date.now() - start;
183
+ const status = response?.status || 200;
184
+ record(route, latency, status >= 500);
185
+ return response;
186
+ } catch (err) {
187
+ const latency = Date.now() - start;
188
+ record(route, latency, true);
189
+ throw err;
190
+ }
191
+ };
192
+ }
193
+
194
+ // -----------------------------------------------------------------------
195
+ // Health endpoint
196
+ // -----------------------------------------------------------------------
197
+
198
+ /**
199
+ * Pages Router: health endpoint handler.
200
+ *
201
+ * // pages/api/margin/health.ts
202
+ * import { marginHealthHandler } from 'margin-ts/adapters/nextjs';
203
+ * export default marginHealthHandler();
204
+ */
205
+ export function marginHealthHandler(thresholds?: NextjsThresholds): (req: any, res: any) => void {
206
+ const t = thresholds || DEFAULT_NEXTJS_THRESHOLDS;
207
+
208
+ return (_req: any, res: any) => {
209
+ const routeHealth: Record<string, any> = {};
210
+ let worstOverall = Health.INTACT;
211
+ const SEVERITY: Record<Health, number> = {
212
+ [Health.INTACT]: 0, [Health.RECOVERING]: 1, [Health.DEGRADED]: 2,
213
+ [Health.ABLATED]: 3, [Health.OOD]: 4,
214
+ };
215
+
216
+ for (const [route, stats] of _routes) {
217
+ const expr = classifyRoute(route, stats, t);
218
+ if (expr) {
219
+ routeHealth[route] = {
220
+ expression: expressionToString(expr),
221
+ ...expressionToDict(expr),
222
+ requests: stats.totalRequests,
223
+ errors: stats.totalErrors,
224
+ };
225
+ for (const obs of expr.observations) {
226
+ if (SEVERITY[obs.health] > SEVERITY[worstOverall]) {
227
+ worstOverall = obs.health;
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ res.status(200).json({
234
+ status: worstOverall,
235
+ routes: routeHealth,
236
+ totalRoutes: _routes.size,
237
+ });
238
+ };
239
+ }
240
+
241
+ /**
242
+ * App Router: health endpoint handler.
243
+ *
244
+ * // app/api/margin/health/route.ts
245
+ * import { marginHealthAppHandler } from 'margin-ts/adapters/nextjs';
246
+ * export const GET = marginHealthAppHandler();
247
+ */
248
+ export function marginHealthAppHandler(thresholds?: NextjsThresholds): (req: any) => any {
249
+ const t = thresholds || DEFAULT_NEXTJS_THRESHOLDS;
250
+
251
+ return (_req: any) => {
252
+ const routeHealth: Record<string, any> = {};
253
+ let worstOverall = Health.INTACT;
254
+ const SEVERITY: Record<Health, number> = {
255
+ [Health.INTACT]: 0, [Health.RECOVERING]: 1, [Health.DEGRADED]: 2,
256
+ [Health.ABLATED]: 3, [Health.OOD]: 4,
257
+ };
258
+
259
+ for (const [route, stats] of _routes) {
260
+ const expr = classifyRoute(route, stats, t);
261
+ if (expr) {
262
+ routeHealth[route] = {
263
+ expression: expressionToString(expr),
264
+ ...expressionToDict(expr),
265
+ requests: stats.totalRequests,
266
+ errors: stats.totalErrors,
267
+ };
268
+ for (const obs of expr.observations) {
269
+ if (SEVERITY[obs.health] > SEVERITY[worstOverall]) {
270
+ worstOverall = obs.health;
271
+ }
272
+ }
273
+ }
274
+ }
275
+
276
+ return new Response(JSON.stringify({
277
+ status: worstOverall,
278
+ routes: routeHealth,
279
+ totalRoutes: _routes.size,
280
+ }), {
281
+ status: 200,
282
+ headers: { 'Content-Type': 'application/json' },
283
+ });
284
+ };
285
+ }
286
+
287
+ /** Clear all tracked routes (for testing). */
288
+ export function resetRoutes(): void {
289
+ _routes.clear();
290
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Streaming health updates via Server-Sent Events (SSE).
3
+ *
4
+ * Push health/drift/anomaly updates to browser dashboards in real-time.
5
+ *
6
+ * Express:
7
+ * app.get('/margin/stream', marginSSE(monitor));
8
+ *
9
+ * Next.js Pages Router:
10
+ * export default marginSSEHandler(monitor);
11
+ *
12
+ * Browser client:
13
+ * const source = new EventSource('/margin/stream');
14
+ * source.onmessage = (e) => {
15
+ * const health = JSON.parse(e.data);
16
+ * updateDashboard(health);
17
+ * };
18
+ *
19
+ * Zero dependencies. Works with any framework that supports
20
+ * writable response streams (Express, Fastify, Next.js, raw Node http).
21
+ */
22
+
23
+ import {
24
+ Confidence,
25
+ Health,
26
+ Observation,
27
+ Expression,
28
+ Parser,
29
+ createExpression,
30
+ expressionToString,
31
+ expressionToDict,
32
+ classifyDrift,
33
+ DriftClassification,
34
+ DriftState,
35
+ DriftDirection,
36
+ classifyAnomaly,
37
+ AnomalyClassification,
38
+ AnomalyState,
39
+ } from '../index.js';
40
+
41
+ // -----------------------------------------------------------------------
42
+ // StreamingMonitor — the TS equivalent of Python's Monitor
43
+ // -----------------------------------------------------------------------
44
+
45
+ interface ComponentWindow {
46
+ values: number[];
47
+ observations: Observation[];
48
+ maxWindow: number;
49
+ }
50
+
51
+ export class StreamingMonitor {
52
+ private parser: Parser;
53
+ private windows: Map<string, ComponentWindow> = new Map();
54
+ private _expression: Expression | null = null;
55
+ private _step = 0;
56
+ private maxWindow: number;
57
+ private minAnomalyRef: number;
58
+
59
+ constructor(parser: Parser, options: { window?: number; minAnomalyRef?: number } = {}) {
60
+ this.parser = parser;
61
+ this.maxWindow = options.window ?? 100;
62
+ this.minAnomalyRef = options.minAnomalyRef ?? 10;
63
+ }
64
+
65
+ get expression(): Expression | null { return this._expression; }
66
+ get step(): number { return this._step; }
67
+
68
+ update(values: Record<string, number>, now?: Date): Expression {
69
+ now = now ?? new Date();
70
+ this._expression = this.parser.parse(values, { step: this._step });
71
+ this._step++;
72
+
73
+ // Update per-component windows
74
+ for (const obs of this._expression.observations) {
75
+ if (!this.windows.has(obs.name)) {
76
+ this.windows.set(obs.name, { values: [], observations: [], maxWindow: this.maxWindow });
77
+ }
78
+ const w = this.windows.get(obs.name)!;
79
+ w.values.push(obs.value);
80
+ // Stamp the observation with time for drift
81
+ const stamped = { ...obs, measuredAt: now };
82
+ w.observations.push(stamped);
83
+ if (w.values.length > this.maxWindow) {
84
+ w.values.shift();
85
+ w.observations.shift();
86
+ }
87
+ }
88
+
89
+ return this._expression;
90
+ }
91
+
92
+ drift(component: string): DriftClassification | null {
93
+ const w = this.windows.get(component);
94
+ if (!w || w.observations.length < 3) return null;
95
+ return classifyDrift(w.observations);
96
+ }
97
+
98
+ anomaly(component: string): AnomalyClassification | null {
99
+ const w = this.windows.get(component);
100
+ if (!w || w.values.length < this.minAnomalyRef + 1) return null;
101
+ const ref = w.values.slice(0, -1);
102
+ const current = w.values[w.values.length - 1];
103
+ return classifyAnomaly(current, ref, { component });
104
+ }
105
+
106
+ status(): Record<string, unknown> {
107
+ const drift: Record<string, unknown> = {};
108
+ const anomaly: Record<string, unknown> = {};
109
+
110
+ for (const [name] of this.windows) {
111
+ const dc = this.drift(name);
112
+ if (dc) drift[name] = { state: dc.state, direction: dc.direction, rate: dc.rate };
113
+ const ac = this.anomaly(name);
114
+ if (ac) anomaly[name] = { state: ac.state, zScore: ac.zScore };
115
+ }
116
+
117
+ return {
118
+ step: this._step,
119
+ expression: this._expression ? expressionToString(this._expression) : null,
120
+ health: this._expression ? expressionToDict(this._expression) : null,
121
+ drift,
122
+ anomaly,
123
+ };
124
+ }
125
+
126
+ reset(): void {
127
+ this.windows.clear();
128
+ this._expression = null;
129
+ this._step = 0;
130
+ }
131
+ }
132
+
133
+ // -----------------------------------------------------------------------
134
+ // SSE helpers
135
+ // -----------------------------------------------------------------------
136
+
137
+ /** Connected SSE client. */
138
+ interface SSEClient {
139
+ id: string;
140
+ res: any;
141
+ }
142
+
143
+ let _clients: SSEClient[] = [];
144
+ let _clientIdCounter = 0;
145
+
146
+ /**
147
+ * Broadcast a status update to all connected SSE clients.
148
+ * Call this after each monitor.update().
149
+ */
150
+ export function broadcast(monitor: StreamingMonitor): void {
151
+ const data = JSON.stringify(monitor.status());
152
+ const message = `data: ${data}\n\n`;
153
+ _clients = _clients.filter(client => {
154
+ try {
155
+ client.res.write(message);
156
+ return true;
157
+ } catch {
158
+ return false; // client disconnected
159
+ }
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Express/Fastify route handler for SSE streaming.
165
+ *
166
+ * app.get('/margin/stream', marginSSE(monitor));
167
+ *
168
+ * Each connected browser receives real-time health updates.
169
+ * Call broadcast(monitor) after each monitor.update() to push.
170
+ */
171
+ export function marginSSE(monitor: StreamingMonitor): (req: any, res: any) => void {
172
+ return (req: any, res: any) => {
173
+ res.writeHead(200, {
174
+ 'Content-Type': 'text/event-stream',
175
+ 'Cache-Control': 'no-cache',
176
+ 'Connection': 'keep-alive',
177
+ 'Access-Control-Allow-Origin': '*',
178
+ });
179
+
180
+ // Send current state immediately
181
+ const initial = JSON.stringify(monitor.status());
182
+ res.write(`data: ${initial}\n\n`);
183
+
184
+ const client: SSEClient = { id: String(++_clientIdCounter), res };
185
+ _clients.push(client);
186
+
187
+ req.on('close', () => {
188
+ _clients = _clients.filter(c => c.id !== client.id);
189
+ });
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Next.js Pages Router SSE handler.
195
+ *
196
+ * // pages/api/margin/stream.ts
197
+ * export default marginSSEHandler(monitor);
198
+ */
199
+ export function marginSSEHandler(monitor: StreamingMonitor): (req: any, res: any) => void {
200
+ return marginSSE(monitor);
201
+ }
202
+
203
+ /**
204
+ * Create a polling endpoint (for environments that don't support SSE).
205
+ * Returns the current status as JSON on each request.
206
+ *
207
+ * app.get('/margin/poll', marginPoll(monitor));
208
+ */
209
+ export function marginPoll(monitor: StreamingMonitor): (req: any, res: any) => void {
210
+ return (_req: any, res: any) => {
211
+ res.json(monitor.status());
212
+ };
213
+ }
214
+
215
+ /** Number of connected SSE clients. */
216
+ export function connectedClients(): number {
217
+ return _clients.length;
218
+ }
219
+
220
+ /** Disconnect all SSE clients (for testing). */
221
+ export function resetClients(): void {
222
+ for (const client of _clients) {
223
+ try { client.res.end(); } catch {}
224
+ }
225
+ _clients = [];
226
+ _clientIdCounter = 0;
227
+ }
package/src/index.ts CHANGED
@@ -44,3 +44,17 @@ export {
44
44
  DEFAULT_SUITE_THRESHOLDS,
45
45
  type SuiteResults, type SuiteThresholds,
46
46
  } from './adapters/vitest.js';
47
+
48
+ export {
49
+ withMargin, withMarginApp,
50
+ marginHealthHandler, marginHealthAppHandler,
51
+ resetRoutes,
52
+ DEFAULT_NEXTJS_THRESHOLDS,
53
+ type WithMarginOptions, type NextjsThresholds,
54
+ } from './adapters/nextjs.js';
55
+
56
+ export {
57
+ StreamingMonitor,
58
+ broadcast, marginSSE, marginSSEHandler, marginPoll,
59
+ connectedClients, resetClients,
60
+ } from './adapters/stream.js';