@a-company/paradigm 3.1.6 → 3.5.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/dist/{accept-orchestration-CWZNCGZX.js → accept-orchestration-DIGPJVUR.js} +6 -5
- package/dist/{aggregate-W7Q6VIM2.js → aggregate-V4KPR3RW.js} +2 -2
- package/dist/{beacon-B47XSTL7.js → beacon-XRXL5KZB.js} +2 -2
- package/dist/{chunk-4LGLU2LO.js → chunk-2E2RTBSM.js} +533 -182
- package/dist/{chunk-YCLN7WXV.js → chunk-2QNZ6PVD.js} +219 -35
- package/dist/{chunk-UM54F7G5.js → chunk-4N6AYEEA.js} +1 -1
- package/dist/{chunk-MVXJVRFI.js → chunk-5TUAVVIG.js} +65 -1
- package/dist/{chunk-5C4SGQKH.js → chunk-6P4IFIK2.js} +4 -2
- package/dist/{chunk-WS5KM7OL.js → chunk-6RNYVBSG.js} +1 -1
- package/dist/{chunk-N6PJAPDE.js → chunk-AK5M6KJB.js} +18 -0
- package/dist/{chunk-VZ7CXFRZ.js → chunk-CRICL4FQ.js} +1004 -17
- package/dist/{chunk-MC7XC7XQ.js → chunk-GZDFVP2N.js} +20 -13
- package/dist/chunk-HPC3JAUP.js +42 -0
- package/dist/chunk-IRVA7NKV.js +657 -0
- package/dist/{chunk-ZPN7MXRA.js → chunk-KFHK6EBI.js} +184 -1
- package/dist/{chunk-UUZ2DMG5.js → chunk-KWDTBXP2.js} +1 -1
- package/dist/{chunk-HXY6AY52.js → chunk-M2XMTJHQ.js} +667 -70
- package/dist/{chunk-PW2EXJQT.js → chunk-MRENOFTR.js} +24 -1
- package/dist/{chunk-QS36NGWV.js → chunk-QHJGB5TV.js} +1 -1
- package/dist/chunk-UI3XXVJ6.js +449 -0
- package/dist/{chunk-AD2LSCHB.js → chunk-Y4XZWCHK.js} +40 -74
- package/dist/{constellation-K3CIQCHI.js → constellation-GNK5DIMH.js} +2 -2
- package/dist/{cost-AEK6R7HK.js → cost-AGO5N7DD.js} +1 -1
- package/dist/{cursorrules-KI5QWHIX.js → cursorrules-LQFA7M62.js} +2 -2
- package/dist/{delete-W67IVTLJ.js → delete-3YXAJ5AA.js} +12 -1
- package/dist/{diff-AJJ5H6HV.js → diff-J6C5IHPV.js} +6 -5
- package/dist/{dist-2F7NO4H4-KSL6SJIO.js → dist-AG5JNIZU-XSEZ2LLK.js} +28 -3
- package/dist/dist-JOHRYQUA.js +7294 -0
- package/dist/{dist-NHJQVVUW.js → dist-Q6SAZI7X.js} +2 -2
- package/dist/{dist-GPQ4LAY3.js → dist-YP2CO4TG.js} +24 -6
- package/dist/{doctor-JBIV5PMN.js → doctor-TQYRF7KK.js} +2 -2
- package/dist/{edit-Y7XPYSMK.js → edit-EOMPXOG5.js} +1 -1
- package/dist/flow-7JUH6D4H.js +185 -0
- package/dist/global-AXILUM5X.js +136 -0
- package/dist/{habits-FA65W77Y.js → habits-CHP4EW5H.js} +234 -5
- package/dist/{hooks-RLJFGKPF.js → hooks-DLZEYHI3.js} +1 -1
- package/dist/index.js +125 -100
- package/dist/{lint-HXKTWRNO.js → lint-N4LMMEXH.js} +141 -1
- package/dist/{list-R3QWW4SC.js → list-JKBJ7ESH.js} +1 -1
- package/dist/mcp.js +9273 -6515
- package/dist/{orchestrate-4ZH5GUQH.js → orchestrate-FAV64G2R.js} +6 -5
- package/dist/{probe-OYCP4JYG.js → probe-X3J2JX62.js} +18 -3
- package/dist/{promote-E6NBZ3BK.js → promote-HZH5E5CO.js} +1 -1
- package/dist/{providers-4PGPZEWP.js → providers-NQ67LO2Z.js} +1 -1
- package/dist/{record-OHQNWOUP.js → record-EECZ3E4I.js} +1 -1
- package/dist/{remember-6VZ74B7E.js → remember-3KJZGDUG.js} +1 -1
- package/dist/{review-RUHX25A5.js → review-BF26ILZB.js} +1 -1
- package/dist/{ripple-SBQOSTZD.js → ripple-JIUAMBLA.js} +2 -2
- package/dist/sentinel-ZTL224IG.js +63 -0
- package/dist/{server-MV4HNFVF.js → server-MZBYDXJY.js} +4193 -9
- package/dist/{setup-DF4F3ICN.js → setup-363IB6MO.js} +1 -1
- package/dist/{setup-JHBPZAG7.js → setup-UKJ3VGHI.js} +4 -4
- package/dist/{shift-YELZUPYG.js → shift-KDVYB6CR.js} +16 -13
- package/dist/{show-WTOJXUTN.js → show-SAMTXEHG.js} +1 -1
- package/dist/{snapshot-GTVPRYZG.js → snapshot-KCMONZAO.js} +2 -2
- package/dist/{spawn-BJRQA2NR.js → spawn-EO7B2UM3.js} +2 -2
- package/dist/{summary-5SBFO7QK.js → summary-E2PU4UN2.js} +3 -3
- package/dist/{switch-6EANJ7O6.js → switch-CC2KACXO.js} +1 -1
- package/dist/{sync-5KSTPJ4B.js → sync-5VJPZQNX.js} +2 -2
- package/dist/sync-llms-7QDA3ZWC.js +166 -0
- package/dist/{team-NWP2KJAB.js → team-6CCNANKE.js} +7 -6
- package/dist/{test-MA5TWJQV.js → test-DK2RWLTK.js} +91 -8
- package/dist/{thread-JCJVRUQR.js → thread-RNSLADXN.js} +18 -2
- package/dist/{timeline-P7BARFLI.js → timeline-TJDVVVA3.js} +1 -1
- package/dist/{triage-TBIWJA6R.js → triage-PXMU3RWV.js} +2 -2
- package/dist/university-content/courses/para-101.json +2 -1
- package/dist/university-content/courses/para-201.json +102 -3
- package/dist/university-content/courses/para-301.json +14 -11
- package/dist/university-content/courses/para-401.json +57 -3
- package/dist/university-content/courses/para-501.json +204 -6
- package/dist/university-content/plsat/v3.0.json +808 -3
- package/dist/university-content/reference.json +270 -0
- package/dist/{upgrade-TIYFQYPO.js → upgrade-RBSE4M6I.js} +1 -1
- package/dist/{validate-QEEY6KFS.js → validate-2LTHHORX.js} +1 -1
- package/dist/{watch-4LT4O6K7.js → watch-NBPOMOEX.js} +76 -0
- package/dist/{watch-2XEYUH43.js → watch-PAEH6MOG.js} +1 -1
- package/package.json +1 -1
- package/dist/chunk-GWM2WRXL.js +0 -1095
- package/dist/sentinel-WB7GIK4V.js +0 -43
- /package/dist/{chunk-TAP5N3HH.js → chunk-CCG6KYBT.js} +0 -0
|
@@ -1,7 +1,420 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
SentinelStorage
|
|
4
|
-
|
|
3
|
+
SentinelStorage,
|
|
4
|
+
v4_default
|
|
5
|
+
} from "./chunk-CRICL4FQ.js";
|
|
6
|
+
|
|
7
|
+
// ../sentinel/dist/chunk-VQ3SIN7S.js
|
|
8
|
+
var DEFAULTS = {
|
|
9
|
+
url: "http://localhost:3838",
|
|
10
|
+
batchSize: 50,
|
|
11
|
+
flushIntervalMs: 5e3,
|
|
12
|
+
maxBufferSize: 1e3,
|
|
13
|
+
maxRetries: 3,
|
|
14
|
+
retryBackoffMs: 1e3
|
|
15
|
+
};
|
|
16
|
+
var SentinelClient = class {
|
|
17
|
+
url;
|
|
18
|
+
service;
|
|
19
|
+
version;
|
|
20
|
+
environment;
|
|
21
|
+
token;
|
|
22
|
+
batchSize;
|
|
23
|
+
maxBufferSize;
|
|
24
|
+
maxRetries;
|
|
25
|
+
retryBackoffMs;
|
|
26
|
+
onDrop;
|
|
27
|
+
onError;
|
|
28
|
+
sessionId;
|
|
29
|
+
logBuffer = [];
|
|
30
|
+
metricsBuffer = [];
|
|
31
|
+
flushTimer = null;
|
|
32
|
+
closed = false;
|
|
33
|
+
beforeUnloadHandler = null;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.url = (options.url ?? DEFAULTS.url).replace(/\/+$/, "");
|
|
36
|
+
this.service = options.service;
|
|
37
|
+
this.version = options.version;
|
|
38
|
+
this.environment = options.environment;
|
|
39
|
+
this.token = options.token;
|
|
40
|
+
this.batchSize = options.batchSize ?? DEFAULTS.batchSize;
|
|
41
|
+
this.maxBufferSize = options.maxBufferSize ?? DEFAULTS.maxBufferSize;
|
|
42
|
+
this.maxRetries = options.maxRetries ?? DEFAULTS.maxRetries;
|
|
43
|
+
this.retryBackoffMs = options.retryBackoffMs ?? DEFAULTS.retryBackoffMs;
|
|
44
|
+
this.onDrop = options.onDrop;
|
|
45
|
+
this.onError = options.onError;
|
|
46
|
+
this.sessionId = v4_default();
|
|
47
|
+
const intervalMs = options.flushIntervalMs ?? DEFAULTS.flushIntervalMs;
|
|
48
|
+
this.flushTimer = setInterval(() => {
|
|
49
|
+
this.flush().catch((err) => {
|
|
50
|
+
this.handleError(err);
|
|
51
|
+
});
|
|
52
|
+
}, intervalMs);
|
|
53
|
+
if (this.flushTimer && typeof this.flushTimer === "object" && "unref" in this.flushTimer) {
|
|
54
|
+
this.flushTimer.unref();
|
|
55
|
+
}
|
|
56
|
+
if (typeof globalThis !== "undefined" && typeof globalThis.addEventListener === "function") {
|
|
57
|
+
this.beforeUnloadHandler = () => {
|
|
58
|
+
this.flushSync();
|
|
59
|
+
};
|
|
60
|
+
globalThis.addEventListener("beforeunload", this.beforeUnloadHandler);
|
|
61
|
+
}
|
|
62
|
+
this.registerService();
|
|
63
|
+
}
|
|
64
|
+
// ── Logging Methods ──────────────────────────────────────────────
|
|
65
|
+
/** Log a debug-level message */
|
|
66
|
+
debug(symbol, message, data) {
|
|
67
|
+
this.log("debug", symbol, message, data);
|
|
68
|
+
}
|
|
69
|
+
/** Log an info-level message */
|
|
70
|
+
info(symbol, message, data) {
|
|
71
|
+
this.log("info", symbol, message, data);
|
|
72
|
+
}
|
|
73
|
+
/** Log a warn-level message */
|
|
74
|
+
warn(symbol, message, data) {
|
|
75
|
+
this.log("warn", symbol, message, data);
|
|
76
|
+
}
|
|
77
|
+
/** Log an error-level message */
|
|
78
|
+
error(symbol, message, data) {
|
|
79
|
+
this.log("error", symbol, message, data);
|
|
80
|
+
}
|
|
81
|
+
/** Log a message at the specified level */
|
|
82
|
+
log(level, symbol, message, data) {
|
|
83
|
+
if (this.closed) return;
|
|
84
|
+
const entry = {
|
|
85
|
+
level,
|
|
86
|
+
symbol,
|
|
87
|
+
message,
|
|
88
|
+
service: this.service,
|
|
89
|
+
sessionId: this.sessionId,
|
|
90
|
+
environment: this.environment,
|
|
91
|
+
data
|
|
92
|
+
};
|
|
93
|
+
this.pushToLogBuffer(entry);
|
|
94
|
+
}
|
|
95
|
+
// ── Metrics Methods ──────────────────────────────────────────────
|
|
96
|
+
/** Record a counter metric (increments) */
|
|
97
|
+
counter(name, value, tags) {
|
|
98
|
+
this.metric({ name, type: "counter", value: value ?? 1, tags });
|
|
99
|
+
}
|
|
100
|
+
/** Record a gauge metric (current value) */
|
|
101
|
+
gauge(name, value, tags) {
|
|
102
|
+
this.metric({ name, type: "gauge", value, tags });
|
|
103
|
+
}
|
|
104
|
+
/** Record a histogram metric (distribution) */
|
|
105
|
+
histogram(name, value, tags) {
|
|
106
|
+
this.metric({ name, type: "histogram", value, tags });
|
|
107
|
+
}
|
|
108
|
+
/** Record a metric of any type */
|
|
109
|
+
metric(input) {
|
|
110
|
+
if (this.closed) return;
|
|
111
|
+
const entry = {
|
|
112
|
+
...input,
|
|
113
|
+
service: this.service,
|
|
114
|
+
environment: this.environment
|
|
115
|
+
};
|
|
116
|
+
this.pushToMetricsBuffer(entry);
|
|
117
|
+
}
|
|
118
|
+
// ── State Push ───────────────────────────────────────────────────
|
|
119
|
+
/** Push an application state snapshot to the server */
|
|
120
|
+
async pushState(state, activeFlows, activeGates) {
|
|
121
|
+
if (this.closed) return;
|
|
122
|
+
await this.post("/api/state", {
|
|
123
|
+
service: this.service,
|
|
124
|
+
sessionId: this.sessionId,
|
|
125
|
+
state,
|
|
126
|
+
activeFlows,
|
|
127
|
+
activeGates
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// ── Tracing ──────────────────────────────────────────────────────
|
|
131
|
+
/** Start a trace span. Call end() on the returned SpanContext when the operation completes. */
|
|
132
|
+
startSpan(symbol, operation, parentSpanId) {
|
|
133
|
+
const traceId = parentSpanId ? parentSpanId.split("-")[0] || v4_default() : v4_default();
|
|
134
|
+
const spanId = v4_default();
|
|
135
|
+
const startTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
136
|
+
const startMs = Date.now();
|
|
137
|
+
const self = this;
|
|
138
|
+
return {
|
|
139
|
+
traceId,
|
|
140
|
+
spanId,
|
|
141
|
+
async end(status = "ok") {
|
|
142
|
+
const endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
143
|
+
const durationMs = Date.now() - startMs;
|
|
144
|
+
const span = {
|
|
145
|
+
traceId,
|
|
146
|
+
spanId,
|
|
147
|
+
parentSpanId,
|
|
148
|
+
service: self.service,
|
|
149
|
+
symbol,
|
|
150
|
+
operation,
|
|
151
|
+
startTime,
|
|
152
|
+
endTime,
|
|
153
|
+
durationMs,
|
|
154
|
+
status
|
|
155
|
+
};
|
|
156
|
+
await self.post("/api/traces", span);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// ── Buffer Management ────────────────────────────────────────────
|
|
161
|
+
/** Flush all buffered logs and metrics to the server */
|
|
162
|
+
async flush() {
|
|
163
|
+
const logEntries = this.drainLogBuffer();
|
|
164
|
+
const metricEntries = this.drainMetricsBuffer();
|
|
165
|
+
const promises = [];
|
|
166
|
+
if (logEntries.length > 0) {
|
|
167
|
+
promises.push(this.sendLogs(logEntries));
|
|
168
|
+
}
|
|
169
|
+
if (metricEntries.length > 0) {
|
|
170
|
+
promises.push(this.sendMetrics(metricEntries));
|
|
171
|
+
}
|
|
172
|
+
await Promise.allSettled(promises);
|
|
173
|
+
}
|
|
174
|
+
// ── Lifecycle ────────────────────────────────────────────────────
|
|
175
|
+
/** Flush remaining entries and shut down the client */
|
|
176
|
+
async close() {
|
|
177
|
+
if (this.closed) return;
|
|
178
|
+
this.closed = true;
|
|
179
|
+
if (this.flushTimer !== null) {
|
|
180
|
+
clearInterval(this.flushTimer);
|
|
181
|
+
this.flushTimer = null;
|
|
182
|
+
}
|
|
183
|
+
if (this.beforeUnloadHandler && typeof globalThis !== "undefined" && typeof globalThis.removeEventListener === "function") {
|
|
184
|
+
globalThis.removeEventListener("beforeunload", this.beforeUnloadHandler);
|
|
185
|
+
this.beforeUnloadHandler = null;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
await this.flush();
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Get the session ID assigned to this client instance */
|
|
193
|
+
getSessionId() {
|
|
194
|
+
return this.sessionId;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Synchronous best-effort flush using sendBeacon (browser) or sync XHR fallback.
|
|
198
|
+
* Used in beforeunload where async is unreliable.
|
|
199
|
+
*/
|
|
200
|
+
flushSync() {
|
|
201
|
+
const logEntries = this.drainLogBuffer();
|
|
202
|
+
const metricEntries = this.drainMetricsBuffer();
|
|
203
|
+
const sendBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? navigator.sendBeacon.bind(navigator) : void 0;
|
|
204
|
+
if (sendBeacon) {
|
|
205
|
+
if (logEntries.length > 0) {
|
|
206
|
+
sendBeacon(`${this.url}/api/logs`, JSON.stringify({ entries: logEntries }));
|
|
207
|
+
}
|
|
208
|
+
if (metricEntries.length > 0) {
|
|
209
|
+
sendBeacon(`${this.url}/api/metrics`, JSON.stringify({ entries: metricEntries }));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
214
|
+
// PRIVATE METHODS
|
|
215
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
216
|
+
/** Register this service with the Sentinel server (fire-and-forget) */
|
|
217
|
+
registerService() {
|
|
218
|
+
const registration = {
|
|
219
|
+
name: this.service,
|
|
220
|
+
version: this.version,
|
|
221
|
+
pid: typeof process !== "undefined" ? process.pid : void 0,
|
|
222
|
+
environment: this.environment
|
|
223
|
+
};
|
|
224
|
+
this.post("/api/services", registration).catch(() => {
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/** Push a log entry into the ring buffer, enforcing maxBufferSize */
|
|
228
|
+
pushToLogBuffer(entry) {
|
|
229
|
+
if (this.logBuffer.length >= this.maxBufferSize) {
|
|
230
|
+
const dropCount = Math.max(1, Math.floor(this.maxBufferSize * 0.1));
|
|
231
|
+
this.logBuffer.splice(0, dropCount);
|
|
232
|
+
if (this.onDrop) {
|
|
233
|
+
this.onDrop(dropCount);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
this.logBuffer.push(entry);
|
|
237
|
+
if (this.logBuffer.length >= this.batchSize) {
|
|
238
|
+
this.flush().catch((err) => {
|
|
239
|
+
this.handleError(err);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** Push a metric entry into the ring buffer, enforcing maxBufferSize */
|
|
244
|
+
pushToMetricsBuffer(entry) {
|
|
245
|
+
if (this.metricsBuffer.length >= this.maxBufferSize) {
|
|
246
|
+
const dropCount = Math.max(1, Math.floor(this.maxBufferSize * 0.1));
|
|
247
|
+
this.metricsBuffer.splice(0, dropCount);
|
|
248
|
+
if (this.onDrop) {
|
|
249
|
+
this.onDrop(dropCount);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
this.metricsBuffer.push(entry);
|
|
253
|
+
if (this.metricsBuffer.length >= this.batchSize) {
|
|
254
|
+
this.flush().catch((err) => {
|
|
255
|
+
this.handleError(err);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** Drain and return all entries from the log buffer */
|
|
260
|
+
drainLogBuffer() {
|
|
261
|
+
const entries = this.logBuffer;
|
|
262
|
+
this.logBuffer = [];
|
|
263
|
+
return entries;
|
|
264
|
+
}
|
|
265
|
+
/** Drain and return all entries from the metrics buffer */
|
|
266
|
+
drainMetricsBuffer() {
|
|
267
|
+
const entries = this.metricsBuffer;
|
|
268
|
+
this.metricsBuffer = [];
|
|
269
|
+
return entries;
|
|
270
|
+
}
|
|
271
|
+
/** Send log entries to the server with retry */
|
|
272
|
+
async sendLogs(entries) {
|
|
273
|
+
try {
|
|
274
|
+
await this.post("/api/logs", { entries });
|
|
275
|
+
} catch (err) {
|
|
276
|
+
const capacity = this.maxBufferSize - this.logBuffer.length;
|
|
277
|
+
if (capacity > 0) {
|
|
278
|
+
const toRestore = entries.slice(0, capacity);
|
|
279
|
+
this.logBuffer.unshift(...toRestore);
|
|
280
|
+
const dropped = entries.length - toRestore.length;
|
|
281
|
+
if (dropped > 0 && this.onDrop) {
|
|
282
|
+
this.onDrop(dropped);
|
|
283
|
+
}
|
|
284
|
+
} else if (this.onDrop) {
|
|
285
|
+
this.onDrop(entries.length);
|
|
286
|
+
}
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/** Send metric entries to the server with retry */
|
|
291
|
+
async sendMetrics(entries) {
|
|
292
|
+
try {
|
|
293
|
+
await this.post("/api/metrics", { entries });
|
|
294
|
+
} catch (err) {
|
|
295
|
+
const capacity = this.maxBufferSize - this.metricsBuffer.length;
|
|
296
|
+
if (capacity > 0) {
|
|
297
|
+
const toRestore = entries.slice(0, capacity);
|
|
298
|
+
this.metricsBuffer.unshift(...toRestore);
|
|
299
|
+
const dropped = entries.length - toRestore.length;
|
|
300
|
+
if (dropped > 0 && this.onDrop) {
|
|
301
|
+
this.onDrop(dropped);
|
|
302
|
+
}
|
|
303
|
+
} else if (this.onDrop) {
|
|
304
|
+
this.onDrop(entries.length);
|
|
305
|
+
}
|
|
306
|
+
throw err;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* POST JSON to the Sentinel server with exponential backoff retry.
|
|
311
|
+
* Retries on network errors and 5xx responses. Does NOT retry on 4xx.
|
|
312
|
+
*/
|
|
313
|
+
async post(path4, body) {
|
|
314
|
+
const fetchFn = this.getFetch();
|
|
315
|
+
if (!fetchFn) {
|
|
316
|
+
return void 0;
|
|
317
|
+
}
|
|
318
|
+
const url = `${this.url}${path4}`;
|
|
319
|
+
const headers = {
|
|
320
|
+
"Content-Type": "application/json"
|
|
321
|
+
};
|
|
322
|
+
if (this.token) {
|
|
323
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
324
|
+
}
|
|
325
|
+
let lastError;
|
|
326
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
327
|
+
try {
|
|
328
|
+
const response = await fetchFn(url, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers,
|
|
331
|
+
body: JSON.stringify(body)
|
|
332
|
+
});
|
|
333
|
+
if (response.status >= 400 && response.status < 500) {
|
|
334
|
+
const text = await response.text().catch(() => "");
|
|
335
|
+
const err = new Error(`Sentinel server returned ${response.status}: ${text}`);
|
|
336
|
+
this.handleError(err);
|
|
337
|
+
return void 0;
|
|
338
|
+
}
|
|
339
|
+
if (response.status >= 500) {
|
|
340
|
+
const text = await response.text().catch(() => "");
|
|
341
|
+
lastError = new Error(`Sentinel server returned ${response.status}: ${text}`);
|
|
342
|
+
if (attempt < this.maxRetries) {
|
|
343
|
+
await this.backoff(attempt);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
this.handleError(lastError);
|
|
347
|
+
throw lastError;
|
|
348
|
+
}
|
|
349
|
+
const contentType = response.headers.get("content-type") || "";
|
|
350
|
+
if (contentType.includes("application/json")) {
|
|
351
|
+
return await response.json();
|
|
352
|
+
}
|
|
353
|
+
return void 0;
|
|
354
|
+
} catch (err) {
|
|
355
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
356
|
+
if (attempt < this.maxRetries) {
|
|
357
|
+
await this.backoff(attempt);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
this.handleError(lastError);
|
|
361
|
+
throw lastError;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (lastError) throw lastError;
|
|
365
|
+
return void 0;
|
|
366
|
+
}
|
|
367
|
+
/** Get the fetch function, falling back to globalThis.fetch */
|
|
368
|
+
getFetch() {
|
|
369
|
+
if (typeof globalThis !== "undefined" && typeof globalThis.fetch === "function") {
|
|
370
|
+
return globalThis.fetch;
|
|
371
|
+
}
|
|
372
|
+
return void 0;
|
|
373
|
+
}
|
|
374
|
+
/** Wait with exponential backoff */
|
|
375
|
+
backoff(attempt) {
|
|
376
|
+
const delayMs = this.retryBackoffMs * Math.pow(2, attempt);
|
|
377
|
+
const jitter = Math.floor(Math.random() * delayMs * 0.25);
|
|
378
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs + jitter));
|
|
379
|
+
}
|
|
380
|
+
/** Handle an error, calling the onError callback if provided */
|
|
381
|
+
handleError(err) {
|
|
382
|
+
if (this.onError) {
|
|
383
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
384
|
+
this.onError(error);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
function createSentinelClient(options) {
|
|
389
|
+
return new SentinelClient(options);
|
|
390
|
+
}
|
|
391
|
+
var SentinelTransport = class {
|
|
392
|
+
client;
|
|
393
|
+
constructor(client) {
|
|
394
|
+
this.client = client;
|
|
395
|
+
}
|
|
396
|
+
send(entry) {
|
|
397
|
+
this.client.log(
|
|
398
|
+
entry.level,
|
|
399
|
+
entry.symbol,
|
|
400
|
+
entry.message,
|
|
401
|
+
{
|
|
402
|
+
...entry.data,
|
|
403
|
+
symbolType: entry.symbolType,
|
|
404
|
+
correlationId: entry.correlationId
|
|
405
|
+
}
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
function createSentinelTransport(clientOrOptions) {
|
|
410
|
+
const client = clientOrOptions instanceof SentinelClient ? clientOrOptions : new SentinelClient(clientOrOptions);
|
|
411
|
+
return new SentinelTransport(client);
|
|
412
|
+
}
|
|
413
|
+
function enableSentinel(logger, clientOrOptions) {
|
|
414
|
+
const transport = createSentinelTransport(clientOrOptions);
|
|
415
|
+
logger.addTransport(transport);
|
|
416
|
+
return transport;
|
|
417
|
+
}
|
|
5
418
|
|
|
6
419
|
// ../sentinel/dist/index.js
|
|
7
420
|
import * as path from "path";
|
|
@@ -12,8 +425,6 @@ import * as path2 from "path";
|
|
|
12
425
|
import * as fs3 from "fs";
|
|
13
426
|
import * as path3 from "path";
|
|
14
427
|
import * as fs4 from "fs";
|
|
15
|
-
import * as path4 from "path";
|
|
16
|
-
import * as fs5 from "fs";
|
|
17
428
|
var DEFAULT_CONFIG = {
|
|
18
429
|
minScore: 30,
|
|
19
430
|
maxResults: 5,
|
|
@@ -501,136 +912,6 @@ var Sentinel = class {
|
|
|
501
912
|
return this.matcher;
|
|
502
913
|
}
|
|
503
914
|
};
|
|
504
|
-
var CONFIG_FILES = [".sentinel.yaml", ".sentinel.yml"];
|
|
505
|
-
function loadConfig(projectDir) {
|
|
506
|
-
for (const filename of CONFIG_FILES) {
|
|
507
|
-
const filePath = path2.join(projectDir, filename);
|
|
508
|
-
if (fs2.existsSync(filePath)) {
|
|
509
|
-
const content = fs2.readFileSync(filePath, "utf-8");
|
|
510
|
-
return parseSimpleYaml(content);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return null;
|
|
514
|
-
}
|
|
515
|
-
function writeConfig(projectDir, config) {
|
|
516
|
-
const filePath = path2.join(projectDir, ".sentinel.yaml");
|
|
517
|
-
const content = serializeSimpleYaml(config);
|
|
518
|
-
fs2.writeFileSync(filePath, content, "utf-8");
|
|
519
|
-
}
|
|
520
|
-
function parseSimpleYaml(content) {
|
|
521
|
-
const config = { version: "1.0", project: "" };
|
|
522
|
-
const lines = content.split("\n");
|
|
523
|
-
let currentSection = null;
|
|
524
|
-
let currentSubSection = null;
|
|
525
|
-
for (const line of lines) {
|
|
526
|
-
const trimmed = line.trimEnd();
|
|
527
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
528
|
-
const topMatch = trimmed.match(/^(\w+):\s*(.+)$/);
|
|
529
|
-
if (topMatch) {
|
|
530
|
-
const [, key, value] = topMatch;
|
|
531
|
-
if (key === "version") config.version = value.replace(/['"]/g, "");
|
|
532
|
-
else if (key === "project") config.project = value.replace(/['"]/g, "");
|
|
533
|
-
else if (key === "environment") config.environment = value.replace(/['"]/g, "");
|
|
534
|
-
currentSection = null;
|
|
535
|
-
currentSubSection = null;
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
const sectionMatch = trimmed.match(/^(\w+):$/);
|
|
539
|
-
if (sectionMatch) {
|
|
540
|
-
currentSection = sectionMatch[1];
|
|
541
|
-
currentSubSection = null;
|
|
542
|
-
if (currentSection === "symbols" && !config.symbols) {
|
|
543
|
-
config.symbols = {};
|
|
544
|
-
}
|
|
545
|
-
if (currentSection === "routes" && !config.routes) {
|
|
546
|
-
config.routes = {};
|
|
547
|
-
}
|
|
548
|
-
if (currentSection === "scrub" && !config.scrub) {
|
|
549
|
-
config.scrub = {};
|
|
550
|
-
}
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
const subMatch = trimmed.match(/^\s{2}(\w+):$/);
|
|
554
|
-
if (subMatch && currentSection) {
|
|
555
|
-
currentSubSection = subMatch[1];
|
|
556
|
-
if (currentSection === "symbols" && config.symbols) {
|
|
557
|
-
config.symbols[currentSubSection] = [];
|
|
558
|
-
}
|
|
559
|
-
if (currentSection === "scrub" && config.scrub) {
|
|
560
|
-
config.scrub[currentSubSection] = [];
|
|
561
|
-
}
|
|
562
|
-
continue;
|
|
563
|
-
}
|
|
564
|
-
const listMatch = trimmed.match(/^\s+-\s+(.+)$/);
|
|
565
|
-
if (listMatch && currentSection && currentSubSection) {
|
|
566
|
-
const value = listMatch[1].replace(/['"]/g, "");
|
|
567
|
-
if (currentSection === "symbols" && config.symbols) {
|
|
568
|
-
const arr = config.symbols[currentSubSection];
|
|
569
|
-
if (Array.isArray(arr)) arr.push(value);
|
|
570
|
-
}
|
|
571
|
-
if (currentSection === "scrub" && config.scrub) {
|
|
572
|
-
const arr = config.scrub[currentSubSection];
|
|
573
|
-
if (Array.isArray(arr)) arr.push(value);
|
|
574
|
-
}
|
|
575
|
-
continue;
|
|
576
|
-
}
|
|
577
|
-
const routeMatch = trimmed.match(/^\s+(['"]?\/[^'"]+['"]?):\s+['"]?([^'"]+)['"]?$/);
|
|
578
|
-
if (routeMatch && currentSection === "routes" && config.routes) {
|
|
579
|
-
const route = routeMatch[1].replace(/['"]/g, "");
|
|
580
|
-
config.routes[route] = routeMatch[2];
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
return config;
|
|
585
|
-
}
|
|
586
|
-
function serializeSimpleYaml(config) {
|
|
587
|
-
const lines = [];
|
|
588
|
-
lines.push(`# Sentinel Configuration`);
|
|
589
|
-
lines.push(`# Auto-generated \u2014 edit freely`);
|
|
590
|
-
lines.push("");
|
|
591
|
-
lines.push(`version: "${config.version}"`);
|
|
592
|
-
lines.push(`project: "${config.project}"`);
|
|
593
|
-
if (config.environment) {
|
|
594
|
-
lines.push(`environment: "${config.environment}"`);
|
|
595
|
-
}
|
|
596
|
-
if (config.symbols) {
|
|
597
|
-
lines.push("");
|
|
598
|
-
lines.push("symbols:");
|
|
599
|
-
for (const [key, values] of Object.entries(config.symbols)) {
|
|
600
|
-
if (values && values.length > 0) {
|
|
601
|
-
lines.push(` ${key}:`);
|
|
602
|
-
for (const v of values) {
|
|
603
|
-
lines.push(` - ${v}`);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
if (config.routes && Object.keys(config.routes).length > 0) {
|
|
609
|
-
lines.push("");
|
|
610
|
-
lines.push("routes:");
|
|
611
|
-
for (const [route, symbol] of Object.entries(config.routes)) {
|
|
612
|
-
lines.push(` "${route}": ${symbol}`);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
if (config.scrub) {
|
|
616
|
-
lines.push("");
|
|
617
|
-
lines.push("scrub:");
|
|
618
|
-
if (config.scrub.headers?.length) {
|
|
619
|
-
lines.push(" headers:");
|
|
620
|
-
for (const h of config.scrub.headers) {
|
|
621
|
-
lines.push(` - ${h}`);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
if (config.scrub.fields?.length) {
|
|
625
|
-
lines.push(" fields:");
|
|
626
|
-
for (const f of config.scrub.fields) {
|
|
627
|
-
lines.push(` - ${f}`);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
lines.push("");
|
|
632
|
-
return lines.join("\n");
|
|
633
|
-
}
|
|
634
915
|
var DIR_PATTERNS = [
|
|
635
916
|
{ dirs: ["services", "src/services"], prefix: "#", type: "components" },
|
|
636
917
|
{ dirs: ["routes", "src/routes", "api", "src/api"], prefix: "#", type: "components" },
|
|
@@ -665,13 +946,13 @@ function detectSymbols(projectDir) {
|
|
|
665
946
|
}
|
|
666
947
|
for (const pattern of DIR_PATTERNS) {
|
|
667
948
|
for (const dir of pattern.dirs) {
|
|
668
|
-
const fullPath =
|
|
669
|
-
if (!
|
|
949
|
+
const fullPath = path2.join(projectDir, dir);
|
|
950
|
+
if (!fs2.existsSync(fullPath)) continue;
|
|
670
951
|
const files = safeReaddir(fullPath);
|
|
671
952
|
for (const file of files) {
|
|
672
|
-
const ext =
|
|
953
|
+
const ext = path2.extname(file);
|
|
673
954
|
if (!CODE_EXTENSIONS.has(ext)) continue;
|
|
674
|
-
const name =
|
|
955
|
+
const name = path2.basename(file, ext);
|
|
675
956
|
if (name === "index" || name.endsWith(".test") || name.endsWith(".spec")) continue;
|
|
676
957
|
const symbol = `${pattern.prefix}${toKebabCase(name)}`;
|
|
677
958
|
if (!result[pattern.type].includes(symbol)) {
|
|
@@ -687,7 +968,7 @@ function generateConfig(projectDir) {
|
|
|
687
968
|
const detected = detectSymbols(projectDir);
|
|
688
969
|
return {
|
|
689
970
|
version: "1.0",
|
|
690
|
-
project:
|
|
971
|
+
project: path2.basename(projectDir),
|
|
691
972
|
symbols: {
|
|
692
973
|
components: detected.components.length > 0 ? detected.components : void 0,
|
|
693
974
|
gates: detected.gates.length > 0 ? detected.gates : void 0,
|
|
@@ -698,8 +979,8 @@ function generateConfig(projectDir) {
|
|
|
698
979
|
};
|
|
699
980
|
}
|
|
700
981
|
function readPurposeFiles(projectDir) {
|
|
701
|
-
const paradigmDir =
|
|
702
|
-
if (!
|
|
982
|
+
const paradigmDir = path2.join(projectDir, ".paradigm");
|
|
983
|
+
if (!fs2.existsSync(paradigmDir)) return null;
|
|
703
984
|
const result = {
|
|
704
985
|
components: [],
|
|
705
986
|
gates: [],
|
|
@@ -710,7 +991,7 @@ function readPurposeFiles(projectDir) {
|
|
|
710
991
|
const purposeFiles = findFiles(projectDir, ".purpose");
|
|
711
992
|
for (const file of purposeFiles) {
|
|
712
993
|
try {
|
|
713
|
-
const content =
|
|
994
|
+
const content = fs2.readFileSync(file, "utf-8");
|
|
714
995
|
extractPurposeSymbols(content, result);
|
|
715
996
|
} catch {
|
|
716
997
|
}
|
|
@@ -763,13 +1044,13 @@ function extractPurposeSymbols(content, result) {
|
|
|
763
1044
|
function scanRoutes(projectDir, result) {
|
|
764
1045
|
const routeDirs = ["routes", "src/routes", "api", "src/api"];
|
|
765
1046
|
for (const dir of routeDirs) {
|
|
766
|
-
const fullPath =
|
|
767
|
-
if (!
|
|
1047
|
+
const fullPath = path2.join(projectDir, dir);
|
|
1048
|
+
if (!fs2.existsSync(fullPath)) continue;
|
|
768
1049
|
const files = safeReaddir(fullPath);
|
|
769
1050
|
for (const file of files) {
|
|
770
|
-
const ext =
|
|
1051
|
+
const ext = path2.extname(file);
|
|
771
1052
|
if (!CODE_EXTENSIONS.has(ext)) continue;
|
|
772
|
-
const name =
|
|
1053
|
+
const name = path2.basename(file, ext);
|
|
773
1054
|
if (name === "index") continue;
|
|
774
1055
|
const routePrefix = `/api/${toKebabCase(name)}`;
|
|
775
1056
|
const component = `#${toKebabCase(name)}`;
|
|
@@ -782,10 +1063,10 @@ function toKebabCase(str) {
|
|
|
782
1063
|
}
|
|
783
1064
|
function safeReaddir(dir) {
|
|
784
1065
|
try {
|
|
785
|
-
return
|
|
786
|
-
const fullPath =
|
|
1066
|
+
return fs2.readdirSync(dir).filter((f) => {
|
|
1067
|
+
const fullPath = path2.join(dir, f);
|
|
787
1068
|
try {
|
|
788
|
-
return
|
|
1069
|
+
return fs2.statSync(fullPath).isFile();
|
|
789
1070
|
} catch {
|
|
790
1071
|
return false;
|
|
791
1072
|
}
|
|
@@ -799,23 +1080,30 @@ function findFiles(dir, filename, maxDepth = 4, depth = 0) {
|
|
|
799
1080
|
const results = [];
|
|
800
1081
|
const skipDirs = /* @__PURE__ */ new Set(["node_modules", "dist", ".git", "coverage", ".next", ".nuxt"]);
|
|
801
1082
|
try {
|
|
802
|
-
const entries =
|
|
1083
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
803
1084
|
for (const entry of entries) {
|
|
804
1085
|
if (entry.isFile() && entry.name === filename) {
|
|
805
|
-
results.push(
|
|
1086
|
+
results.push(path2.join(dir, entry.name));
|
|
806
1087
|
} else if (entry.isDirectory() && !skipDirs.has(entry.name)) {
|
|
807
|
-
results.push(...findFiles(
|
|
1088
|
+
results.push(...findFiles(path2.join(dir, entry.name), filename, maxDepth, depth + 1));
|
|
808
1089
|
}
|
|
809
1090
|
}
|
|
810
1091
|
} catch {
|
|
811
1092
|
}
|
|
812
1093
|
return results;
|
|
813
1094
|
}
|
|
814
|
-
var
|
|
1095
|
+
var DEFAULT_SIMILARITY_THRESHOLD = 0.6;
|
|
1096
|
+
var DECAY_HALF_LIFE_DAYS = 14;
|
|
815
1097
|
var IncidentGrouper = class {
|
|
816
|
-
constructor(storage) {
|
|
1098
|
+
constructor(storage, config) {
|
|
817
1099
|
this.storage = storage;
|
|
1100
|
+
this.similarityThreshold = config?.similarityThreshold ?? DEFAULT_SIMILARITY_THRESHOLD;
|
|
1101
|
+
this.decayHalfLifeDays = config?.decayHalfLifeDays ?? DECAY_HALF_LIFE_DAYS;
|
|
1102
|
+
this.useStackFingerprint = config?.useStackFingerprint ?? true;
|
|
818
1103
|
}
|
|
1104
|
+
similarityThreshold;
|
|
1105
|
+
decayHalfLifeDays;
|
|
1106
|
+
useStackFingerprint;
|
|
819
1107
|
/**
|
|
820
1108
|
* Try to find or create a group for an incident
|
|
821
1109
|
* Returns the group ID if grouped, null if no suitable group
|
|
@@ -861,7 +1149,7 @@ var IncidentGrouper = class {
|
|
|
861
1149
|
continue;
|
|
862
1150
|
}
|
|
863
1151
|
const score = this.calculateSimilarity(incident, candidate);
|
|
864
|
-
if (score >=
|
|
1152
|
+
if (score >= this.similarityThreshold) {
|
|
865
1153
|
similar.push({ incident: candidate, score });
|
|
866
1154
|
}
|
|
867
1155
|
}
|
|
@@ -882,7 +1170,7 @@ var IncidentGrouper = class {
|
|
|
882
1170
|
continue;
|
|
883
1171
|
}
|
|
884
1172
|
const similar = ungrouped.filter(
|
|
885
|
-
(other) => other.id !== incident.id && !processed.has(other.id) && this.calculateSimilarity(incident, other) >=
|
|
1173
|
+
(other) => other.id !== incident.id && !processed.has(other.id) && this.calculateSimilarity(incident, other) >= this.similarityThreshold
|
|
886
1174
|
);
|
|
887
1175
|
if (similar.length + 1 >= minSize) {
|
|
888
1176
|
const members = [incident, ...similar];
|
|
@@ -909,11 +1197,17 @@ var IncidentGrouper = class {
|
|
|
909
1197
|
}
|
|
910
1198
|
/**
|
|
911
1199
|
* Calculate similarity between two incidents (0-1)
|
|
1200
|
+
* Applies time-decay so older incidents contribute less, and optionally
|
|
1201
|
+
* uses stack trace fingerprinting for more accurate grouping.
|
|
912
1202
|
*/
|
|
913
1203
|
calculateSimilarity(a, b) {
|
|
914
1204
|
let score = 0;
|
|
915
1205
|
let maxScore = 0;
|
|
916
|
-
const
|
|
1206
|
+
const hasStacks = this.useStackFingerprint && a.error.stack && b.error.stack;
|
|
1207
|
+
const symbolWeight = hasStacks ? 0.45 : 0.6;
|
|
1208
|
+
const errorWeight = hasStacks ? 0.25 : 0.3;
|
|
1209
|
+
const envWeight = 0.1;
|
|
1210
|
+
const stackWeight = hasStacks ? 0.2 : 0;
|
|
917
1211
|
const symbolTypes = [
|
|
918
1212
|
"feature",
|
|
919
1213
|
"component",
|
|
@@ -933,19 +1227,55 @@ var IncidentGrouper = class {
|
|
|
933
1227
|
}
|
|
934
1228
|
}
|
|
935
1229
|
}
|
|
936
|
-
const errorWeight = 0.3;
|
|
937
1230
|
const errorSimilarity = this.stringSimilarity(
|
|
938
1231
|
a.error.message,
|
|
939
1232
|
b.error.message
|
|
940
1233
|
);
|
|
941
1234
|
score += errorWeight * errorSimilarity;
|
|
942
1235
|
maxScore += errorWeight;
|
|
943
|
-
const envWeight = 0.1;
|
|
944
1236
|
if (a.environment === b.environment) {
|
|
945
1237
|
score += envWeight;
|
|
946
1238
|
}
|
|
947
1239
|
maxScore += envWeight;
|
|
948
|
-
|
|
1240
|
+
if (hasStacks) {
|
|
1241
|
+
const aFingerprint = this.fingerprintStack(a.error.stack);
|
|
1242
|
+
const bFingerprint = this.fingerprintStack(b.error.stack);
|
|
1243
|
+
const stackSimilarity = this.compareFingerprints(aFingerprint, bFingerprint);
|
|
1244
|
+
score += stackWeight * stackSimilarity;
|
|
1245
|
+
maxScore += stackWeight;
|
|
1246
|
+
}
|
|
1247
|
+
const rawScore = maxScore > 0 ? score / maxScore : 0;
|
|
1248
|
+
const timeDelta = Math.abs(
|
|
1249
|
+
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
1250
|
+
);
|
|
1251
|
+
const daysDelta = timeDelta / (1e3 * 60 * 60 * 24);
|
|
1252
|
+
const decayFactor = Math.pow(0.5, daysDelta / this.decayHalfLifeDays);
|
|
1253
|
+
return rawScore * decayFactor;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Extract a fingerprint from a stack trace by normalizing frames.
|
|
1257
|
+
* Strips line numbers, column numbers, and absolute paths to capture
|
|
1258
|
+
* the structural signature of the call stack.
|
|
1259
|
+
*/
|
|
1260
|
+
fingerprintStack(stack) {
|
|
1261
|
+
return stack.split("\n").filter((line) => line.trim().startsWith("at ")).slice(0, 10).map((frame) => {
|
|
1262
|
+
return frame.trim().replace(/:\d+:\d+\)?$/, "").replace(/\(.*[/\\]/, "(").replace(/^\s*at\s+/, "");
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Compare two stack fingerprints (0-1 similarity)
|
|
1267
|
+
*/
|
|
1268
|
+
compareFingerprints(a, b) {
|
|
1269
|
+
if (a.length === 0 && b.length === 0) return 1;
|
|
1270
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
1271
|
+
let matches = 0;
|
|
1272
|
+
const maxLen = Math.max(a.length, b.length);
|
|
1273
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
1274
|
+
if (a[i] === b[i]) {
|
|
1275
|
+
matches++;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return matches / maxLen;
|
|
949
1279
|
}
|
|
950
1280
|
/**
|
|
951
1281
|
* Calculate string similarity using Levenshtein distance
|
|
@@ -1550,12 +1880,12 @@ var ContextEnricher = class {
|
|
|
1550
1880
|
* Find symbol in premise index
|
|
1551
1881
|
*/
|
|
1552
1882
|
findInSymbolIndex(symbol) {
|
|
1553
|
-
const indexPath =
|
|
1554
|
-
if (!
|
|
1883
|
+
const indexPath = path3.join(this.projectRoot, ".paradigm", "index.json");
|
|
1884
|
+
if (!fs3.existsSync(indexPath)) {
|
|
1555
1885
|
return null;
|
|
1556
1886
|
}
|
|
1557
1887
|
try {
|
|
1558
|
-
const indexContent =
|
|
1888
|
+
const indexContent = fs3.readFileSync(indexPath, "utf-8");
|
|
1559
1889
|
const index = JSON.parse(indexContent);
|
|
1560
1890
|
if (index.symbols && Array.isArray(index.symbols)) {
|
|
1561
1891
|
return index.symbols.find(
|
|
@@ -1573,8 +1903,8 @@ var ContextEnricher = class {
|
|
|
1573
1903
|
findInPurposeFiles(symbol) {
|
|
1574
1904
|
const searchPaths = this.getSearchPathsForSymbol(symbol);
|
|
1575
1905
|
for (const searchPath of searchPaths) {
|
|
1576
|
-
const fullPath =
|
|
1577
|
-
if (!
|
|
1906
|
+
const fullPath = path3.join(this.projectRoot, searchPath);
|
|
1907
|
+
if (!fs3.existsSync(fullPath)) {
|
|
1578
1908
|
continue;
|
|
1579
1909
|
}
|
|
1580
1910
|
const cached = this.purposeCache.get(fullPath);
|
|
@@ -1585,7 +1915,7 @@ var ContextEnricher = class {
|
|
|
1585
1915
|
continue;
|
|
1586
1916
|
}
|
|
1587
1917
|
try {
|
|
1588
|
-
const content =
|
|
1918
|
+
const content = fs3.readFileSync(fullPath, "utf-8");
|
|
1589
1919
|
const purpose = this.parsePurposeFile(content);
|
|
1590
1920
|
this.purposeCache.set(fullPath, purpose);
|
|
1591
1921
|
if (purpose.symbol === symbol) {
|
|
@@ -1615,11 +1945,11 @@ var ContextEnricher = class {
|
|
|
1615
1945
|
const prefix = symbol[0];
|
|
1616
1946
|
const dirs = prefixDirs[prefix] || [];
|
|
1617
1947
|
for (const dir of dirs) {
|
|
1618
|
-
paths.push(
|
|
1619
|
-
paths.push(
|
|
1948
|
+
paths.push(path3.join(dir, cleanSymbol, ".purpose"));
|
|
1949
|
+
paths.push(path3.join(dir, `${cleanSymbol}.purpose`));
|
|
1620
1950
|
}
|
|
1621
|
-
paths.push(
|
|
1622
|
-
paths.push(
|
|
1951
|
+
paths.push(path3.join(".paradigm", "purposes", `${cleanSymbol}.yaml`));
|
|
1952
|
+
paths.push(path3.join(".paradigm", "purposes", `${cleanSymbol}.json`));
|
|
1623
1953
|
return paths;
|
|
1624
1954
|
}
|
|
1625
1955
|
/**
|
|
@@ -1682,7 +2012,7 @@ var PatternSuggester = class {
|
|
|
1682
2012
|
},
|
|
1683
2013
|
resolution: {
|
|
1684
2014
|
description: incident.resolution?.notes || "Resolution approach TBD",
|
|
1685
|
-
strategy:
|
|
2015
|
+
strategy: this.inferStrategy([incident]),
|
|
1686
2016
|
priority: "medium"
|
|
1687
2017
|
},
|
|
1688
2018
|
source: "suggested",
|
|
@@ -1697,6 +2027,7 @@ var PatternSuggester = class {
|
|
|
1697
2027
|
suggestFromGroup(group) {
|
|
1698
2028
|
const baseId = `group-${group.id.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
|
|
1699
2029
|
const symbols = this.buildSymbolCriteria(group.commonSymbols);
|
|
2030
|
+
const groupIncidents = group.incidents.slice(0, 20).map((id) => this.storage.getIncident(id)).filter((i) => i != null);
|
|
1700
2031
|
const pattern = {
|
|
1701
2032
|
id: baseId,
|
|
1702
2033
|
name: group.name || `Pattern from group ${group.id}`,
|
|
@@ -1707,7 +2038,7 @@ var PatternSuggester = class {
|
|
|
1707
2038
|
},
|
|
1708
2039
|
resolution: {
|
|
1709
2040
|
description: "Resolution approach TBD based on grouped incidents",
|
|
1710
|
-
strategy: "fix-code",
|
|
2041
|
+
strategy: groupIncidents.length > 0 ? this.inferStrategy(groupIncidents) : "fix-code",
|
|
1711
2042
|
priority: this.getPriorityFromCount(group.count)
|
|
1712
2043
|
},
|
|
1713
2044
|
source: "suggested",
|
|
@@ -2006,21 +2337,38 @@ var PatternSuggester = class {
|
|
|
2006
2337
|
return false;
|
|
2007
2338
|
}
|
|
2008
2339
|
/**
|
|
2009
|
-
* Infer resolution strategy from
|
|
2340
|
+
* Infer resolution strategy from incident error patterns and context.
|
|
2341
|
+
* Uses keyword heuristics across all incident messages to pick the
|
|
2342
|
+
* most likely resolution approach.
|
|
2010
2343
|
*/
|
|
2011
2344
|
inferStrategy(incidents) {
|
|
2012
2345
|
const messages = incidents.map((i) => i.error.message.toLowerCase());
|
|
2013
|
-
|
|
2346
|
+
const hasKeyword = (keywords) => messages.some((m) => keywords.some((k) => m.includes(k)));
|
|
2347
|
+
if (hasKeyword(["revert", "rollback", "regression", "broke after deploy", "since deploy"])) {
|
|
2348
|
+
return "rollback";
|
|
2349
|
+
}
|
|
2350
|
+
if (hasKeyword(["config", "environment variable", "env var", "missing key", "secret", "credential"])) {
|
|
2351
|
+
return "config-change";
|
|
2352
|
+
}
|
|
2353
|
+
if (hasKeyword(["out of memory", "oom", "heap", "memory limit", "capacity", "too many connections", "pool exhausted"])) {
|
|
2354
|
+
return "scale-up";
|
|
2355
|
+
}
|
|
2356
|
+
if (hasKeyword(["timeout", "network", "econnrefused", "econnreset", "dns", "socket hang up"])) {
|
|
2014
2357
|
return "retry";
|
|
2015
2358
|
}
|
|
2016
|
-
if (
|
|
2017
|
-
|
|
2018
|
-
|
|
2359
|
+
if (hasKeyword(["unavailable", "service down", "circuit breaker", "fallback", "503", "502"])) {
|
|
2360
|
+
return "fallback";
|
|
2361
|
+
}
|
|
2362
|
+
if (hasKeyword(["validation", "invalid", "required", "constraint", "duplicate", "not found", "404"])) {
|
|
2019
2363
|
return "fix-data";
|
|
2020
2364
|
}
|
|
2021
|
-
if (
|
|
2365
|
+
if (hasKeyword(["permission", "forbidden", "403", "401", "unauthorized", "access denied"])) {
|
|
2022
2366
|
return "escalate";
|
|
2023
2367
|
}
|
|
2368
|
+
const uniqueTypes = new Set(incidents.map((i) => i.error.type).filter(Boolean));
|
|
2369
|
+
if (uniqueTypes.size > 2) {
|
|
2370
|
+
return "investigate";
|
|
2371
|
+
}
|
|
2024
2372
|
return "fix-code";
|
|
2025
2373
|
}
|
|
2026
2374
|
/**
|
|
@@ -2127,10 +2475,10 @@ var PatternImporter = class {
|
|
|
2127
2475
|
* Load patterns from a JSON file
|
|
2128
2476
|
*/
|
|
2129
2477
|
loadFromFile(filePath) {
|
|
2130
|
-
if (!
|
|
2478
|
+
if (!fs4.existsSync(filePath)) {
|
|
2131
2479
|
throw new Error(`File not found: ${filePath}`);
|
|
2132
2480
|
}
|
|
2133
|
-
const content =
|
|
2481
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2134
2482
|
const data = JSON.parse(content);
|
|
2135
2483
|
const validation = this.validate(data);
|
|
2136
2484
|
if (!validation.valid) {
|
|
@@ -2232,14 +2580,17 @@ var PatternImporter = class {
|
|
|
2232
2580
|
};
|
|
2233
2581
|
|
|
2234
2582
|
export {
|
|
2583
|
+
SentinelClient,
|
|
2584
|
+
createSentinelClient,
|
|
2585
|
+
SentinelTransport,
|
|
2586
|
+
createSentinelTransport,
|
|
2587
|
+
enableSentinel,
|
|
2235
2588
|
PatternMatcher,
|
|
2236
2589
|
loadUniversalPatterns,
|
|
2237
2590
|
loadParadigmPatterns,
|
|
2238
2591
|
loadAllSeedPatterns,
|
|
2239
2592
|
FlowTracker,
|
|
2240
2593
|
Sentinel,
|
|
2241
|
-
loadConfig,
|
|
2242
|
-
writeConfig,
|
|
2243
2594
|
detectSymbols,
|
|
2244
2595
|
generateConfig,
|
|
2245
2596
|
IncidentGrouper,
|