chainlesschain 0.66.0 → 0.81.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.
@@ -21,6 +21,25 @@ import {
21
21
  generateReport,
22
22
  SLA_TERMS,
23
23
  VIOLATION_SEVERITY,
24
+ // V2
25
+ SLA_STATUS_V2,
26
+ SLA_TIER_V2,
27
+ SLA_TERM_V2,
28
+ VIOLATION_SEVERITY_V2,
29
+ VIOLATION_STATUS_V2,
30
+ SLA_DEFAULT_MAX_ACTIVE_PER_ORG,
31
+ setMaxActiveSlasPerOrg,
32
+ getMaxActiveSlasPerOrg,
33
+ getActiveSlaCountForOrg,
34
+ createSLAV2,
35
+ setSLAStatus,
36
+ expireSLA,
37
+ autoExpireSLAs,
38
+ setViolationStatus,
39
+ acknowledgeViolation,
40
+ resolveViolation,
41
+ waiveViolation,
42
+ getSLAStatsV2,
24
43
  } from "../lib/sla-manager.js";
25
44
 
26
45
  function _dbFromCtx(ctx) {
@@ -349,4 +368,244 @@ export function registerSlaCommand(program) {
349
368
  process.exit(1);
350
369
  }
351
370
  });
371
+
372
+ // ---------- V2 (Phase 61) ----------
373
+ const withDb = async (fn) => {
374
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
375
+ if (!ctx.db) {
376
+ logger.error("Database not available");
377
+ process.exit(1);
378
+ }
379
+ try {
380
+ const db = ctx.db.getDatabase();
381
+ ensureSlaTables(db);
382
+ return await fn(db);
383
+ } finally {
384
+ await shutdown();
385
+ }
386
+ };
387
+
388
+ sla
389
+ .command("statuses")
390
+ .description("List SLA_STATUS_V2 values")
391
+ .action(() => {
392
+ console.log(JSON.stringify(Object.values(SLA_STATUS_V2), null, 2));
393
+ });
394
+
395
+ sla
396
+ .command("tier-names")
397
+ .description("List SLA_TIER_V2 values")
398
+ .action(() => {
399
+ console.log(JSON.stringify(Object.values(SLA_TIER_V2), null, 2));
400
+ });
401
+
402
+ sla
403
+ .command("term-names")
404
+ .description("List SLA_TERM_V2 values")
405
+ .action(() => {
406
+ console.log(JSON.stringify(Object.values(SLA_TERM_V2), null, 2));
407
+ });
408
+
409
+ sla
410
+ .command("severities")
411
+ .description("List VIOLATION_SEVERITY_V2 values")
412
+ .action(() => {
413
+ console.log(
414
+ JSON.stringify(Object.values(VIOLATION_SEVERITY_V2), null, 2),
415
+ );
416
+ });
417
+
418
+ sla
419
+ .command("violation-statuses")
420
+ .description("List VIOLATION_STATUS_V2 values")
421
+ .action(() => {
422
+ console.log(JSON.stringify(Object.values(VIOLATION_STATUS_V2), null, 2));
423
+ });
424
+
425
+ sla
426
+ .command("default-max-active")
427
+ .description("Show SLA_DEFAULT_MAX_ACTIVE_PER_ORG")
428
+ .action(() => {
429
+ console.log(SLA_DEFAULT_MAX_ACTIVE_PER_ORG);
430
+ });
431
+
432
+ sla
433
+ .command("max-active")
434
+ .description("Show current max active SLAs per org")
435
+ .action(() => {
436
+ console.log(getMaxActiveSlasPerOrg());
437
+ });
438
+
439
+ sla
440
+ .command("set-max-active <n>")
441
+ .description("Set per-org active-contract admission cap")
442
+ .action((n) => {
443
+ try {
444
+ const v = setMaxActiveSlasPerOrg(Number(n));
445
+ logger.success(`maxActiveSlasPerOrg=${v}`);
446
+ } catch (err) {
447
+ logger.error(`Failed: ${err.message}`);
448
+ process.exit(1);
449
+ }
450
+ });
451
+
452
+ sla
453
+ .command("active-count <org-id>")
454
+ .description("Show active SLA count for an org")
455
+ .action((orgId) => {
456
+ console.log(getActiveSlaCountForOrg(orgId));
457
+ });
458
+
459
+ sla
460
+ .command("create-v2 <org-id>")
461
+ .description("Create a V2 SLA contract (enforces per-org active cap)")
462
+ .option("-t, --tier <tier>", "gold|silver|bronze", "silver")
463
+ .option("-d, --duration <ms>", "Contract duration in ms", parseInt)
464
+ .option("-f, --fee <amount>", "Monthly fee", parseFloat)
465
+ .option("--json", "Output as JSON")
466
+ .action(async (orgId, options) => {
467
+ try {
468
+ await withDb((db) => {
469
+ const c = createSLAV2(db, {
470
+ orgId,
471
+ tier: options.tier,
472
+ duration: options.duration,
473
+ monthlyFee: options.fee,
474
+ });
475
+ if (options.json) {
476
+ console.log(JSON.stringify(c, null, 2));
477
+ } else {
478
+ logger.success(
479
+ `Created ${c.slaId.slice(0, 8)} [${c.tier}] → ${c.status}`,
480
+ );
481
+ }
482
+ });
483
+ } catch (err) {
484
+ logger.error(`Failed: ${err.message}`);
485
+ process.exit(1);
486
+ }
487
+ });
488
+
489
+ sla
490
+ .command("set-status <sla-id> <status>")
491
+ .description("Transition SLA to a given status (state-machine guarded)")
492
+ .action(async (slaId, status) => {
493
+ try {
494
+ await withDb((db) => {
495
+ const c = setSLAStatus(db, slaId, status);
496
+ logger.success(`${c.slaId.slice(0, 8)} → ${c.status}`);
497
+ });
498
+ } catch (err) {
499
+ logger.error(`Failed: ${err.message}`);
500
+ process.exit(1);
501
+ }
502
+ });
503
+
504
+ sla
505
+ .command("expire <sla-id>")
506
+ .description("Expire an SLA (shortcut for set-status ... expired)")
507
+ .action(async (slaId) => {
508
+ try {
509
+ await withDb((db) => {
510
+ const c = expireSLA(db, slaId);
511
+ logger.success(`${c.slaId.slice(0, 8)} → ${c.status}`);
512
+ });
513
+ } catch (err) {
514
+ logger.error(`Failed: ${err.message}`);
515
+ process.exit(1);
516
+ }
517
+ });
518
+
519
+ sla
520
+ .command("auto-expire")
521
+ .description("Bulk-flip ACTIVE contracts past endDate to EXPIRED")
522
+ .option("--json", "Output as JSON")
523
+ .action(async (options) => {
524
+ try {
525
+ await withDb((db) => {
526
+ const flipped = autoExpireSLAs(db);
527
+ if (options.json) {
528
+ console.log(JSON.stringify(flipped, null, 2));
529
+ } else {
530
+ logger.success(`Auto-expired ${flipped.length} contract(s)`);
531
+ }
532
+ });
533
+ } catch (err) {
534
+ logger.error(`Failed: ${err.message}`);
535
+ process.exit(1);
536
+ }
537
+ });
538
+
539
+ sla
540
+ .command("set-violation-status <violation-id> <status>")
541
+ .description("Transition a violation (open→{acknowledged,resolved,waived})")
542
+ .option("--note <note>", "Attach a note")
543
+ .action(async (violationId, status, options) => {
544
+ try {
545
+ await withDb((db) => {
546
+ const v = setViolationStatus(db, violationId, status, {
547
+ note: options.note,
548
+ });
549
+ logger.success(`${v.violationId.slice(0, 8)} → ${v.v2Status}`);
550
+ });
551
+ } catch (err) {
552
+ logger.error(`Failed: ${err.message}`);
553
+ process.exit(1);
554
+ }
555
+ });
556
+
557
+ sla
558
+ .command("acknowledge-violation <violation-id>")
559
+ .description("Acknowledge a violation")
560
+ .option("--note <note>", "Attach a note")
561
+ .action(async (violationId, options) => {
562
+ try {
563
+ await withDb((db) => {
564
+ const v = acknowledgeViolation(db, violationId, options.note);
565
+ logger.success(`${v.violationId.slice(0, 8)} → ${v.v2Status}`);
566
+ });
567
+ } catch (err) {
568
+ logger.error(`Failed: ${err.message}`);
569
+ process.exit(1);
570
+ }
571
+ });
572
+
573
+ sla
574
+ .command("resolve-violation <violation-id>")
575
+ .description("Resolve a violation")
576
+ .option("--note <note>", "Attach a note")
577
+ .action(async (violationId, options) => {
578
+ try {
579
+ await withDb((db) => {
580
+ const v = resolveViolation(db, violationId, options.note);
581
+ logger.success(`${v.violationId.slice(0, 8)} → ${v.v2Status}`);
582
+ });
583
+ } catch (err) {
584
+ logger.error(`Failed: ${err.message}`);
585
+ process.exit(1);
586
+ }
587
+ });
588
+
589
+ sla
590
+ .command("waive-violation <violation-id>")
591
+ .description("Waive a violation")
592
+ .option("--note <note>", "Attach a note")
593
+ .action(async (violationId, options) => {
594
+ try {
595
+ await withDb((db) => {
596
+ const v = waiveViolation(db, violationId, options.note);
597
+ logger.success(`${v.violationId.slice(0, 8)} → ${v.v2Status}`);
598
+ });
599
+ } catch (err) {
600
+ logger.error(`Failed: ${err.message}`);
601
+ process.exit(1);
602
+ }
603
+ });
604
+
605
+ sla
606
+ .command("stats-v2")
607
+ .description("Show aggregate V2 SLA stats (byStatus/byTier/violations)")
608
+ .action(() => {
609
+ console.log(JSON.stringify(getSLAStatsV2(), null, 2));
610
+ });
352
611
  }
