@zakkster/lite-perf-gate 1.0.1 → 1.2.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.0] - 2026-07-07
4
+
5
+ - **`toNDJSON(x, meta?)`** -- NDJSON verdict output for CI artifacts.
6
+ Accepts a `suiteGate()` result, a `measure()` result, or an array of
7
+ either; emits `budget` lines before their `suite-gate` summary line and
8
+ `measure` lines, with optional per-run `meta` fields merged into every
9
+ line. Otherwise the API is frozen, per the suite roadmap.
10
+
11
+ ## [1.1.0] - 2026-07-07
12
+
13
+ - **`suiteGate(config)`** -- SPP stream-fed budgets. Consumes a Float64Array
14
+ slab or any forEach record source (a lite-scope memory sink qualifies),
15
+ reduces per-budget metrics (count/sum/max/mean/last over slot t/a/b,
16
+ matched by packed header, stream+op, or op-only), and delegates every
17
+ threshold comparison to `verdict()` -- one comparison authority,
18
+ per-budget structured results. No package import: lite-perf-gate speaks
19
+ the Scope Probe Protocol (SPP v1), it does not depend on lite-scope.
20
+ suiteGate never touches process exit codes; runner semantics (0/1/3 in
21
+ the VersionMatrix scripts, 0/1/2 in `runGate`) stay in the runner layer.
22
+ CONT continuation records are never budget targets in v1.1.
23
+ - **Version discipline fix**: `VERSION` now reads `1.2.0` and the self-test
24
+ asserts it against `package.json` (the published 1.0.1 tarball shipped
25
+ `VERSION = '1.0.0'` because the old test pinned a literal instead of the
26
+ manifest -- that class of slip is now structurally impossible).
27
+
3
28
  ## [1.0.0] - 2026-07-07
4
29
 
5
30
  Initial release. Generalized from the `@zakkster/lite-signal` zero-GC gate
package/PerfGate.d.ts CHANGED
@@ -129,3 +129,67 @@ export function runGate(config: GateConfig): Promise<GateResult>;
129
129
 
130
130
  /** @internal */
131
131
  export function _controlKeepAlive(): number;
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // suiteGate (v1.1) + toNDJSON (v1.2)
135
+ // ---------------------------------------------------------------------------
136
+
137
+ /** forEach-style SPP record source (e.g. a lite-scope memory sink). */
138
+ export interface SppRecordSource {
139
+ forEach(cb: (packed: number, t: number, a: number, b: number) => void): void;
140
+ }
141
+
142
+ export interface SuiteBudget {
143
+ name: string;
144
+ /** Exact packed header (streamId << 16 | opcode). */
145
+ packed?: number;
146
+ /** Stream id; combine with op for an exact match. */
147
+ stream?: number;
148
+ /** Opcode; alone it matches the op on any stream. */
149
+ op?: number;
150
+ /** Record slot to read. Default 'a'. */
151
+ slot?: 't' | 'a' | 'b';
152
+ /** Reduction over matching records. Default 'max'. */
153
+ reduce?: 'count' | 'sum' | 'max' | 'mean' | 'last';
154
+ /** Inclusive budget: verdict() flags value > max. */
155
+ max: number;
156
+ }
157
+
158
+ export interface SuiteBudgetResult {
159
+ name: string;
160
+ value: number;
161
+ count: number;
162
+ max: number;
163
+ pass: boolean;
164
+ reasons: string[];
165
+ }
166
+
167
+ export interface SuiteGateResult {
168
+ name: string;
169
+ pass: boolean;
170
+ reasons: string[];
171
+ budgets: SuiteBudgetResult[];
172
+ }
173
+
174
+ /**
175
+ * Evaluate SPP stream records against numeric budgets. Pure reduction with
176
+ * per-budget delegation to verdict(); no measurement, no process exit
177
+ * codes, no record emission. Wide-record CONT payloads are not budget
178
+ * targets in v1.1 (base-record t/a/b slots only).
179
+ */
180
+ export function suiteGate(config: {
181
+ source: Float64Array | SppRecordSource;
182
+ budgets: SuiteBudget[];
183
+ name?: string;
184
+ }): SuiteGateResult;
185
+
186
+ /**
187
+ * Serialize suiteGate() and/or measure() results as NDJSON for CI
188
+ * artifacts: one JSON object per line, budget lines before their
189
+ * suite-gate summary, trailing newline. `meta` fields merge into every
190
+ * line; core fields win on collision.
191
+ */
192
+ export function toNDJSON(
193
+ x: SuiteGateResult | MeasureResult | Array<SuiteGateResult | MeasureResult>,
194
+ meta?: Record<string, unknown>
195
+ ): string;
package/PerfGate.js CHANGED
@@ -17,7 +17,7 @@
17
17
  * MIT License
