@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.cjs
CHANGED
|
@@ -1235,6 +1235,425 @@ process.once("unhandledRejection", async (arg) => {
|
|
|
1235
1235
|
// src/act.ts
|
|
1236
1236
|
var import_node_events = __toESM(require("events"), 1);
|
|
1237
1237
|
|
|
1238
|
+
// src/internal/event-versions.ts
|
|
1239
|
+
var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
|
|
1240
|
+
function parse(name) {
|
|
1241
|
+
const m = name.match(VERSION_SUFFIX);
|
|
1242
|
+
if (m) {
|
|
1243
|
+
const v = Number.parseInt(m[2], 10);
|
|
1244
|
+
if (v >= 2) return { base: m[1], version: v };
|
|
1245
|
+
}
|
|
1246
|
+
return { base: name, version: 1 };
|
|
1247
|
+
}
|
|
1248
|
+
function deprecatedEventNames(names) {
|
|
1249
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1250
|
+
for (const name of names) {
|
|
1251
|
+
const { base, version } = parse(name);
|
|
1252
|
+
const list = groups.get(base);
|
|
1253
|
+
if (list) list.push({ version, name });
|
|
1254
|
+
else groups.set(base, [{ version, name }]);
|
|
1255
|
+
}
|
|
1256
|
+
const deprecated = /* @__PURE__ */ new Set();
|
|
1257
|
+
for (const list of groups.values()) {
|
|
1258
|
+
if (list.length < 2) continue;
|
|
1259
|
+
list.sort((a, b) => b.version - a.version);
|
|
1260
|
+
for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
|
|
1261
|
+
}
|
|
1262
|
+
return deprecated;
|
|
1263
|
+
}
|
|
1264
|
+
function currentVersionOf(deprecatedName, allNames) {
|
|
1265
|
+
const target = parse(deprecatedName);
|
|
1266
|
+
let highest;
|
|
1267
|
+
for (const name of allNames) {
|
|
1268
|
+
const { base, version } = parse(name);
|
|
1269
|
+
if (base !== target.base) continue;
|
|
1270
|
+
if (!highest || version > highest.version) highest = { version, name };
|
|
1271
|
+
}
|
|
1272
|
+
return highest && highest.version > target.version ? highest.name : void 0;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/internal/audit.ts
|
|
1276
|
+
var DEFAULTS = {
|
|
1277
|
+
idle_days: 90,
|
|
1278
|
+
restart_min: 1e4,
|
|
1279
|
+
stuck_minutes: 30,
|
|
1280
|
+
deprecated_min: 0.1,
|
|
1281
|
+
drift_min: 500,
|
|
1282
|
+
near_block: 3
|
|
1283
|
+
};
|
|
1284
|
+
var ALL_CATEGORIES = [
|
|
1285
|
+
"schema",
|
|
1286
|
+
"close-candidate",
|
|
1287
|
+
"restart-candidate",
|
|
1288
|
+
"deprecated-load",
|
|
1289
|
+
"reaction-health",
|
|
1290
|
+
"snapshot-drift",
|
|
1291
|
+
"routing-health",
|
|
1292
|
+
"correlation-gaps",
|
|
1293
|
+
"clock-anomalies"
|
|
1294
|
+
];
|
|
1295
|
+
async function* audit(deps, categories, options = {}) {
|
|
1296
|
+
const requested = new Set(categories ?? [...ALL_CATEGORIES]);
|
|
1297
|
+
const orderedCategories = ALL_CATEGORIES.filter((c) => requested.has(c));
|
|
1298
|
+
const passes = orderedCategories.map(
|
|
1299
|
+
(c) => PASS_FACTORIES[c](deps, options)
|
|
1300
|
+
);
|
|
1301
|
+
const needStats = passes.some((p) => p.onStat !== void 0);
|
|
1302
|
+
const needStreams = passes.some((p) => p.onStream !== void 0);
|
|
1303
|
+
const needEvents = passes.some((p) => p.onEvent !== void 0);
|
|
1304
|
+
if (needStats) {
|
|
1305
|
+
const stats = await deps.store().query_stats({}, { count: true, names: true });
|
|
1306
|
+
for (const [stream, s] of stats) {
|
|
1307
|
+
for (const p of passes) p.onStat?.(stream, s);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (needStreams) {
|
|
1311
|
+
await deps.store().query_streams((pos) => {
|
|
1312
|
+
for (const p of passes) p.onStream?.(pos);
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
if (needEvents) {
|
|
1316
|
+
await deps.store().query((event) => {
|
|
1317
|
+
for (const p of passes) p.onEvent?.(event);
|
|
1318
|
+
}, options.query);
|
|
1319
|
+
}
|
|
1320
|
+
for (const p of passes) await p.finalize?.(deps);
|
|
1321
|
+
for (const p of passes) {
|
|
1322
|
+
for (const f of p.drain()) yield f;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
var makeSchemaPass = (deps) => {
|
|
1326
|
+
const findings = [];
|
|
1327
|
+
return {
|
|
1328
|
+
category: "schema",
|
|
1329
|
+
onEvent(event) {
|
|
1330
|
+
const name = String(event.name);
|
|
1331
|
+
const state2 = deps.event_to_state.get(name);
|
|
1332
|
+
if (!state2) {
|
|
1333
|
+
if (name.startsWith("__")) return;
|
|
1334
|
+
findings.push({
|
|
1335
|
+
category: "schema",
|
|
1336
|
+
stream: event.stream,
|
|
1337
|
+
event_id: event.id,
|
|
1338
|
+
name,
|
|
1339
|
+
reason: "unknown_event_name"
|
|
1340
|
+
});
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
const schema = state2.events[name];
|
|
1344
|
+
const parsed = schema.safeParse(event.data);
|
|
1345
|
+
if (!parsed.success) {
|
|
1346
|
+
findings.push({
|
|
1347
|
+
category: "schema",
|
|
1348
|
+
stream: event.stream,
|
|
1349
|
+
event_id: event.id,
|
|
1350
|
+
name,
|
|
1351
|
+
reason: "schema_validation_failed",
|
|
1352
|
+
zod_error: parsed.error
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
},
|
|
1356
|
+
drain: () => findings
|
|
1357
|
+
};
|
|
1358
|
+
};
|
|
1359
|
+
var makeDeprecatedLoadPass = (deps, options) => {
|
|
1360
|
+
const share_min = options.thresholds?.deprecated_min ?? DEFAULTS.deprecated_min;
|
|
1361
|
+
const totals = /* @__PURE__ */ new Map();
|
|
1362
|
+
const perStream = /* @__PURE__ */ new Map();
|
|
1363
|
+
return {
|
|
1364
|
+
category: "deprecated-load",
|
|
1365
|
+
onStat(stream, { names }) {
|
|
1366
|
+
for (const [name, count] of Object.entries(names)) {
|
|
1367
|
+
totals.set(name, (totals.get(name) ?? 0) + count);
|
|
1368
|
+
let m = perStream.get(name);
|
|
1369
|
+
if (!m) {
|
|
1370
|
+
m = /* @__PURE__ */ new Map();
|
|
1371
|
+
perStream.set(name, m);
|
|
1372
|
+
}
|
|
1373
|
+
m.set(stream, count);
|
|
1374
|
+
}
|
|
1375
|
+
},
|
|
1376
|
+
drain() {
|
|
1377
|
+
const findings = [];
|
|
1378
|
+
const grand = [...totals.values()].reduce((s, n) => s + n, 0);
|
|
1379
|
+
if (grand === 0) return findings;
|
|
1380
|
+
const deprecated = deprecatedEventNames(deps.known_events);
|
|
1381
|
+
const sorted = [...deprecated].map((name) => ({ name, count: totals.get(name) ?? 0 })).sort((a, b) => b.count - a.count);
|
|
1382
|
+
for (const { name, count } of sorted) {
|
|
1383
|
+
if (count === 0) continue;
|
|
1384
|
+
if (count / grand < share_min) continue;
|
|
1385
|
+
const currentVersion = currentVersionOf(name, deps.known_events);
|
|
1386
|
+
const topStreams = [...perStream.get(name).entries()].map(([stream, c]) => ({ stream, count: c })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
1387
|
+
findings.push({
|
|
1388
|
+
category: "deprecated-load",
|
|
1389
|
+
name,
|
|
1390
|
+
current_version: currentVersion,
|
|
1391
|
+
total: count,
|
|
1392
|
+
top_streams: topStreams
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
return findings;
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
};
|
|
1399
|
+
var makeCloseCandidatePass = (deps, options) => {
|
|
1400
|
+
const idle_days = options.thresholds?.idle_days ?? DEFAULTS.idle_days;
|
|
1401
|
+
const terminal_events = new Set(options.thresholds?.terminal_events ?? []);
|
|
1402
|
+
const idle_cutoff = Date.now() - idle_days * 24 * 60 * 60 * 1e3;
|
|
1403
|
+
const findings = [];
|
|
1404
|
+
return {
|
|
1405
|
+
category: "close-candidate",
|
|
1406
|
+
onStat(stream, { head }) {
|
|
1407
|
+
const head_name = String(head.name);
|
|
1408
|
+
if (head_name.startsWith("__")) return;
|
|
1409
|
+
const head_time = head.created.getTime();
|
|
1410
|
+
const is_idle = head_time < idle_cutoff;
|
|
1411
|
+
const is_terminal = terminal_events.has(head_name);
|
|
1412
|
+
if (!is_idle && !is_terminal) return;
|
|
1413
|
+
findings.push({
|
|
1414
|
+
category: "close-candidate",
|
|
1415
|
+
stream,
|
|
1416
|
+
last_event_at: head.created.toISOString(),
|
|
1417
|
+
reason: is_terminal ? "terminal" : "idle",
|
|
1418
|
+
idle_days: is_idle ? Math.floor((Date.now() - head_time) / (24 * 60 * 60 * 1e3)) : void 0,
|
|
1419
|
+
restart_supported: restartIsSupported(deps, head_name)
|
|
1420
|
+
});
|
|
1421
|
+
},
|
|
1422
|
+
drain: () => findings
|
|
1423
|
+
};
|
|
1424
|
+
};
|
|
1425
|
+
var makeRestartCandidatePass = (deps, options) => {
|
|
1426
|
+
const threshold = options.thresholds?.restart_min ?? DEFAULTS.restart_min;
|
|
1427
|
+
const findings = [];
|
|
1428
|
+
return {
|
|
1429
|
+
category: "restart-candidate",
|
|
1430
|
+
onStat(stream, { head, count, names }) {
|
|
1431
|
+
if (count < threshold) return;
|
|
1432
|
+
const head_name = String(head.name);
|
|
1433
|
+
if (head_name.startsWith("__")) return;
|
|
1434
|
+
if (!restartIsSupported(deps, head_name)) return;
|
|
1435
|
+
findings.push({
|
|
1436
|
+
category: "restart-candidate",
|
|
1437
|
+
stream,
|
|
1438
|
+
count,
|
|
1439
|
+
// names map is sparse — `__snapshot__` key absent when the
|
|
1440
|
+
// stream has never been snapshotted (a common case for the
|
|
1441
|
+
// restart-candidate signal).
|
|
1442
|
+
snaps: names["__snapshot__"] ?? 0
|
|
1443
|
+
});
|
|
1444
|
+
},
|
|
1445
|
+
drain: () => findings
|
|
1446
|
+
};
|
|
1447
|
+
};
|
|
1448
|
+
var makeReactionHealthPass = (_deps, options) => {
|
|
1449
|
+
const near_block = options.thresholds?.near_block ?? DEFAULTS.near_block;
|
|
1450
|
+
const stuck_minutes = options.thresholds?.stuck_minutes ?? DEFAULTS.stuck_minutes;
|
|
1451
|
+
const stuck_cutoff = Date.now() - stuck_minutes * 60 * 1e3;
|
|
1452
|
+
const findings = [];
|
|
1453
|
+
return {
|
|
1454
|
+
category: "reaction-health",
|
|
1455
|
+
onStream(p) {
|
|
1456
|
+
if (p.blocked) {
|
|
1457
|
+
findings.push({
|
|
1458
|
+
category: "reaction-health",
|
|
1459
|
+
stream: p.stream,
|
|
1460
|
+
status: "blocked",
|
|
1461
|
+
retry: p.retry,
|
|
1462
|
+
reason: p.error || "blocked without recorded error"
|
|
1463
|
+
});
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
if (p.retry >= near_block) {
|
|
1467
|
+
findings.push({
|
|
1468
|
+
category: "reaction-health",
|
|
1469
|
+
stream: p.stream,
|
|
1470
|
+
status: "near-block",
|
|
1471
|
+
retry: p.retry,
|
|
1472
|
+
reason: `retry ${p.retry} \u2265 near-block threshold ${near_block}`
|
|
1473
|
+
});
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (p.leased_by && p.leased_until && p.leased_until.getTime() < stuck_cutoff) {
|
|
1477
|
+
const minutes = Math.floor(
|
|
1478
|
+
(Date.now() - p.leased_until.getTime()) / (60 * 1e3)
|
|
1479
|
+
);
|
|
1480
|
+
findings.push({
|
|
1481
|
+
category: "reaction-health",
|
|
1482
|
+
stream: p.stream,
|
|
1483
|
+
status: "stuck-backoff",
|
|
1484
|
+
retry: p.retry,
|
|
1485
|
+
reason: `lease expired ${minutes}m ago without release`
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
},
|
|
1489
|
+
drain: () => findings
|
|
1490
|
+
};
|
|
1491
|
+
};
|
|
1492
|
+
var makeSnapshotDriftPass = (deps, options) => {
|
|
1493
|
+
const drift_min = options.thresholds?.drift_min ?? DEFAULTS.drift_min;
|
|
1494
|
+
const candidates = [];
|
|
1495
|
+
const findings = [];
|
|
1496
|
+
return {
|
|
1497
|
+
category: "snapshot-drift",
|
|
1498
|
+
onStat(stream, { head, count, names }) {
|
|
1499
|
+
if (!restartIsSupported(deps, String(head.name))) return;
|
|
1500
|
+
if (count < drift_min) return;
|
|
1501
|
+
candidates.push({
|
|
1502
|
+
stream,
|
|
1503
|
+
total: count,
|
|
1504
|
+
snaps: names["__snapshot__"] ?? 0
|
|
1505
|
+
});
|
|
1506
|
+
},
|
|
1507
|
+
async finalize(deps2) {
|
|
1508
|
+
for (const { stream, total, snaps } of candidates) {
|
|
1509
|
+
let events_since_snap = total;
|
|
1510
|
+
let snap_at;
|
|
1511
|
+
if (snaps > 0) {
|
|
1512
|
+
const collected = [];
|
|
1513
|
+
await deps2.store().query(
|
|
1514
|
+
(e) => {
|
|
1515
|
+
collected.push({ id: e.id });
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
stream,
|
|
1519
|
+
stream_exact: true,
|
|
1520
|
+
names: ["__snapshot__"],
|
|
1521
|
+
backward: true,
|
|
1522
|
+
limit: 1,
|
|
1523
|
+
with_snaps: true
|
|
1524
|
+
}
|
|
1525
|
+
);
|
|
1526
|
+
snap_at = collected[0].id;
|
|
1527
|
+
let after = 0;
|
|
1528
|
+
await deps2.store().query(
|
|
1529
|
+
() => {
|
|
1530
|
+
after++;
|
|
1531
|
+
},
|
|
1532
|
+
{ stream, stream_exact: true, after: snap_at }
|
|
1533
|
+
);
|
|
1534
|
+
events_since_snap = after;
|
|
1535
|
+
}
|
|
1536
|
+
if (events_since_snap < drift_min) continue;
|
|
1537
|
+
findings.push({
|
|
1538
|
+
category: "snapshot-drift",
|
|
1539
|
+
stream,
|
|
1540
|
+
events_since_snap,
|
|
1541
|
+
snap_at
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
drain: () => findings
|
|
1546
|
+
};
|
|
1547
|
+
};
|
|
1548
|
+
var makeRoutingHealthPass = (deps) => {
|
|
1549
|
+
const findings = [];
|
|
1550
|
+
const seenEventNames = /* @__PURE__ */ new Set();
|
|
1551
|
+
return {
|
|
1552
|
+
category: "routing-health",
|
|
1553
|
+
onStream(p) {
|
|
1554
|
+
if (!p.lane) return;
|
|
1555
|
+
if (deps.declared_lanes.has(p.lane)) return;
|
|
1556
|
+
findings.push({
|
|
1557
|
+
category: "routing-health",
|
|
1558
|
+
stream: p.stream,
|
|
1559
|
+
reason: "unknown-lane",
|
|
1560
|
+
lane: p.lane
|
|
1561
|
+
});
|
|
1562
|
+
},
|
|
1563
|
+
onStat(_stream, { names }) {
|
|
1564
|
+
for (const name of Object.keys(names)) {
|
|
1565
|
+
seenEventNames.add(name);
|
|
1566
|
+
}
|
|
1567
|
+
},
|
|
1568
|
+
finalize() {
|
|
1569
|
+
for (const name of seenEventNames) {
|
|
1570
|
+
if (name.startsWith("__")) continue;
|
|
1571
|
+
if (deps.routed_events.has(name)) continue;
|
|
1572
|
+
findings.push({
|
|
1573
|
+
category: "routing-health",
|
|
1574
|
+
stream: "*",
|
|
1575
|
+
reason: "unrouted"
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
return Promise.resolve();
|
|
1579
|
+
},
|
|
1580
|
+
drain: () => findings
|
|
1581
|
+
};
|
|
1582
|
+
};
|
|
1583
|
+
var makeCorrelationGapsPass = () => {
|
|
1584
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1585
|
+
const checks = [];
|
|
1586
|
+
return {
|
|
1587
|
+
category: "correlation-gaps",
|
|
1588
|
+
onEvent(e) {
|
|
1589
|
+
seenIds.add(e.id);
|
|
1590
|
+
const causation = e.meta?.causation;
|
|
1591
|
+
const parentId = causation?.event?.id;
|
|
1592
|
+
if (parentId !== void 0) {
|
|
1593
|
+
checks.push({ stream: e.stream, id: e.id, parentId });
|
|
1594
|
+
}
|
|
1595
|
+
},
|
|
1596
|
+
drain() {
|
|
1597
|
+
const findings = [];
|
|
1598
|
+
for (const { stream, id, parentId } of checks) {
|
|
1599
|
+
if (!seenIds.has(parentId)) {
|
|
1600
|
+
findings.push({
|
|
1601
|
+
category: "correlation-gaps",
|
|
1602
|
+
stream,
|
|
1603
|
+
event_id: id,
|
|
1604
|
+
reason: "orphan-parent"
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return findings;
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
};
|
|
1612
|
+
var makeClockAnomaliesPass = () => {
|
|
1613
|
+
const findings = [];
|
|
1614
|
+
const lastPerStream = /* @__PURE__ */ new Map();
|
|
1615
|
+
return {
|
|
1616
|
+
category: "clock-anomalies",
|
|
1617
|
+
onEvent(e) {
|
|
1618
|
+
const created = e.created.getTime();
|
|
1619
|
+
if (created > Date.now()) {
|
|
1620
|
+
findings.push({
|
|
1621
|
+
category: "clock-anomalies",
|
|
1622
|
+
stream: e.stream,
|
|
1623
|
+
event_id: e.id,
|
|
1624
|
+
reason: "future-created"
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
const prev = lastPerStream.get(e.stream);
|
|
1628
|
+
if (prev !== void 0 && created < prev) {
|
|
1629
|
+
findings.push({
|
|
1630
|
+
category: "clock-anomalies",
|
|
1631
|
+
stream: e.stream,
|
|
1632
|
+
event_id: e.id,
|
|
1633
|
+
reason: "out-of-order"
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
lastPerStream.set(e.stream, created);
|
|
1637
|
+
},
|
|
1638
|
+
drain: () => findings
|
|
1639
|
+
};
|
|
1640
|
+
};
|
|
1641
|
+
function restartIsSupported(deps, headEventName) {
|
|
1642
|
+
const state2 = deps.event_to_state.get(headEventName);
|
|
1643
|
+
return state2?.snap !== void 0;
|
|
1644
|
+
}
|
|
1645
|
+
var PASS_FACTORIES = {
|
|
1646
|
+
schema: makeSchemaPass,
|
|
1647
|
+
"deprecated-load": makeDeprecatedLoadPass,
|
|
1648
|
+
"close-candidate": makeCloseCandidatePass,
|
|
1649
|
+
"restart-candidate": makeRestartCandidatePass,
|
|
1650
|
+
"reaction-health": makeReactionHealthPass,
|
|
1651
|
+
"snapshot-drift": makeSnapshotDriftPass,
|
|
1652
|
+
"routing-health": makeRoutingHealthPass,
|
|
1653
|
+
"correlation-gaps": makeCorrelationGapsPass,
|
|
1654
|
+
"clock-anomalies": makeClockAnomaliesPass
|
|
1655
|
+
};
|
|
1656
|
+
|
|
1238
1657
|
// src/internal/build-classify.ts
|
|
1239
1658
|
var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
1240
1659
|
function classifyRegistry(registry, states) {
|
|
@@ -2210,43 +2629,6 @@ var DrainController = class {
|
|
|
2210
2629
|
}
|
|
2211
2630
|
};
|
|
2212
2631
|
|
|
2213
|
-
// src/internal/event-versions.ts
|
|
2214
|
-
var VERSION_SUFFIX = /^(.+?)_v(\d+)$/;
|
|
2215
|
-
function parse(name) {
|
|
2216
|
-
const m = name.match(VERSION_SUFFIX);
|
|
2217
|
-
if (m) {
|
|
2218
|
-
const v = Number.parseInt(m[2], 10);
|
|
2219
|
-
if (v >= 2) return { base: m[1], version: v };
|
|
2220
|
-
}
|
|
2221
|
-
return { base: name, version: 1 };
|
|
2222
|
-
}
|
|
2223
|
-
function deprecatedEventNames(names) {
|
|
2224
|
-
const groups = /* @__PURE__ */ new Map();
|
|
2225
|
-
for (const name of names) {
|
|
2226
|
-
const { base, version } = parse(name);
|
|
2227
|
-
const list = groups.get(base);
|
|
2228
|
-
if (list) list.push({ version, name });
|
|
2229
|
-
else groups.set(base, [{ version, name }]);
|
|
2230
|
-
}
|
|
2231
|
-
const deprecated = /* @__PURE__ */ new Set();
|
|
2232
|
-
for (const list of groups.values()) {
|
|
2233
|
-
if (list.length < 2) continue;
|
|
2234
|
-
list.sort((a, b) => b.version - a.version);
|
|
2235
|
-
for (let i = 1; i < list.length; i++) deprecated.add(list[i].name);
|
|
2236
|
-
}
|
|
2237
|
-
return deprecated;
|
|
2238
|
-
}
|
|
2239
|
-
function currentVersionOf(deprecatedName, allNames) {
|
|
2240
|
-
const target = parse(deprecatedName);
|
|
2241
|
-
let highest;
|
|
2242
|
-
for (const name of allNames) {
|
|
2243
|
-
const { base, version } = parse(name);
|
|
2244
|
-
if (base !== target.base) continue;
|
|
2245
|
-
if (!highest || version > highest.version) highest = { version, name };
|
|
2246
|
-
}
|
|
2247
|
-
return highest && highest.version > target.version ? highest.name : void 0;
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
2632
|
// src/internal/merge.ts
|
|
2251
2633
|
var import_zod4 = require("zod");
|
|
2252
2634
|
function baseTypeName(zodType) {
|
|
@@ -2636,6 +3018,15 @@ var Act = class {
|
|
|
2636
3018
|
if (cfg?.cycleMs !== void 0) controller.start(cfg.cycleMs);
|
|
2637
3019
|
this._drain_controllers.set(name, controller);
|
|
2638
3020
|
}
|
|
3021
|
+
this._audit_deps = {
|
|
3022
|
+
store: store2,
|
|
3023
|
+
logger: this._logger,
|
|
3024
|
+
event_to_state: eventToState,
|
|
3025
|
+
states: this._states,
|
|
3026
|
+
known_events: new Set(eventToState.keys()),
|
|
3027
|
+
declared_lanes: new Set(this._drain_controllers.keys()),
|
|
3028
|
+
routed_events: new Set(eventToLanes.keys())
|
|
3029
|
+
};
|
|
2639
3030
|
this._correlate = new CorrelateCycle(
|
|
2640
3031
|
this.registry,
|
|
2641
3032
|
staticTargets,
|
|
@@ -2727,6 +3118,14 @@ var Act = class {
|
|
|
2727
3118
|
* reactions target.
|
|
2728
3119
|
*/
|
|
2729
3120
|
_event_to_lanes;
|
|
3121
|
+
/**
|
|
3122
|
+
* Audit dependency bag (#723). Built once at construction; held as
|
|
3123
|
+
* an immutable snapshot of the registry state the audit module
|
|
3124
|
+
* needs. Lives in `internal/audit.ts` — this orchestrator never
|
|
3125
|
+
* carries audit logic, only the deps + a one-liner that hands them
|
|
3126
|
+
* over.
|
|
3127
|
+
*/
|
|
3128
|
+
_audit_deps;
|
|
2730
3129
|
/** Logger resolved at construction time (after user port configuration) */
|
|
2731
3130
|
_logger = log();
|
|
2732
3131
|
/** Wraps a public-method body so internal `store()`/`cache()` resolve to the
|
|
@@ -3343,6 +3742,50 @@ var Act = class {
|
|
|
3343
3742
|
return positions;
|
|
3344
3743
|
});
|
|
3345
3744
|
}
|
|
3745
|
+
/**
|
|
3746
|
+
* Operator-driven store audit (#723).
|
|
3747
|
+
*
|
|
3748
|
+
* Walks the connected store and yields per-category findings —
|
|
3749
|
+
* each tagged with the remediation it suggests. Same operator-
|
|
3750
|
+
* driven category as `app.close()` / `app.reset()` /
|
|
3751
|
+
* `app.unblock()` / `app.blocked_streams()`: never auto-invoked by
|
|
3752
|
+
* the framework; the operator decides when to run it (CI gate,
|
|
3753
|
+
* scheduled job, ad-hoc forensics) and what to do with the
|
|
3754
|
+
* findings.
|
|
3755
|
+
*
|
|
3756
|
+
* Categories are independent — pass a subset to scope the work,
|
|
3757
|
+
* or omit to run everything:
|
|
3758
|
+
*
|
|
3759
|
+
* ```typescript
|
|
3760
|
+
* // Targeted: schema drift + deprecated-event load only
|
|
3761
|
+
* for await (const f of app.audit(["schema", "deprecated-load"], {
|
|
3762
|
+
* query: { created_after: lastScan },
|
|
3763
|
+
* thresholds: { deprecatedLoadShareMin: 0.10 },
|
|
3764
|
+
* })) {
|
|
3765
|
+
* await escalate(f);
|
|
3766
|
+
* }
|
|
3767
|
+
*
|
|
3768
|
+
* // Full audit, default thresholds
|
|
3769
|
+
* for await (const f of app.audit()) console.log(f);
|
|
3770
|
+
* ```
|
|
3771
|
+
*
|
|
3772
|
+
* Returns an `AsyncIterable` so callers can `break` early — the
|
|
3773
|
+
* underlying store paginations respect the iterator protocol and
|
|
3774
|
+
* stop cleanly. Each finding is emitted independently, so
|
|
3775
|
+
* pipelining into Slack / persistence / further analysis works
|
|
3776
|
+
* without buffering the full report in memory.
|
|
3777
|
+
*
|
|
3778
|
+
* Findings shape — see {@link AuditFinding}. The discriminated
|
|
3779
|
+
* union carries enough context for the operator to act on each
|
|
3780
|
+
* finding directly: stream id, event id, recommendation hints.
|
|
3781
|
+
*
|
|
3782
|
+
* @param categories - Subset of categories to run (default: all).
|
|
3783
|
+
* @param options - Query window + per-category thresholds.
|
|
3784
|
+
* @returns Async iterable of {@link AuditFinding}.
|
|
3785
|
+
*/
|
|
3786
|
+
audit(categories, options) {
|
|
3787
|
+
return audit(this._audit_deps, categories, options);
|
|
3788
|
+
}
|
|
3346
3789
|
/**
|
|
3347
3790
|
* Bulk-update scheduling priority for streams matching `filter`.
|
|
3348
3791
|
*
|