@@ -15,6 +15,22 @@ import {
15
15
  analyzeBottlenecks,
16
16
  generateCapacityPlan,
17
17
  listLoadLevels,
18
+ // V2
19
+ RUN_STATUS_V2,
20
+ LEVEL_NAME_V2,
21
+ BOTTLENECK_KIND_V2,
22
+ BOTTLENECK_SEVERITY_V2,
23
+ STRESS_DEFAULT_MAX_CONCURRENT,
24
+ setMaxConcurrentTests,
25
+ getMaxConcurrentTests,
26
+ getActiveTestCount,
27
+ startStressTestV2,
28
+ completeStressTest,
29
+ stopStressTestV2,
30
+ failStressTest,
31
+ setRunStatus,
32
+ recommendLevelV2,
33
+ getStressStatsV2,
18
34
  } from "../lib/stress-tester.js";
19
35
 
20
36
  function _dbFromCtx(ctx) {
@@ -249,4 +265,218 @@ export function registerStressCommand(program) {
249
265
  }
250
266
  }
251
267
  });
268
+
269
+ // ---------- V2 (Phase 59) ----------
270
+ const withDb = async (fn) => {
271
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
272
+ if (!ctx.db) {
273
+ logger.error("Database not available");
274
+ process.exit(1);
275
+ }
276
+ try {
277
+ const db = ctx.db.getDatabase();
278
+ ensureStressTables(db);
279
+ return await fn(db);
280
+ } finally {
281
+ await shutdown();
282
+ }
283
+ };
284
+
285
+ stress
286
+ .command("run-statuses")
287
+ .description("List RUN_STATUS_V2 values")
288
+ .action(() => {
289
+ console.log(JSON.stringify(Object.values(RUN_STATUS_V2), null, 2));
290
+ });
291
+
292
+ stress
293
+ .command("level-names")
294
+ .description("List LEVEL_NAME_V2 values")
295
+ .action(() => {
296
+ console.log(JSON.stringify(Object.values(LEVEL_NAME_V2), null, 2));
297
+ });
298
+
299
+ stress
300
+ .command("bottleneck-kinds")
301
+ .description("List BOTTLENECK_KIND_V2 values")
302
+ .action(() => {
303
+ console.log(JSON.stringify(Object.values(BOTTLENECK_KIND_V2), null, 2));
304
+ });
305
+
306
+ stress
307
+ .command("bottleneck-severities")
308
+ .description("List BOTTLENECK_SEVERITY_V2 values")
309
+ .action(() => {
310
+ console.log(
311
+ JSON.stringify(Object.values(BOTTLENECK_SEVERITY_V2), null, 2),
312
+ );
313
+ });
314
+
315
+ stress
316
+ .command("default-max-concurrent")
317
+ .description("Show STRESS_DEFAULT_MAX_CONCURRENT")
318
+ .action(() => {
319
+ console.log(STRESS_DEFAULT_MAX_CONCURRENT);
320
+ });
321
+
322
+ stress
323
+ .command("max-concurrent")
324
+ .description("Show current max concurrent test limit")
325
+ .action(() => {
326
+ console.log(getMaxConcurrentTests());
327
+ });
328
+
329
+ stress
330
+ .command("active-test-count")
331
+ .description("Show current active (RUNNING) test count")
332
+ .action(() => {
333
+ console.log(getActiveTestCount());
334
+ });
335
+
336
+ stress
337
+ .command("set-max-concurrent <n>")
338
+ .description("Set max concurrent test admission limit")
339
+ .action((n) => {
340
+ try {
341
+ const v = setMaxConcurrentTests(Number(n));
342
+ logger.success(`maxConcurrentTests=${v}`);
343
+ } catch (err) {
344
+ logger.error(`Failed: ${err.message}`);
345
+ process.exit(1);
346
+ }
347
+ });
348
+
349
+ stress
350
+ .command("start-v2")
351
+ .description("Start a V2 stress run (RUNNING, no metrics until complete)")
352
+ .option(
353
+ "-l, --level <level>",
354
+ "Load level (light|medium|heavy|extreme)",
355
+ "medium",
356
+ )
357
+ .option("-c, --concurrency <n>", "Override concurrency", parseInt)
358
+ .option("-r, --rps <n>", "Override requests per second", parseInt)
359
+ .option("-d, --duration <ms>", "Override duration in ms", parseInt)
360
+ .option("--json", "Output as JSON")
361
+ .action(async (options) => {
362
+ try {
363
+ await withDb((db) => {
364
+ const run = startStressTestV2(db, {
365
+ level: options.level,
366
+ concurrency: options.concurrency,
367
+ requestsPerSecond: options.rps,
368
+ duration: options.duration,
369
+ });
370
+ if (options.json) {
371
+ console.log(JSON.stringify(run, null, 2));
372
+ } else {
373
+ logger.success(
374
+ `Started ${run.testId.slice(0, 8)} [${run.loadLevel}] → ${run.status}`,
375
+ );
376
+ }
377
+ });
378
+ } catch (err) {
379
+ logger.error(`Failed: ${err.message}`);
380
+ process.exit(1);
381
+ }
382
+ });
383
+
384
+ stress
385
+ .command("complete <test-id>")
386
+ .description("Complete a RUNNING run and compute metrics")
387
+ .option("--json", "Output as JSON")
388
+ .action(async (testId, options) => {
389
+ try {
390
+ await withDb((db) => {
391
+ const r = completeStressTest(db, testId);
392
+ if (options.json) {
393
+ console.log(JSON.stringify(r, null, 2));
394
+ } else {
395
+ logger.success(
396
+ `${r.testId.slice(0, 8)} → ${r.status} (tps=${r.result.tps})`,
397
+ );
398
+ }
399
+ });
400
+ } catch (err) {
401
+ logger.error(`Failed: ${err.message}`);
402
+ process.exit(1);
403
+ }
404
+ });
405
+
406
+ stress
407
+ .command("stop-v2 <test-id>")
408
+ .description("Stop a RUNNING run (→ STOPPED)")
409
+ .action(async (testId) => {
410
+ try {
411
+ await withDb((db) => {
412
+ const r = stopStressTestV2(db, testId);
413
+ logger.success(`${r.testId.slice(0, 8)} → ${r.status}`);
414
+ });
415
+ } catch (err) {
416
+ logger.error(`Failed: ${err.message}`);
417
+ process.exit(1);
418
+ }
419
+ });
420
+
421
+ stress
422
+ .command("fail <test-id> <error-message>")
423
+ .description("Fail a RUNNING run with an error message (→ FAILED)")
424
+ .action(async (testId, errorMessage) => {
425
+ try {
426
+ await withDb((db) => {
427
+ const r = failStressTest(db, testId, errorMessage);
428
+ logger.success(
429
+ `${r.testId.slice(0, 8)} → ${r.status} (${r.errorMessage})`,
430
+ );
431
+ });
432
+ } catch (err) {
433
+ logger.error(`Failed: ${err.message}`);
434
+ process.exit(1);
435
+ }
436
+ });
437
+
438
+ stress
439
+ .command("set-status <test-id> <status>")
440
+ .description("Transition run to a given status (state-machine guarded)")
441
+ .option("--error-message <msg>", "Attach error message (for failed)")
442
+ .action(async (testId, status, options) => {
443
+ try {
444
+ await withDb((db) => {
445
+ const patch = {};
446
+ if (options.errorMessage) patch.errorMessage = options.errorMessage;
447
+ const r = setRunStatus(db, testId, status, patch);
448
+ logger.success(`${r.testId.slice(0, 8)} → ${r.status}`);
449
+ });
450
+ } catch (err) {
451
+ logger.error(`Failed: ${err.message}`);
452
+ process.exit(1);
453
+ }
454
+ });
455
+
456
+ stress
457
+ .command("recommend-level <target-rps>")
458
+ .description("Recommend the largest built-in level ≤ targetRps")
459
+ .option("--json", "Output as JSON")
460
+ .action((targetRps, options) => {
461
+ try {
462
+ const level = recommendLevelV2(Number(targetRps));
463
+ if (options.json) {
464
+ console.log(JSON.stringify(level, null, 2));
465
+ } else {
466
+ logger.log(
467
+ ` ${chalk.cyan(level.name)} concurrency=${level.concurrency} rps=${level.requestsPerSecond} duration=${level.duration}ms`,
468
+ );
469
+ }
470
+ } catch (err) {
471
+ logger.error(`Failed: ${err.message}`);
472
+ process.exit(1);
473
+ }
474
+ });
475
+
476
+ stress
477
+ .command("stats-v2")
478
+ .description("Show aggregate V2 stats (byStatus/byLevel/bottlenecks)")
479
+ .action(() => {
480
+ console.log(JSON.stringify(getStressStatsV2(), null, 2));
481
+ });
252
482
  }