18
18
  */
19
19
 
20
- export const VERSION = '1.0.0';
20
+ export const VERSION = '1.2.0';
21
21
 
22
22
  import {PerformanceObserver, constants} from 'node:perf_hooks';
23
23
  import {setTimeout as sleep} from 'node:timers/promises';
@@ -57,7 +57,9 @@ async function gc2() {
57
57
  // teardown (used across the ecosystem in lite-cleanup / lite-observe
58
58
  // / lite-floating) can leave the heap in an intermediate state
59
59
  // between the two collections, inflating the retained delta.
60
- await new Promise(function (r) { setImmediate(r); });
60
+ await new Promise(function (r) {
61
+ setImmediate(r);
62
+ });
61
63
  globalThis.gc();
62
64
  }
63
65
  }
@@ -69,9 +71,9 @@ async function gc2() {
69
71
  // contention) can stall the event loop for tens of milliseconds and drop
70
72
  // entries with a shorter wait. Override per-measurement via the `flushMs`
71
73
  // option, or globally via the PERF_GATE_FLUSH_MS environment variable.
72
- var DEFAULT_FLUSH_MS = 100;
74
+ let DEFAULT_FLUSH_MS = 100;
73
75
  if (typeof process !== 'undefined' && process.env && process.env.PERF_GATE_FLUSH_MS) {
74
- var envMs = parseInt(process.env.PERF_GATE_FLUSH_MS, 10);
76
+ const envMs = parseInt(process.env.PERF_GATE_FLUSH_MS, 10);
75
77
  if (envMs > 0 && envMs < 60000) DEFAULT_FLUSH_MS = envMs;
76
78
  }
77
79
 
@@ -428,3 +430,230 @@ export async function runGate(config) {
428
430
  }
429
431
  return {passed: passed, results: results};
430
432
  }
