agentxchain 2.99.0 → 2.100.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/package.json
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const repoRoot = resolve(__dirname, '..', '..');
|
|
9
|
+
const releasesDir = join(repoRoot, 'website-v2', 'docs', 'releases');
|
|
10
|
+
|
|
11
|
+
function parseReleaseFile(file) {
|
|
12
|
+
const match = /^v(\d+)-(\d+)-(\d+)\.mdx$/.exec(file);
|
|
13
|
+
if (!match) {
|
|
14
|
+
throw new Error(`Unparseable release note filename: ${file}`);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
file,
|
|
18
|
+
version: match.slice(1).map(Number),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function compareDesc(left, right) {
|
|
23
|
+
for (let i = 0; i < 3; i += 1) {
|
|
24
|
+
if (left.version[i] !== right.version[i]) {
|
|
25
|
+
return right.version[i] - left.version[i];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function updateSidebarPosition(content, nextPosition) {
|
|
32
|
+
if (!content.startsWith('---\n')) {
|
|
33
|
+
throw new Error('Release note is missing YAML frontmatter opening delimiter');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const frontmatterEnd = content.indexOf('\n---\n', 4);
|
|
37
|
+
if (frontmatterEnd === -1) {
|
|
38
|
+
throw new Error('Release note is missing YAML frontmatter closing delimiter');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const frontmatter = content.slice(0, frontmatterEnd + 5);
|
|
42
|
+
const body = content.slice(frontmatterEnd + 5);
|
|
43
|
+
const expectedLine = `sidebar_position: ${nextPosition}`;
|
|
44
|
+
|
|
45
|
+
let updatedFrontmatter;
|
|
46
|
+
if (/^sidebar_position:\s*\d+\s*$/m.test(frontmatter)) {
|
|
47
|
+
updatedFrontmatter = frontmatter.replace(/^sidebar_position:\s*\d+\s*$/m, expectedLine);
|
|
48
|
+
} else {
|
|
49
|
+
updatedFrontmatter = frontmatter.replace(/^---\n/, `---\n${expectedLine}\n`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const updated = updatedFrontmatter + body;
|
|
53
|
+
return {
|
|
54
|
+
updated,
|
|
55
|
+
changed: updated !== content,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const releaseFiles = readdirSync(releasesDir)
|
|
60
|
+
.filter((file) => /^v\d+-\d+-\d+\.mdx$/.test(file))
|
|
61
|
+
.map(parseReleaseFile)
|
|
62
|
+
.sort(compareDesc);
|
|
63
|
+
|
|
64
|
+
if (releaseFiles.length === 0) {
|
|
65
|
+
throw new Error(`No release notes found in ${releasesDir}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let changedCount = 0;
|
|
69
|
+
for (const [index, release] of releaseFiles.entries()) {
|
|
70
|
+
const filePath = join(releasesDir, release.file);
|
|
71
|
+
const content = readFileSync(filePath, 'utf8');
|
|
72
|
+
const { updated, changed } = updateSidebarPosition(content, index);
|
|
73
|
+
if (changed) {
|
|
74
|
+
writeFileSync(filePath, updated);
|
|
75
|
+
changedCount += 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(
|
|
80
|
+
`Normalized release note sidebar positions across ${releaseFiles.length} files (${changedCount} updated).`,
|
|
81
|
+
);
|
package/scripts/release-bump.sh
CHANGED
|
@@ -192,13 +192,18 @@ if [[ "${#SURFACE_ERRORS[@]}" -gt 0 ]]; then
|
|
|
192
192
|
fi
|
|
193
193
|
echo " OK: all 8 governed version surfaces reference ${TARGET_VERSION}"
|
|
194
194
|
|
|
195
|
-
# 5.
|
|
195
|
+
# 5. Normalize release-note sidebar ordering
|
|
196
|
+
echo "[5/10] Normalizing release-note sidebar positions..."
|
|
197
|
+
node "${CLI_DIR}/scripts/normalize-release-note-sidebar-positions.mjs"
|
|
198
|
+
echo " OK: release-note sidebar positions normalized newest-first"
|
|
199
|
+
|
|
200
|
+
# 6. Auto-align Homebrew mirror to target version
|
|
196
201
|
# The formula URL and README version/tarball are updated automatically.
|
|
197
202
|
# The SHA256 is carried from the previous committed formula — it is inherently a
|
|
198
203
|
# post-publish artifact (npm registry tarballs are not byte-identical to
|
|
199
204
|
# local npm-pack output). Any working-tree SHA edit is overwritten here.
|
|
200
205
|
# sync-homebrew.sh corrects the SHA after publish.
|
|
201
|
-
echo "[
|
|
206
|
+
echo "[6/10] Auto-aligning Homebrew mirror to ${TARGET_VERSION}..."
|
|
202
207
|
HOMEBREW_MIRROR="${REPO_ROOT}/cli/homebrew/agentxchain.rb"
|
|
203
208
|
HOMEBREW_MIRROR_README="${REPO_ROOT}/cli/homebrew/README.md"
|
|
204
209
|
TARBALL_URL="https://registry.npmjs.org/agentxchain/-/agentxchain-${TARGET_VERSION}.tgz"
|
|
@@ -256,13 +261,13 @@ else
|
|
|
256
261
|
echo " Skipped: no Homebrew mirror files found"
|
|
257
262
|
fi
|
|
258
263
|
|
|
259
|
-
#
|
|
260
|
-
echo "[
|
|
264
|
+
# 7. Update version files (no git operations)
|
|
265
|
+
echo "[7/10] Updating version files..."
|
|
261
266
|
npm version "$TARGET_VERSION" --no-git-tag-version
|
|
262
267
|
echo " OK: package.json updated to ${TARGET_VERSION}"
|
|
263
268
|
|
|
264
|
-
#
|
|
265
|
-
echo "[
|
|
269
|
+
# 8. Stage version files
|
|
270
|
+
echo "[8/10] Staging version files..."
|
|
266
271
|
git add -- package.json
|
|
267
272
|
if [[ -f package-lock.json ]]; then
|
|
268
273
|
git add -- package-lock.json
|
|
@@ -270,10 +275,11 @@ fi
|
|
|
270
275
|
for rel_path in "${ALLOWED_RELEASE_PATHS[@]}"; do
|
|
271
276
|
stage_if_present "$rel_path"
|
|
272
277
|
done
|
|
278
|
+
git -C "$REPO_ROOT" add -- website-v2/docs/releases
|
|
273
279
|
echo " OK: version files and allowed release surfaces staged"
|
|
274
280
|
|
|
275
|
-
#
|
|
276
|
-
echo "[
|
|
281
|
+
# 9. Create release commit
|
|
282
|
+
echo "[9/10] Creating release commit..."
|
|
277
283
|
git commit -m "${TARGET_VERSION}"
|
|
278
284
|
RELEASE_SHA=$(git rev-parse HEAD)
|
|
279
285
|
COMMIT_MSG=$(git log -1 --format=%s)
|
|
@@ -283,13 +289,13 @@ if [[ "$COMMIT_MSG" != "$TARGET_VERSION" ]]; then
|
|
|
283
289
|
fi
|
|
284
290
|
echo " OK: commit ${RELEASE_SHA:0:7} with message '${TARGET_VERSION}'"
|
|
285
291
|
|
|
286
|
-
#
|
|
292
|
+
# 9.5. Inline preflight gate — tests, pack, and docs build must pass before tag
|
|
287
293
|
if [[ "$SKIP_PREFLIGHT" -eq 1 ]]; then
|
|
288
294
|
echo ""
|
|
289
|
-
echo "[
|
|
295
|
+
echo "[9.5/11] Inline preflight gate SKIPPED (--skip-preflight)"
|
|
290
296
|
else
|
|
291
297
|
echo ""
|
|
292
|
-
echo "[
|
|
298
|
+
echo "[9.5/11] Running inline preflight gate..."
|
|
293
299
|
echo " Running test suite..."
|
|
294
300
|
|
|
295
301
|
# Install MCP example deps if needed (same as release-preflight.sh)
|
|
@@ -342,8 +348,8 @@ else
|
|
|
342
348
|
echo " Inline preflight gate passed — proceeding to tag"
|
|
343
349
|
fi
|
|
344
350
|
|
|
345
|
-
#
|
|
346
|
-
echo "[
|
|
351
|
+
# 10. Create annotated tag
|
|
352
|
+
echo "[10/11] Creating annotated tag..."
|
|
347
353
|
git tag -a "v${TARGET_VERSION}" -m "v${TARGET_VERSION}"
|
|
348
354
|
TAG_SHA=$(git rev-parse "v${TARGET_VERSION}")
|
|
349
355
|
if [[ -z "$TAG_SHA" ]]; then
|
package/src/commands/diff.js
CHANGED
|
@@ -132,6 +132,8 @@ function formatExportDiffText(diff) {
|
|
|
132
132
|
.map((entry) => `${entry.key}: ${formatValue(entry.left)} -> ${formatValue(entry.right)}${formatDelta(entry.delta, entry.label)}`));
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
appendRegressionSection(lines, diff.regressions);
|
|
136
|
+
|
|
135
137
|
return lines.join('\n');
|
|
136
138
|
}
|
|
137
139
|
|
|
@@ -144,6 +146,18 @@ function appendChangedSection(lines, heading, items) {
|
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
function appendRegressionSection(lines, regressions) {
|
|
150
|
+
if (!regressions || regressions.length === 0) return;
|
|
151
|
+
lines.push('');
|
|
152
|
+
lines.push(chalk.bold.red('Governance Regressions:'));
|
|
153
|
+
for (const reg of regressions) {
|
|
154
|
+
const severityTag = reg.severity === 'error'
|
|
155
|
+
? chalk.red(`[${reg.severity}]`)
|
|
156
|
+
: chalk.yellow(`[${reg.severity}]`);
|
|
157
|
+
lines.push(`${severityTag} ${reg.id}: ${reg.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
147
161
|
function listChangeItems(entry) {
|
|
148
162
|
const items = [];
|
|
149
163
|
if (entry.added.length > 0) {
|
package/src/lib/export-diff.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
|
|
4
|
+
const FAILED_STATUSES = new Set(['failed', 'error', 'crashed']);
|
|
5
|
+
|
|
4
6
|
const RUN_EXPORT_SCALAR_FIELDS = [
|
|
5
7
|
['run_id', 'Run ID'],
|
|
6
8
|
['status', 'Status'],
|
|
@@ -119,6 +121,7 @@ function buildRunExportDiff(leftArtifact, rightArtifact, refs) {
|
|
|
119
121
|
};
|
|
120
122
|
|
|
121
123
|
const changed = hasChanged(scalar_changes, numeric_changes, list_changes);
|
|
124
|
+
const regressions = detectRunRegressions(left, right);
|
|
122
125
|
|
|
123
126
|
return {
|
|
124
127
|
comparison_mode: 'export',
|
|
@@ -132,6 +135,9 @@ function buildRunExportDiff(leftArtifact, rightArtifact, refs) {
|
|
|
132
135
|
scalar_changes,
|
|
133
136
|
numeric_changes,
|
|
134
137
|
list_changes,
|
|
138
|
+
regressions,
|
|
139
|
+
regression_count: regressions.length,
|
|
140
|
+
has_regressions: regressions.length > 0,
|
|
135
141
|
};
|
|
136
142
|
}
|
|
137
143
|
|
|
@@ -153,6 +159,7 @@ function buildCoordinatorExportDiff(leftArtifact, rightArtifact, refs) {
|
|
|
153
159
|
|| repo_status_changes.some((entry) => entry.changed)
|
|
154
160
|
|| repo_export_changes.some((entry) => entry.changed)
|
|
155
161
|
|| event_type_changes.some((entry) => entry.changed);
|
|
162
|
+
const regressions = detectCoordinatorRegressions(left, right);
|
|
156
163
|
|
|
157
164
|
return {
|
|
158
165
|
comparison_mode: 'export',
|
|
@@ -169,12 +176,18 @@ function buildCoordinatorExportDiff(leftArtifact, rightArtifact, refs) {
|
|
|
169
176
|
repo_status_changes,
|
|
170
177
|
repo_export_changes,
|
|
171
178
|
event_type_changes,
|
|
179
|
+
regressions,
|
|
180
|
+
regression_count: regressions.length,
|
|
181
|
+
has_regressions: regressions.length > 0,
|
|
172
182
|
};
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
function normalizeRunExport(artifact) {
|
|
176
186
|
const summary = artifact.summary || {};
|
|
177
187
|
const repoDecisions = summary.repo_decisions || {};
|
|
188
|
+
const state = artifact.state || {};
|
|
189
|
+
const budgetStatus = state.budget_status || {};
|
|
190
|
+
const phaseGateStatus = state.phase_gate_status || {};
|
|
178
191
|
return {
|
|
179
192
|
export_kind: artifact.export_kind,
|
|
180
193
|
run_id: summary.run_id || null,
|
|
@@ -197,6 +210,9 @@ function normalizeRunExport(artifact) {
|
|
|
197
210
|
retained_turn_ids: normalizeStringArray(summary.retained_turn_ids),
|
|
198
211
|
active_repo_decision_ids: normalizeStringArray((repoDecisions.active || []).map((entry) => entry?.id)),
|
|
199
212
|
overridden_repo_decision_ids: normalizeStringArray((repoDecisions.overridden || []).map((entry) => entry?.id)),
|
|
213
|
+
budget_warn_mode: budgetStatus.warn_mode === true,
|
|
214
|
+
budget_exhausted: budgetStatus.exhausted === true,
|
|
215
|
+
phase_gate_status: normalizeGateStatusMap(phaseGateStatus),
|
|
200
216
|
};
|
|
201
217
|
}
|
|
202
218
|
|
|
@@ -345,3 +361,168 @@ function toNumber(value) {
|
|
|
345
361
|
function isEqual(left, right) {
|
|
346
362
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
347
363
|
}
|
|
364
|
+
|
|
365
|
+
function normalizeGateStatusMap(gateStatus) {
|
|
366
|
+
if (!gateStatus || typeof gateStatus !== 'object' || Array.isArray(gateStatus)) return {};
|
|
367
|
+
return Object.fromEntries(
|
|
368
|
+
Object.entries(gateStatus)
|
|
369
|
+
.filter(([key]) => typeof key === 'string' && key.trim().length > 0)
|
|
370
|
+
.map(([key, value]) => [key.trim(), typeof value === 'string' ? value.trim() : String(value)]),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const GATE_PASSED_STATES = new Set(['passed', 'approved', 'satisfied']);
|
|
375
|
+
const GATE_FAILED_STATES = new Set(['failed', 'blocked', 'rejected']);
|
|
376
|
+
|
|
377
|
+
function detectRunRegressions(left, right) {
|
|
378
|
+
const regressions = [];
|
|
379
|
+
let counter = 0;
|
|
380
|
+
|
|
381
|
+
// Status regression: completed/active -> failed/error/crashed
|
|
382
|
+
if (left.status && right.status && !FAILED_STATUSES.has(left.status) && FAILED_STATUSES.has(right.status)) {
|
|
383
|
+
regressions.push({
|
|
384
|
+
id: `REG-STATUS-${String(++counter).padStart(3, '0')}`,
|
|
385
|
+
category: 'status',
|
|
386
|
+
severity: 'error',
|
|
387
|
+
message: `Run status regressed from ${left.status} to ${right.status}`,
|
|
388
|
+
field: 'status',
|
|
389
|
+
left: left.status,
|
|
390
|
+
right: right.status,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Budget warn_mode regression
|
|
395
|
+
if (left.budget_warn_mode === false && right.budget_warn_mode === true) {
|
|
396
|
+
regressions.push({
|
|
397
|
+
id: `REG-BUDGET-WARN-${String(++counter).padStart(3, '0')}`,
|
|
398
|
+
category: 'budget',
|
|
399
|
+
severity: 'warning',
|
|
400
|
+
message: 'Budget entered warn mode (over budget)',
|
|
401
|
+
field: 'budget_warn_mode',
|
|
402
|
+
left: false,
|
|
403
|
+
right: true,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Budget exhausted regression
|
|
408
|
+
if (left.budget_exhausted === false && right.budget_exhausted === true) {
|
|
409
|
+
regressions.push({
|
|
410
|
+
id: `REG-BUDGET-EXHAUST-${String(++counter).padStart(3, '0')}`,
|
|
411
|
+
category: 'budget',
|
|
412
|
+
severity: 'error',
|
|
413
|
+
message: 'Budget exhausted',
|
|
414
|
+
field: 'budget_exhausted',
|
|
415
|
+
left: false,
|
|
416
|
+
right: true,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Decision override count increase
|
|
421
|
+
const leftOverrides = typeof left.overridden_repo_decision_count === 'number' ? left.overridden_repo_decision_count : 0;
|
|
422
|
+
const rightOverrides = typeof right.overridden_repo_decision_count === 'number' ? right.overridden_repo_decision_count : 0;
|
|
423
|
+
if (rightOverrides > leftOverrides) {
|
|
424
|
+
regressions.push({
|
|
425
|
+
id: `REG-DECISION-OVERRIDE-${String(++counter).padStart(3, '0')}`,
|
|
426
|
+
category: 'decisions',
|
|
427
|
+
severity: 'warning',
|
|
428
|
+
message: `Repo decision overrides increased from ${leftOverrides} to ${rightOverrides}`,
|
|
429
|
+
field: 'overridden_repo_decision_count',
|
|
430
|
+
left: leftOverrides,
|
|
431
|
+
right: rightOverrides,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Gate regressions: passed/approved -> failed/blocked
|
|
436
|
+
const allGateIds = new Set([...Object.keys(left.phase_gate_status || {}), ...Object.keys(right.phase_gate_status || {})]);
|
|
437
|
+
for (const gateId of allGateIds) {
|
|
438
|
+
const leftGate = (left.phase_gate_status || {})[gateId] || null;
|
|
439
|
+
const rightGate = (right.phase_gate_status || {})[gateId] || null;
|
|
440
|
+
if (leftGate && rightGate && GATE_PASSED_STATES.has(leftGate) && GATE_FAILED_STATES.has(rightGate)) {
|
|
441
|
+
regressions.push({
|
|
442
|
+
id: `REG-GATE-${String(++counter).padStart(3, '0')}`,
|
|
443
|
+
category: 'gate',
|
|
444
|
+
severity: 'error',
|
|
445
|
+
message: `Gate "${gateId}" regressed from ${leftGate} to ${rightGate}`,
|
|
446
|
+
field: `phase_gate_status.${gateId}`,
|
|
447
|
+
left: leftGate,
|
|
448
|
+
right: rightGate,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return regressions;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function detectCoordinatorRegressions(left, right) {
|
|
457
|
+
// Start with the run-level regressions that apply to coordinator summaries
|
|
458
|
+
const regressions = detectRunRegressions(left, right);
|
|
459
|
+
let counter = regressions.length;
|
|
460
|
+
|
|
461
|
+
// Repo status regressions: child repo completed -> failed
|
|
462
|
+
const allRepoIds = new Set([...Object.keys(left.repo_run_statuses || {}), ...Object.keys(right.repo_run_statuses || {})]);
|
|
463
|
+
for (const repoId of allRepoIds) {
|
|
464
|
+
const leftStatus = (left.repo_run_statuses || {})[repoId] || null;
|
|
465
|
+
const rightStatus = (right.repo_run_statuses || {})[repoId] || null;
|
|
466
|
+
if (leftStatus && rightStatus && !FAILED_STATUSES.has(leftStatus) && FAILED_STATUSES.has(rightStatus)) {
|
|
467
|
+
regressions.push({
|
|
468
|
+
id: `REG-REPO-STATUS-${String(++counter).padStart(3, '0')}`,
|
|
469
|
+
category: 'repo_status',
|
|
470
|
+
severity: 'error',
|
|
471
|
+
message: `Child repo "${repoId}" status regressed from ${leftStatus} to ${rightStatus}`,
|
|
472
|
+
field: `repo_run_statuses.${repoId}`,
|
|
473
|
+
left: leftStatus,
|
|
474
|
+
right: rightStatus,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Repo export regressions: ok true -> false
|
|
480
|
+
const allExportRepoIds = new Set([...Object.keys(left.repo_export_status || {}), ...Object.keys(right.repo_export_status || {})]);
|
|
481
|
+
for (const repoId of allExportRepoIds) {
|
|
482
|
+
const leftOk = (left.repo_export_status || {})[repoId];
|
|
483
|
+
const rightOk = (right.repo_export_status || {})[repoId];
|
|
484
|
+
if (leftOk === true && rightOk === false) {
|
|
485
|
+
regressions.push({
|
|
486
|
+
id: `REG-REPO-EXPORT-${String(++counter).padStart(3, '0')}`,
|
|
487
|
+
category: 'repo_export',
|
|
488
|
+
severity: 'error',
|
|
489
|
+
message: `Child repo "${repoId}" export regressed from ok to failed`,
|
|
490
|
+
field: `repo_export_status.${repoId}`,
|
|
491
|
+
left: true,
|
|
492
|
+
right: false,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Barrier count decrease
|
|
498
|
+
const leftBarriers = typeof left.barrier_count === 'number' ? left.barrier_count : 0;
|
|
499
|
+
const rightBarriers = typeof right.barrier_count === 'number' ? right.barrier_count : 0;
|
|
500
|
+
if (leftBarriers > 0 && rightBarriers < leftBarriers) {
|
|
501
|
+
regressions.push({
|
|
502
|
+
id: `REG-BARRIER-${String(++counter).padStart(3, '0')}`,
|
|
503
|
+
category: 'barrier',
|
|
504
|
+
severity: 'warning',
|
|
505
|
+
message: `Barrier count decreased from ${leftBarriers} to ${rightBarriers}`,
|
|
506
|
+
field: 'barrier_count',
|
|
507
|
+
left: leftBarriers,
|
|
508
|
+
right: rightBarriers,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Event loss: total_events decreased
|
|
513
|
+
const leftEvents = typeof left.total_events === 'number' ? left.total_events : 0;
|
|
514
|
+
const rightEvents = typeof right.total_events === 'number' ? right.total_events : 0;
|
|
515
|
+
if (leftEvents > 0 && rightEvents < leftEvents) {
|
|
516
|
+
regressions.push({
|
|
517
|
+
id: `REG-EVENT-LOSS-${String(++counter).padStart(3, '0')}`,
|
|
518
|
+
category: 'events',
|
|
519
|
+
severity: 'warning',
|
|
520
|
+
message: `Aggregated event count decreased from ${leftEvents} to ${rightEvents}`,
|
|
521
|
+
field: 'total_events',
|
|
522
|
+
left: leftEvents,
|
|
523
|
+
right: rightEvents,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return regressions;
|
|
528
|
+
}
|