@rotorsoft/act 0.45.0 → 0.46.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/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +51 -1
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/internal/audit.d.ts +95 -0
- package/dist/@types/internal/audit.d.ts.map +1 -0
- package/dist/@types/internal/index.d.ts +1 -0
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/types/audit.d.ts +126 -0
- package/dist/@types/types/audit.d.ts.map +1 -0
- package/dist/@types/types/index.d.ts +1 -0
- package/dist/@types/types/index.d.ts.map +1 -1
- package/dist/{chunk-VMX7RPTC.js → chunk-TZWDSNSN.js} +1 -1
- package/dist/{chunk-VMX7RPTC.js.map → chunk-TZWDSNSN.js.map} +1 -1
- package/dist/{chunk-PGTC7VOC.js → chunk-VC6MSVC3.js} +2 -2
- package/dist/index.cjs +480 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +482 -39
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +9 -9
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +11 -11
- package/dist/test/index.js.map +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +1 -1
- package/package.json +1 -1
- /package/dist/{chunk-PGTC7VOC.js.map → chunk-VC6MSVC3.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
sleep,
|
|
20
20
|
store,
|
|
21
21
|
validate
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-VC6MSVC3.js";
|
|
23
23
|
import {
|
|
24
24
|
ActorSchema,
|
|
25
25
|
CausationEventSchema,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
TargetSchema,
|
|
37
37
|
ValidationError,
|
|
38
38
|
ZodEmpty
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-TZWDSNSN.js";
|
|
40
40
|
import "./chunk-5WRI5ZAA.js";
|
|
41
41
|
|
|
42
42
|
// src/signals.ts
|
|
@@ -60,6 +60,425 @@ process.once("unhandledRejection", async (arg) => {
|
|
|
60
60
|
// src/act.ts
|
|
61
61
|
import EventEmitter from "events";
|
|
62
62
|
|
|
63
|
+
// src/internal/event-versions.ts
|
|
64
|
+
var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
|
|
65
|
+
function parse(name) {
|
|
66
|
+
const m = name.match(VERSION_SUFFIX);
|
|
67
|
+
if (m) {
|
|
68
|
+
const v = Number.parseInt(m[2], 10);
|
|
69
|
+
if (v >= 2) return { base: m[1], version: v };
|
|
70
|
+
}
|
|
71
|
+
return { base: name, version: 1 };
|
|
72
|
+
}
|
|
73
|
+
function deprecatedEventNames(names) {
|
|
74
|
+
const groups = /* @__PURE__ */ new Map();
|
|
75
|
+
for (const name of names) {
|
|
76
|
+
const { base, version } = parse(name);
|
|
77
|
+
const list = groups.get(base);
|
|
78
|
+
if (list) list.push({ version, name });
|
|
79
|
+
else groups.set(base, [{ version, name }]);
|
|
80
|
+
}
|
|
81
|
+
const deprecated = /* @__PURE__ */ new Set();
|
|
82
|
+
for (const list of groups.values()) {
|
|
83
|
+
if (list.length < 2) continue;
|
|
84
|
+
list.sort((a, b) => b.version - a.version);
|
|
85
|
+
for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
|
|
86
|
+
}
|
|
87
|
+
return deprecated;
|
|
88
|
+
}
|
|
89
|
+
function currentVersionOf(deprecatedName, allNames) {
|
|
90
|
+
const target = parse(deprecatedName);
|
|
91
|
+
let highest;
|
|
92
|
+
for (const name of allNames) {
|
|
93
|
+
const { base, version } = parse(name);
|
|
94
|
+
if (base !== target.base) continue;
|
|
95
|
+
if (!highest || version > highest.version) highest = { version, name };
|
|
96
|
+
}
|
|
97
|
+
return highest && highest.version > target.version ? highest.name : void 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/internal/audit.ts
|
|
101
|
+
var DEFAULTS = {
|
|
102
|
+
idle_days: 90,
|
|
103
|
+
restart_min: 1e4,
|
|
104
|
+
stuck_minutes: 30,
|
|
105
|
+
deprecated_min: 0.1,
|
|
106
|
+
drift_min: 500,
|
|
107
|
+
near_block: 3
|
|
108
|
+
};
|
|
109
|
+
var ALL_CATEGORIES = [
|
|
110
|
+
"schema",
|
|
111
|
+
"close-candidate",
|
|
112
|
+
"restart-candidate",
|
|
113
|
+
"deprecated-load",
|
|
114
|
+
"reaction-health",
|
|
115
|
+
"snapshot-drift",
|
|
116
|
+
"routing-health",
|
|
117
|
+
"correlation-gaps",
|
|
118
|
+
"clock-anomalies"
|
|
119
|
+
];
|
|
120
|
+
async function* audit(deps, categories, options = {}) {
|
|
121
|
+
const requested = new Set(categories ?? [...ALL_CATEGORIES]);
|
|
122
|
+
const orderedCategories = ALL_CATEGORIES.filter((c) => requested.has(c));
|
|
123
|
+
const passes = orderedCategories.map(
|
|
124
|
+
(c) => PASS_FACTORIES[c](deps, options)
|
|
125
|
+
);
|
|
126
|
+
const needStats = passes.some((p) => p.onStat !== void 0);
|
|
127
|
+
const needStreams = passes.some((p) => p.onStream !== void 0);
|
|
128
|
+
const needEvents = passes.some((p) => p.onEvent !== void 0);
|
|
129
|
+
if (needStats) {
|
|
130
|
+
const stats = await deps.store().query_stats({}, { count: true, names: true });
|
|
131
|
+
for (const [stream, s] of stats) {
|
|
132
|
+
for (const p of passes) p.onStat?.(stream, s);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (needStreams) {
|
|
136
|
+
await deps.store().query_streams((pos) => {
|
|
137
|
+
for (const p of passes) p.onStream?.(pos);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (needEvents) {
|
|
141
|
+
await deps.store().query((event) => {
|
|
142
|
+
for (const p of passes) p.onEvent?.(event);
|
|
143
|
+
}, options.query);
|
|
144
|
+
}
|
|
145
|
+
for (const p of passes) await p.finalize?.(deps);
|
|
146
|
+
for (const p of passes) {
|
|
147
|
+
for (const f of p.drain()) yield f;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
var makeSchemaPass = (deps) => {
|
|
151
|
+
const findings = [];
|
|
152
|
+
return {
|
|
153
|
+
category: "schema",
|
|
154
|
+
onEvent(event) {
|
|
155
|
+
const name = String(event.name);
|
|
156
|
+
const state2 = deps.event_to_state.get(name);
|
|
157
|
+
if (!state2) {
|
|
158
|
+
if (name.startsWith("__")) return;
|
|
159
|
+
findings.push({
|
|
160
|
+
category: "schema",
|
|
161
|
+
stream: event.stream,
|
|
162
|
+
event_id: event.id,
|
|
163
|
+
name,
|
|
164
|
+
reason: "unknown_event_name"
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const schema = state2.events[name];
|
|
169
|
+
const parsed = schema.safeParse(event.data);
|
|
170
|
+
if (!parsed.success) {
|
|
171
|
+
findings.push({
|
|
172
|
+
category: "schema",
|
|
173
|
+
stream: event.stream,
|
|
174
|
+
event_id: event.id,
|
|
175
|
+
name,
|
|
176
|
+
reason: "schema_validation_failed",
|
|
177
|
+
zod_error: parsed.error
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
drain: () => findings
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
var makeDeprecatedLoadPass = (deps, options) => {
|
|
185
|
+
const share_min = options.thresholds?.deprecated_min ?? DEFAULTS.deprecated_min;
|
|
186
|
+
const totals = /* @__PURE__ */ new Map();
|
|
187
|
+
const perStream = /* @__PURE__ */ new Map();
|
|
188
|
+
return {
|
|
189
|
+
category: "deprecated-load",
|
|
190
|
+
onStat(stream, { names }) {
|
|
191
|
+
for (const [name, count] of Object.entries(names)) {
|
|
192
|
+
totals.set(name, (totals.get(name) ?? 0) + count);
|
|
193
|
+
let m = perStream.get(name);
|
|
194
|
+
if (!m) {
|
|
195
|
+
m = /* @__PURE__ */ new Map();
|
|
196
|
+
perStream.set(name, m);
|
|
197
|
+
}
|
|
198
|
+
m.set(stream, count);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
drain() {
|
|
202
|
+
const findings = [];
|
|
203
|
+
const grand = [...totals.values()].reduce((s, n) => s + n, 0);
|
|
204
|
+
if (grand === 0) return findings;
|
|
205
|
+
const deprecated = deprecatedEventNames(deps.known_events);
|
|
206
|
+
const sorted = [...deprecated].map((name) => ({ name, count: totals.get(name) ?? 0 })).sort((a, b) => b.count - a.count);
|
|
207
|
+
for (const { name, count } of sorted) {
|
|
208
|
+
if (count === 0) continue;
|
|
209
|
+
if (count / grand < share_min) continue;
|
|
210
|
+
const currentVersion = currentVersionOf(name, deps.known_events);
|
|
211
|
+
const topStreams = [...perStream.get(name).entries()].map(([stream, c]) => ({ stream, count: c })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
212
|
+
findings.push({
|
|
213
|
+
category: "deprecated-load",
|
|
214
|
+
name,
|
|
215
|
+
current_version: currentVersion,
|
|
216
|
+
total: count,
|
|
217
|
+
top_streams: topStreams
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return findings;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
var makeCloseCandidatePass = (deps, options) => {
|
|
225
|
+
const idle_days = options.thresholds?.idle_days ?? DEFAULTS.idle_days;
|
|
226
|
+
const terminal_events = new Set(options.thresholds?.terminal_events ?? []);
|
|
227
|
+
const idle_cutoff = Date.now() - idle_days * 24 * 60 * 60 * 1e3;
|
|
228
|
+
const findings = [];
|
|
229
|
+
return {
|
|
230
|
+
category: "close-candidate",
|
|
231
|
+
onStat(stream, { head }) {
|
|
232
|
+
const head_name = String(head.name);
|
|
233
|
+
if (head_name.startsWith("__")) return;
|
|
234
|
+
const head_time = head.created.getTime();
|
|
235
|
+
const is_idle = head_time < idle_cutoff;
|
|
236
|
+
const is_terminal = terminal_events.has(head_name);
|
|
237
|
+
if (!is_idle && !is_terminal) return;
|
|
238
|
+
findings.push({
|
|
239
|
+
category: "close-candidate",
|
|
240
|
+
stream,
|
|
241
|
+
last_event_at: head.created.toISOString(),
|
|
242
|
+
reason: is_terminal ? "terminal" : "idle",
|
|
243
|
+
idle_days: is_idle ? Math.floor((Date.now() - head_time) / (24 * 60 * 60 * 1e3)) : void 0,
|
|
244
|
+
restart_supported: restartIsSupported(deps, head_name)
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
drain: () => findings
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
var makeRestartCandidatePass = (deps, options) => {
|
|
251
|
+
const threshold = options.thresholds?.restart_min ?? DEFAULTS.restart_min;
|
|
252
|
+
const findings = [];
|
|
253
|
+
return {
|
|
254
|
+
category: "restart-candidate",
|
|
255
|
+
onStat(stream, { head, count, names }) {
|
|
256
|
+
if (count < threshold) return;
|
|
257
|
+
const head_name = String(head.name);
|
|
258
|
+
if (head_name.startsWith("__")) return;
|
|
259
|
+
if (!restartIsSupported(deps, head_name)) return;
|
|
260
|
+
findings.push({
|
|
261
|
+
category: "restart-candidate",
|
|
262
|
+
stream,
|
|
263
|
+
count,
|
|
264
|
+
// names map is sparse — `__snapshot__` key absent when the
|
|
265
|
+
// stream has never been snapshotted (a common case for the
|
|
266
|
+
// restart-candidate signal).
|
|
267
|
+
snaps: names["__snapshot__"] ?? 0
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
drain: () => findings
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
var makeReactionHealthPass = (_deps, options) => {
|
|
274
|
+
const near_block = options.thresholds?.near_block ?? DEFAULTS.near_block;
|
|
275
|
+
const stuck_minutes = options.thresholds?.stuck_minutes ?? DEFAULTS.stuck_minutes;
|
|
276
|
+
const stuck_cutoff = Date.now() - stuck_minutes * 60 * 1e3;
|
|
277
|
+
const findings = [];
|
|
278
|
+
return {
|
|
279
|
+
category: "reaction-health",
|
|
280
|
+
onStream(p) {
|
|
281
|
+
if (p.blocked) {
|
|
282
|
+
findings.push({
|
|
283
|
+
category: "reaction-health",
|
|
284
|
+
stream: p.stream,
|
|
285
|
+
status: "blocked",
|
|
286
|
+
retry: p.retry,
|
|
287
|
+
reason: p.error || "blocked without recorded error"
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (p.retry >= near_block) {
|
|
292
|
+
findings.push({
|
|
293
|
+
category: "reaction-health",
|
|
294
|
+
stream: p.stream,
|
|
295
|
+
status: "near-block",
|
|
296
|
+
retry: p.retry,
|
|
297
|
+
reason: `retry ${p.retry} \u2265 near-block threshold ${near_block}`
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (p.leased_by && p.leased_until && p.leased_until.getTime() < stuck_cutoff) {
|
|
302
|
+
const minutes = Math.floor(
|
|
303
|
+
(Date.now() - p.leased_until.getTime()) / (60 * 1e3)
|
|
304
|
+
);
|
|
305
|
+
findings.push({
|
|
306
|
+
category: "reaction-health",
|
|
307
|
+
stream: p.stream,
|
|
308
|
+
status: "stuck-backoff",
|
|
309
|
+
retry: p.retry,
|
|
310
|
+
reason: `lease expired ${minutes}m ago without release`
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
drain: () => findings
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
var makeSnapshotDriftPass = (deps, options) => {
|
|
318
|
+
const drift_min = options.thresholds?.drift_min ?? DEFAULTS.drift_min;
|
|
319
|
+
const candidates = [];
|
|
320
|
+
const findings = [];
|
|
321
|
+
return {
|
|
322
|
+
category: "snapshot-drift",
|
|
323
|
+
onStat(stream, { head, count, names }) {
|
|
324
|
+
if (!restartIsSupported(deps, String(head.name))) return;
|
|
325
|
+
if (count < drift_min) return;
|
|
326
|
+
candidates.push({
|
|
327
|
+
stream,
|
|
328
|
+
total: count,
|
|
329
|
+
snaps: names["__snapshot__"] ?? 0
|
|
330
|
+
});
|
|
331
|
+
},
|
|
332
|
+
async finalize(deps2) {
|
|
333
|
+
for (const { stream, total, snaps } of candidates) {
|
|
334
|
+
let events_since_snap = total;
|
|
335
|
+
let snap_at;
|
|
336
|
+
if (snaps > 0) {
|
|
337
|
+
const collected = [];
|
|
338
|
+
await deps2.store().query(
|
|
339
|
+
(e) => {
|
|
340
|
+
collected.push({ id: e.id });
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
stream,
|
|
344
|
+
stream_exact: true,
|
|
345
|
+
names: ["__snapshot__"],
|
|
346
|
+
backward: true,
|
|
347
|
+
limit: 1,
|
|
348
|
+
with_snaps: true
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
snap_at = collected[0].id;
|
|
352
|
+
let after = 0;
|
|
353
|
+
await deps2.store().query(
|
|
354
|
+
() => {
|
|
355
|
+
after++;
|
|
356
|
+
},
|
|
357
|
+
{ stream, stream_exact: true, after: snap_at }
|
|
358
|
+
);
|
|
359
|
+
events_since_snap = after;
|
|
360
|
+
}
|
|
361
|
+
if (events_since_snap < drift_min) continue;
|
|
362
|
+
findings.push({
|
|
363
|
+
category: "snapshot-drift",
|
|
364
|
+
stream,
|
|
365
|
+
events_since_snap,
|
|
366
|
+
snap_at
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
drain: () => findings
|
|
371
|
+
};
|
|
372
|
+
};
|
|
373
|
+
var makeRoutingHealthPass = (deps) => {
|
|
374
|
+
const findings = [];
|
|
375
|
+
const seenEventNames = /* @__PURE__ */ new Set();
|
|
376
|
+
return {
|
|
377
|
+
category: "routing-health",
|
|
378
|
+
onStream(p) {
|
|
379
|
+
if (!p.lane) return;
|
|
380
|
+
if (deps.declared_lanes.has(p.lane)) return;
|
|
381
|
+
findings.push({
|
|
382
|
+
category: "routing-health",
|
|
383
|
+
stream: p.stream,
|
|
384
|
+
reason: "unknown-lane",
|
|
385
|
+
lane: p.lane
|
|
386
|
+
});
|
|
387
|
+
},
|
|
388
|
+
onStat(_stream, { names }) {
|
|
389
|
+
for (const name of Object.keys(names)) {
|
|
390
|
+
seenEventNames.add(name);
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
finalize() {
|
|
394
|
+
for (const name of seenEventNames) {
|
|
395
|
+
if (name.startsWith("__")) continue;
|
|
396
|
+
if (deps.routed_events.has(name)) continue;
|
|
397
|
+
findings.push({
|
|
398
|
+
category: "routing-health",
|
|
399
|
+
stream: "*",
|
|
400
|
+
reason: "unrouted"
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return Promise.resolve();
|
|
404
|
+
},
|
|
405
|
+
drain: () => findings
|
|
406
|
+
};
|
|
407
|
+
};
|
|
408
|
+
var makeCorrelationGapsPass = () => {
|
|
409
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
410
|
+
const checks = [];
|
|
411
|
+
return {
|
|
412
|
+
category: "correlation-gaps",
|
|
413
|
+
onEvent(e) {
|
|
414
|
+
seenIds.add(e.id);
|
|
415
|
+
const causation = e.meta?.causation;
|
|
416
|
+
const parentId = causation?.event?.id;
|
|
417
|
+
if (parentId !== void 0) {
|
|
418
|
+
checks.push({ stream: e.stream, id: e.id, parentId });
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
drain() {
|
|
422
|
+
const findings = [];
|
|
423
|
+
for (const { stream, id, parentId } of checks) {
|
|
424
|
+
if (!seenIds.has(parentId)) {
|
|
425
|
+
findings.push({
|
|
426
|
+
category: "correlation-gaps",
|
|
427
|
+
stream,
|
|
428
|
+
event_id: id,
|
|
429
|
+
reason: "orphan-parent"
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return findings;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
};
|
|
437
|
+
var makeClockAnomaliesPass = () => {
|
|
438
|
+
const findings = [];
|
|
439
|
+
const lastPerStream = /* @__PURE__ */ new Map();
|
|
440
|
+
return {
|
|
441
|
+
category: "clock-anomalies",
|
|
442
|
+
onEvent(e) {
|
|
443
|
+
const created = e.created.getTime();
|
|
444
|
+
if (created > Date.now()) {
|
|
445
|
+
findings.push({
|
|
446
|
+
category: "clock-anomalies",
|
|
447
|
+
stream: e.stream,
|
|
448
|
+
event_id: e.id,
|
|
449
|
+
reason: "future-created"
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
const prev = lastPerStream.get(e.stream);
|
|
453
|
+
if (prev !== void 0 && created < prev) {
|
|
454
|
+
findings.push({
|
|
455
|
+
category: "clock-anomalies",
|
|
456
|
+
stream: e.stream,
|
|
457
|
+
event_id: e.id,
|
|
458
|
+
reason: "out-of-order"
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
lastPerStream.set(e.stream, created);
|
|
462
|
+
},
|
|
463
|
+
drain: () => findings
|
|
464
|
+
};
|
|
465
|
+
};
|
|
466
|
+
function restartIsSupported(deps, headEventName) {
|
|
467
|
+
const state2 = deps.event_to_state.get(headEventName);
|
|
468
|
+
return state2?.snap !== void 0;
|
|
469
|
+
}
|
|
470
|
+
var PASS_FACTORIES = {
|
|
471
|
+
schema: makeSchemaPass,
|
|
472
|
+
"deprecated-load": makeDeprecatedLoadPass,
|
|
473
|
+
"close-candidate": makeCloseCandidatePass,
|
|
474
|
+
"restart-candidate": makeRestartCandidatePass,
|
|
475
|
+
"reaction-health": makeReactionHealthPass,
|
|
476
|
+
"snapshot-drift": makeSnapshotDriftPass,
|
|
477
|
+
"routing-health": makeRoutingHealthPass,
|
|
478
|
+
"correlation-gaps": makeCorrelationGapsPass,
|
|
479
|
+
"clock-anomalies": makeClockAnomaliesPass
|
|
480
|
+
};
|
|
481
|
+
|
|
63
482
|
// src/internal/build-classify.ts
|
|
64
483
|
var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
65
484
|
function classifyRegistry(registry, states) {
|
|
@@ -1035,43 +1454,6 @@ var DrainController = class {
|
|
|
1035
1454
|
}
|
|
1036
1455
|
};
|
|
1037
1456
|
|
|
1038
|
-
// src/internal/event-versions.ts
|
|
1039
|
-
var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
|
|
1040
|
-
function parse(name) {
|
|
1041
|
-
const m = name.match(VERSION_SUFFIX);
|
|
1042
|
-
if (m) {
|
|
1043
|
-
const v = Number.parseInt(m[2], 10);
|
|
1044
|
-
if (v >= 2) return { base: m[1], version: v };
|
|
1045
|
-
}
|
|
1046
|
-
return { base: name, version: 1 };
|
|
1047
|
-
}
|
|
1048
|
-
function deprecatedEventNames(names) {
|
|
1049
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1050
|
-
for (const name of names) {
|
|
1051
|
-
const { base, version } = parse(name);
|
|
1052
|
-
const list = groups.get(base);
|
|
1053
|
-
if (list) list.push({ version, name });
|
|
1054
|
-
else groups.set(base, [{ version, name }]);
|
|
1055
|
-
}
|
|
1056
|
-
const deprecated = /* @__PURE__ */ new Set();
|
|
1057
|
-
for (const list of groups.values()) {
|
|
1058
|
-
if (list.length < 2) continue;
|
|
1059
|
-
list.sort((a, b) => b.version - a.version);
|
|
1060
|
-
for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
|
|
1061
|
-
}
|
|
1062
|
-
return deprecated;
|
|
1063
|
-
}
|
|
1064
|
-
function currentVersionOf(deprecatedName, allNames) {
|
|
1065
|
-
const target = parse(deprecatedName);
|
|
1066
|
-
let highest;
|
|
1067
|
-
for (const name of allNames) {
|
|
1068
|
-
const { base, version } = parse(name);
|
|
1069
|
-
if (base !== target.base) continue;
|
|
1070
|
-
if (!highest || version > highest.version) highest = { version, name };
|
|
1071
|
-
}
|
|
1072
|
-
return highest && highest.version > target.version ? highest.name : void 0;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
1457
|
// src/internal/merge.ts
|
|
1076
1458
|
import { ZodObject } from "zod";
|
|
1077
1459
|
function baseTypeName(zodType) {
|
|
@@ -1461,6 +1843,15 @@ var Act = class {
|
|
|
1461
1843
|
if (cfg?.cycleMs !== void 0) controller.start(cfg.cycleMs);
|
|
1462
1844
|
this._drain_controllers.set(name, controller);
|
|
1463
1845
|
}
|
|
1846
|
+
this._audit_deps = {
|
|
1847
|
+
store,
|
|
1848
|
+
logger: this._logger,
|
|
1849
|
+
event_to_state: eventToState,
|
|
1850
|
+
states: this._states,
|
|
1851
|
+
known_events: new Set(eventToState.keys()),
|
|
1852
|
+
declared_lanes: new Set(this._drain_controllers.keys()),
|
|
1853
|
+
routed_events: new Set(eventToLanes.keys())
|
|
1854
|
+
};
|
|
1464
1855
|
this._correlate = new CorrelateCycle(
|
|
1465
1856
|
this.registry,
|
|
1466
1857
|
staticTargets,
|
|
@@ -1552,6 +1943,14 @@ var Act = class {
|
|
|
1552
1943
|
* reactions target.
|
|
1553
1944
|
*/
|
|
1554
1945
|
_event_to_lanes;
|
|
1946
|
+
/**
|
|
1947
|
+
* Audit dependency bag (#723). Built once at construction; held as
|
|
1948
|
+
* an immutable snapshot of the registry state the audit module
|
|
1949
|
+
* needs. Lives in `internal/audit.ts` — this orchestrator never
|
|
1950
|
+
* carries audit logic, only the deps + a one-liner that hands them
|
|
1951
|
+
* over.
|
|
1952
|
+
*/
|
|
1953
|
+
_audit_deps;
|
|
1555
1954
|
/** Logger resolved at construction time (after user port configuration) */
|
|
1556
1955
|
_logger = log();
|
|
1557
1956
|
/** Wraps a public-method body so internal `store()`/`cache()` resolve to the
|
|
@@ -2168,6 +2567,50 @@ var Act = class {
|
|
|
2168
2567
|
return positions;
|
|
2169
2568
|
});
|
|
2170
2569
|
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Operator-driven store audit (#723).
|
|
2572
|
+
*
|
|
2573
|
+
* Walks the connected store and yields per-category findings —
|
|
2574
|
+
* each tagged with the remediation it suggests. Same operator-
|
|
2575
|
+
* driven category as `app.close()` / `app.reset()` /
|
|
2576
|
+
* `app.unblock()` / `app.blocked_streams()`: never auto-invoked by
|
|
2577
|
+
* the framework; the operator decides when to run it (CI gate,
|
|
2578
|
+
* scheduled job, ad-hoc forensics) and what to do with the
|
|
2579
|
+
* findings.
|
|
2580
|
+
*
|
|
2581
|
+
* Categories are independent — pass a subset to scope the work,
|
|
2582
|
+
* or omit to run everything:
|
|
2583
|
+
*
|
|
2584
|
+
* ```typescript
|
|
2585
|
+
* // Targeted: schema drift + deprecated-event load only
|
|
2586
|
+
* for await (const f of app.audit(["schema", "deprecated-load"], {
|
|
2587
|
+
* query: { created_after: lastScan },
|
|
2588
|
+
* thresholds: { deprecatedLoadShareMin: 0.10 },
|
|
2589
|
+
* })) {
|
|
2590
|
+
* await escalate(f);
|
|
2591
|
+
* }
|
|
2592
|
+
*
|
|
2593
|
+
* // Full audit, default thresholds
|
|
2594
|
+
* for await (const f of app.audit()) console.log(f);
|
|
2595
|
+
* ```
|
|
2596
|
+
*
|
|
2597
|
+
* Returns an `AsyncIterable` so callers can `break` early — the
|
|
2598
|
+
* underlying store paginations respect the iterator protocol and
|
|
2599
|
+
* stop cleanly. Each finding is emitted independently, so
|
|
2600
|
+
* pipelining into Slack / persistence / further analysis works
|
|
2601
|
+
* without buffering the full report in memory.
|
|
2602
|
+
*
|
|
2603
|
+
* Findings shape — see {@link AuditFinding}. The discriminated
|
|
2604
|
+
* union carries enough context for the operator to act on each
|
|
2605
|
+
* finding directly: stream id, event id, recommendation hints.
|
|
2606
|
+
*
|
|
2607
|
+
* @param categories - Subset of categories to run (default: all).
|
|
2608
|
+
* @param options - Query window + per-category thresholds.
|
|
2609
|
+
* @returns Async iterable of {@link AuditFinding}.
|
|
2610
|
+
*/
|
|
2611
|
+
audit(categories, options) {
|
|
2612
|
+
return audit(this._audit_deps, categories, options);
|
|
2613
|
+
}
|
|
2171
2614
|
/**
|
|
2172
2615
|
* Bulk-update scheduling priority for streams matching `filter`.
|
|
2173
2616
|
*
|