433
+
434
+ // ---------------------------------------------------------------------------
435
+ // suiteGate -- SPP stream-fed budgets (v1.1)
436
+ // ---------------------------------------------------------------------------
437
+ // lite-perf-gate does NOT import @zakkster/lite-scope. Probes, gates, and
438
+ // consumers are coupled by the Scope Probe Protocol (SPP v1 -- PROTOCOL.md
439
+ // in lite-scope), never by packages. The two protocol facts used here:
440
+ // - a record is 4 numbers [packed, t, a, b], where
441
+ // packed = (streamId << 16 | opcode) >>> 0, both u16;
442
+ // - opcode 0x0F01 (CONT) is a wide-record continuation and is never a
443
+ // budget target. Budgets therefore see the base record's t/a/b slots;
444
+ // extended CONT payload slots are out of scope for v1.1 (a lite-scope
445
+ // side bridge can pre-reduce wide records if that ever gates).
446
+ // suiteGate never touches process exit codes: runner semantics (0 pass /
447
+ // 1 regression / 3 recapture in the VersionMatrix scripts, 0/1/2 in
448
+ // runGate) stay in the runner layer. All threshold comparison is delegated
449
+ // to verdict() -- one comparison authority, per-budget.
450
+
451
+ const SPP_OP_CONT = 0x0F01;
452
+
453
+ const SUITE_REDUCES = {count: 1, sum: 1, max: 1, mean: 1, last: 1};
454
+ const SUITE_SLOTS = {t: 1, a: 2, b: 3};
455
+
456
+ function suiteMatcher(b, i) {
457
+ if (b.packed !== undefined) {
458
+ if (!Number.isInteger(b.packed) || b.packed < 0 || b.packed > 0xFFFFFFFF) {
459
+ throw new RangeError('suiteGate: budget[' + i + '] packed must be a u32');
460
+ }
461
+ return {exact: b.packed >>> 0, op: -1};
462
+ }
463
+ if (b.op === undefined || !Number.isInteger(b.op) || b.op < 0 || b.op > 0xFFFF) {
464
+ throw new RangeError('suiteGate: budget[' + i + '] needs a u16 op (with optional stream), or packed');
465
+ }
466
+ if (b.stream !== undefined) {
467
+ if (!Number.isInteger(b.stream) || b.stream < 0 || b.stream > 0xFFFF) {
468
+ throw new RangeError('suiteGate: budget[' + i + '] stream must be a u16');
469
+ }
470
+ return {exact: ((b.stream << 16) | b.op) >>> 0, op: -1};
471
+ }
472
+ return {exact: -1, op: b.op}; // op-only: matches the op on any stream
473
+ }
474
+
475
+ /**
476
+ * Evaluate SPP stream records against numeric budgets. Pure reduction plus
477
+ * per-budget delegation to verdict(); no measurement, no exit codes, no
478
+ * record emission (callers bridge verdicts to GATE_VERDICT meta records).
479
+ *
480
+ * @param {object} config
481
+ * @param {Float64Array | { forEach: (cb: (packed: number, t: number, a: number, b: number) => void) => void }} config.source
482
+ * A contiguous SPP slab (length divisible by 4) or any forEach-style
483
+ * record source (e.g. a lite-scope memory sink).
484
+ * @param {Array<{
485
+ * name: string,
486
+ * packed?: number, stream?: number, op?: number,
487
+ * slot?: 't' | 'a' | 'b',
488
+ * reduce?: 'count' | 'sum' | 'max' | 'mean' | 'last',
489
+ * max: number
490
+ * }>} config.budgets
491
+ * slot defaults to 'a', reduce to 'max'. Budgets that match zero records
492
+ * reduce to 0 (count 0 is reported so callers can tell silence from luck).
493
+ * @param {string} [config.name='suite-gate']
494
+ * @returns {{
495
+ * name: string, pass: boolean, reasons: string[],
496
+ * budgets: Array<{ name: string, value: number, count: number, max: number, pass: boolean, reasons: string[] }>
497
+ * }}
498
+ */
499
+ export function suiteGate(config) {
500
+ if (!config || typeof config !== 'object') {
501
+ throw new TypeError('suiteGate: expects a config object');
502
+ }
503
+ const source = config.source;
504
+ const budgets = config.budgets;
505
+ if (!Array.isArray(budgets) || budgets.length === 0) {
506
+ throw new TypeError('suiteGate: budgets must be a non-empty array');
507
+ }
508
+
509
+ const n = budgets.length;
510
+ const match = new Array(n);
511
+ const slot = new Array(n);
512
+ const reduce = new Array(n);
513
+ const seen = {};
514
+ for (let i = 0; i < n; i++) {
515
+ const b = budgets[i];
516
+ if (!b || typeof b.name !== 'string' || b.name.length === 0) {
517
+ throw new TypeError('suiteGate: budget[' + i + '] needs a non-empty name');
518
+ }
519
+ if (seen[b.name]) throw new RangeError('suiteGate: duplicate budget name "' + b.name + '"');
520
+ seen[b.name] = 1;
521
+ if (typeof b.max !== 'number' || !isFinite(b.max)) {
522
+ throw new RangeError('suiteGate: budget "' + b.name + '" needs a finite numeric max');
523
+ }
524
+ const sl = b.slot === undefined ? 'a' : b.slot;
525
+ if (SUITE_SLOTS[sl] === undefined) {
526
+ throw new RangeError('suiteGate: budget "' + b.name + '" slot must be t, a, or b');
527
+ }
528
+ const rd = b.reduce === undefined ? 'max' : b.reduce;
529
+ if (SUITE_REDUCES[rd] === undefined) {
530
+ throw new RangeError('suiteGate: budget "' + b.name + '" reduce must be count, sum, max, mean, or last');
531
+ }
532
+ match[i] = suiteMatcher(b, i);
533
+ slot[i] = SUITE_SLOTS[sl];
534
+ reduce[i] = rd;
535
+ }
536
+
537
+ const count = new Float64Array(n);
538
+ const sum = new Float64Array(n);
539
+ const maxv = new Float64Array(n);
540
+ const last = new Float64Array(n);
541
+ maxv.fill(-Infinity);
542
+
543
+ function visit(packed, t, a, b) {
544
+ const op = packed & 0xFFFF;
545
+ if (op === SPP_OP_CONT) return;
546
+ for (let i = 0; i < n; i++) {
547
+ const m = match[i];
548
+ if (m.exact >= 0 ? (packed >>> 0) !== m.exact : op !== m.op) continue;
549
+ const val = slot[i] === 1 ? t : slot[i] === 2 ? a : b;
550
+ count[i] += 1;
551
+ sum[i] += val;
552
+ if (val > maxv[i]) maxv[i] = val;
553
+ last[i] = val;
554
+ }
555
+ }
556
+
557
+ if (source && typeof source.forEach === 'function' && !(source instanceof Float64Array)) {
558
+ source.forEach(visit);
559
+ } else if (source instanceof Float64Array) {
560
+ if (source.length % 4 !== 0) {
561
+ throw new RangeError('suiteGate: slab length must be divisible by 4');
562
+ }
563
+ for (let r = 0; r < source.length; r += 4) {
564
+ visit(source[r], source[r + 1], source[r + 2], source[r + 3]);
565
+ }
566
+ } else {
567
+ throw new TypeError('suiteGate: source must be a Float64Array slab or a forEach record source');
568
+ }
569
+
570
+ const perBudget = [];
571
+ const reasons = [];
572
+ let pass = true;
573
+ for (let i = 0; i < n; i++) {
574
+ const b = budgets[i];
575
+ let value;
576
+ if (reduce[i] === 'count') value = count[i];
577
+ else if (reduce[i] === 'sum') value = sum[i];
578
+ else if (reduce[i] === 'max') value = count[i] > 0 ? maxv[i] : 0;
579
+ else if (reduce[i] === 'mean') value = count[i] > 0 ? sum[i] / count[i] : 0;
580
+ else value = count[i] > 0 ? last[i] : 0;
581
+
582
+ const counters = {};
583
+ counters[b.name] = value;
584
+ const ct = {};
585
+ ct[b.name] = b.max;
586
+ const v = verdict(
587
+ {name: b.name, minorHi: 0, majorHi: 0, retainedKB_hi: 0, counters_hi: counters},
588
+ {counters: ct}
589
+ );
590
+ if (!v.pass) pass = false;
591
+ for (let ri = 0; ri < v.reasons.length; ri++) reasons.push(v.reasons[ri]);
592
+ perBudget.push({
593
+ name: b.name, value: value, count: count[i], max: b.max,
594
+ pass: v.pass, reasons: v.reasons
595
+ });
596
+ }
597
+
598
+ return {
599
+ name: typeof config.name === 'string' ? config.name : 'suite-gate',
600
+ pass: pass,
601
+ reasons: reasons,
602
+ budgets: perBudget
603
+ };
604
+ }
605
+
606
+ // ---------------------------------------------------------------------------
607
+ // toNDJSON -- verdict output for CI artifacts (v1.2)
608
+ // ---------------------------------------------------------------------------
609
+
610
+ function ndjsonSuiteGate(r, meta, lines) {
611
+ for (let i = 0; i < r.budgets.length; i++) {
612
+ const b = r.budgets[i];
613
+ lines.push(JSON.stringify(Object.assign({}, meta, {
614
+ type: 'budget', gate: r.name, name: b.name,
615
+ value: b.value, count: b.count, max: b.max, pass: b.pass
616
+ })));
617
+ }
618
+ lines.push(JSON.stringify(Object.assign({}, meta, {
619
+ type: 'suite-gate', name: r.name, pass: r.pass, reasons: r.reasons
620
+ })));
621
+ }
622
+
623
+ function ndjsonMeasure(r, meta, lines) {
624
+ lines.push(JSON.stringify(Object.assign({}, meta, {
625
+ type: 'measure', name: r.name, N: r.N, k: r.k,
626
+ minorLo: r.minorLo, minorHi: r.minorHi,
627
+ majorLo: r.majorLo, majorHi: r.majorHi,
628
+ retainedKB_lo: r.retainedKB_lo, retainedKB_hi: r.retainedKB_hi,
629
+ counters_lo: r.counters_lo, counters_hi: r.counters_hi
630
+ })));
631
+ }
632
+
633
+ /**
634
+ * Serialize gate output as NDJSON for CI artifacts: one JSON object per
635
+ * line, budget lines before their suite-gate summary line, trailing
636
+ * newline. Accepts a suiteGate() result, a measure() result, or an array
637
+ * mixing both. `meta` fields (run id, package, version, ...) are merged
638
+ * into every line; core fields win on collision.
639
+ *
640
+ * @param {object | object[]} x
641
+ * @param {object} [meta]
642
+ * @returns {string}
643
+ */
644
+ export function toNDJSON(x, meta) {
645
+ const rows = Array.isArray(x) ? x : [x];
646
+ const m = meta && typeof meta === 'object' ? meta : {};
647
+ const lines = [];
648
+ for (let i = 0; i < rows.length; i++) {
649
+ const r = rows[i];
650
+ if (r && Array.isArray(r.budgets) && typeof r.pass === 'boolean') {
651
+ ndjsonSuiteGate(r, m, lines);
652
+ } else if (r && typeof r.minorHi === 'number' && typeof r.N === 'number') {
653
+ ndjsonMeasure(r, m, lines);
654
+ } else {
655
+ throw new TypeError('toNDJSON: row ' + i + ' is neither a suiteGate result nor a measure result');
656
+ }
657
+ }
658
+ return lines.join('\n') + '\n';
659
+ }
package/README.md CHANGED
@@ -24,6 +24,44 @@ npm install @zakkster/lite-perf-gate
24
24
 
25
25
  Scavenge-counting is the only reliable detector of transient allocation. This library packages the methodology into a `node:test`-native harness that validates its own detector on every run.
26
26
 
27
+
28
+ ## suiteGate -- SPP stream-fed budgets (v1.1)
29
+
30
+ Gate live probe streams, not just measured scenarios. `suiteGate()` reads
31
+ SPP records (a `Float64Array` slab or any `forEach(cb(packed, t, a, b))`
32
+ source -- a `@zakkster/lite-scope` memory sink qualifies), reduces each
33
+ budget's metric, and delegates every comparison to `verdict()`:
34
+
35
+ ```js
36
+ import { suiteGate, toNDJSON } from '@zakkster/lite-perf-gate';
37
+
38
+ const gate = suiteGate({
39
+ name: 'ci-gate',
40
+ source: sink.toSlab(), // or the sink itself (forEach source)
41
+ budgets: [
42
+ { name: 'gc.pause.max', stream: 2, op: 0x0201, slot: 'a', reduce: 'max', max: 8 },
43
+ { name: 'leak.orphans', op: 0x0801, reduce: 'count', max: 0 },
44
+ { name: 'inp.worst', op: 0x0601, slot: 'a', reduce: 'max', max: 200 }
45
+ ]
46
+ });
47
+
48
+ process.stdout.write(toNDJSON(gate, { pkg: 'my-lib', run: process.env.CI_RUN }));
49
+ if (!gate.pass) process.exitCode = 1; // exit codes stay in YOUR runner
50
+ ```
51
+
52
+ No package coupling: lite-perf-gate speaks the Scope Probe Protocol
53
+ (SPP v1, `PROTOCOL.md` in lite-scope) and never imports lite-scope.
54
+ CONT continuation records are never budget targets (base-record slots
55
+ only in v1.1). `suiteGate` returns structured per-budget verdicts and
56
+ leaves exit codes to the runner layer.
57
+
58
+ ## toNDJSON -- CI artifacts (v1.2)
59
+
60
+ One JSON object per line: `budget` lines, then the `suite-gate` summary;
61
+ `measure()` results serialize as `measure` lines. Optional `meta` fields
62
+ (package, run id, versions) merge into every line. Pipe to a file in your
63
+ gate script and attach it as a CI artifact.
64
+
27
65
  ## Quick start
28
66
 
29
67
  ```js
package/llms.txt CHANGED
@@ -63,3 +63,14 @@ is the only reliable detector of transient allocation in V8.
63
63
 
64
64
  Single-file ESM. Imports only node:perf_hooks, node:timers/promises,
65
65
  node:test, node:assert/strict.
66
+
67
+ v1.1 suiteGate(config): SPP stream-fed budgets. config.source = Float64Array
68
+ slab or { forEach(cb(packed,t,a,b)) } (lite-scope memory sink). budgets[] =
69
+ { name, packed? | stream?+op | op, slot: t|a|b (default a), reduce:
70
+ count|sum|max|mean|last (default max), max }. Delegates every comparison to
71
+ verdict(); returns { name, pass, reasons, budgets: [{name, value, count,
72
+ max, pass, reasons}] }. Never sets exit codes; CONT records (op 0x0F01) are
73
+ never budget targets. No lite-scope import: coupled by SPP v1 protocol only.
74
+ v1.2 toNDJSON(x, meta?): NDJSON for CI artifacts; accepts suiteGate result,
75
+ measure result, or array; budget lines then suite-gate summary; meta merged
76
+ per line; trailing newline.
package/package.json CHANGED
@@ -1,63 +1,63 @@
1
1
  {
2
- "name": "@zakkster/lite-perf-gate",
3
- "version": "1.0.1",
4
- "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
5
- "description": "Zero-GC and performance regression gate for node:test. Scavenge-counting, scaling verdict, detector self-validation. Proves your hot path allocates nothing -- or names what did.",
6
- "type": "module",
7
- "sideEffects": false,
8
- "main": "./PerfGate.js",
9
- "module": "./PerfGate.js",
10
- "types": "./PerfGate.d.ts",
11
- "exports": {
12
- ".": {
13
- "types": "./PerfGate.d.ts",
14
- "node": "./PerfGate.js",
15
- "import": "./PerfGate.js",
16
- "default": "./PerfGate.js"
17
- }
18
- },
19
- "files": [
20
- "PerfGate.js",
21
- "PerfGate.d.ts",
22
- "README.md",
23
- "llms.txt",
24
- "LICENSE",
25
- "CHANGELOG.md"
26
- ],
27
- "scripts": {
28
- "test": "node --expose-gc --max-semi-space-size=4 --test test/self.test.mjs"
29
- },
30
- "keywords": [
31
- "benchmark",
32
- "regression",
33
- "gate",
34
- "zero-gc",
35
- "gc",
36
- "scavenge",
37
- "allocation",
38
- "performance",
39
- "node-test",
40
- "ci",
41
- "perf-hooks"
42
- ],
43
- "publishConfig": {
44
- "access": "public"
45
- },
46
- "license": "MIT",
47
- "homepage": "https://github.com/PeshoVurtoleta/lite-perf-gate#readme",
48
- "repository": {
49
- "type": "git",
50
- "url": "git+https://github.com/PeshoVurtoleta/lite-perf-gate.git"
51
- },
52
- "bugs": {
53
- "url": "https://github.com/PeshoVurtoleta/lite-perf-gate/issues",
54
- "email": "shinikchiev@yahoo.com"
55
- },
56
- "funding": {
57
- "type": "github",
58
- "url": "https://github.com/sponsors/PeshoVurtoleta"
59
- },
60
- "engines": {
61
- "node": ">=18"
2
+ "name": "@zakkster/lite-perf-gate",
3
+ "version": "1.2.1",
4
+ "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
5
+ "description": "Zero-GC and performance regression gate for node:test. Scavenge-counting, scaling verdict, detector self-validation. Proves your hot path allocates nothing -- or names what did.",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./PerfGate.js",
9
+ "module": "./PerfGate.js",
10
+ "types": "./PerfGate.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./PerfGate.d.ts",
14
+ "node": "./PerfGate.js",
15
+ "import": "./PerfGate.js",
16
+ "default": "./PerfGate.js"
62
17
  }
18
+ },
19
+ "files": [
20
+ "PerfGate.js",
21
+ "PerfGate.d.ts",
22
+ "README.md",
23
+ "llms.txt",
24
+ "LICENSE",
25
+ "CHANGELOG.md"
26
+ ],
27
+ "scripts": {
28
+ "test": "node --expose-gc --max-semi-space-size=4 --test test/self.test.mjs"
29
+ },
30
+ "keywords": [
31
+ "benchmark",
32
+ "regression",
33
+ "gate",
34
+ "zero-gc",
35
+ "gc",
36
+ "scavenge",
37
+ "allocation",
38
+ "performance",
39
+ "node-test",
40
+ "ci",
41
+ "perf-hooks"
42
+ ],
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "license": "MIT",
47
+ "homepage": "https://github.com/PeshoVurtoleta/lite-perf-gate#readme",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/PeshoVurtoleta/lite-perf-gate.git"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/PeshoVurtoleta/lite-perf-gate/issues",
54
+ "email": "shinikchiev@yahoo.com"
55
+ },
56
+ "funding": {
57
+ "type": "github",
58
+ "url": "https://github.com/sponsors/PeshoVurtoleta"
59
+ },
60
+ "engines": {
61
+ "node": ">=18"
62
+ }
63
63
  }