ccqa 1.48.2 → 1.49.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/bin/ccqa.mjs +74 -37
- package/dist/hub-client/index.d.mts +4 -4
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { connect as connect$1 } from "node:tls";
|
|
|
23
23
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
24
24
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
25
25
|
import { createInterface as createInterface$1 } from "node:readline";
|
|
26
|
+
import { pipeline } from "node:stream/promises";
|
|
26
27
|
//#region src/run/report-constants.ts
|
|
27
28
|
/**
|
|
28
29
|
* Pure report/run constants with no runtime dependencies. Kept separate from
|
|
@@ -29403,7 +29404,7 @@ const PRUNE_AGE_SLACK_MS = 3600 * 1e3;
|
|
|
29403
29404
|
* event per line, appended in place (not atomic-rewritten — an append must
|
|
29404
29405
|
* not cost the whole stream). A reader can therefore observe a partial final
|
|
29405
29406
|
* line mid-append; the read side counts such lines as skipped rather than
|
|
29406
|
-
* failing, and the prune's full rewrite goes through
|
|
29407
|
+
* failing, and the prune's full rewrite goes through a temp file + rename.
|
|
29407
29408
|
*/
|
|
29408
29409
|
function createFileCoverageEventStore(root, caps) {
|
|
29409
29410
|
const maxEvents = caps?.maxEvents ?? 2e5;
|
|
@@ -29415,15 +29416,15 @@ function createFileCoverageEventStore(root, caps) {
|
|
|
29415
29416
|
async function loadState(project, path) {
|
|
29416
29417
|
const cached = states.get(project);
|
|
29417
29418
|
if (cached) return cached;
|
|
29418
|
-
const
|
|
29419
|
+
const tail = await statTail(path);
|
|
29419
29420
|
const state = {
|
|
29420
29421
|
nextSeq: 1,
|
|
29421
29422
|
count: 0,
|
|
29422
|
-
bytes:
|
|
29423
|
+
bytes: tail?.size ?? 0,
|
|
29423
29424
|
oldestAt: null,
|
|
29424
|
-
endsWithNewline:
|
|
29425
|
+
endsWithNewline: tail?.endsWithNewline ?? true
|
|
29425
29426
|
};
|
|
29426
|
-
for (const rawLine of
|
|
29427
|
+
for await (const rawLine of streamLines(path)) {
|
|
29427
29428
|
const line = parseLine(rawLine);
|
|
29428
29429
|
if (line === null) continue;
|
|
29429
29430
|
if (line.seq >= state.nextSeq) state.nextSeq = line.seq + 1;
|
|
@@ -29438,18 +29439,39 @@ function createFileCoverageEventStore(root, caps) {
|
|
|
29438
29439
|
const overBytes = state.bytes > maxBytes;
|
|
29439
29440
|
const overAge = state.oldestAt !== null && state.oldestAt < now - retentionMs - PRUNE_AGE_SLACK_MS;
|
|
29440
29441
|
if (!overCount && !overBytes && !overAge) return;
|
|
29441
|
-
const lines = await readLines(path);
|
|
29442
29442
|
const cutoff = now - retentionMs;
|
|
29443
|
-
const
|
|
29443
|
+
const freshSizes = [];
|
|
29444
|
+
for await (const line of streamFreshLines(path, cutoff)) freshSizes.push(Buffer.byteLength(JSON.stringify(line)) + 1);
|
|
29444
29445
|
const keep = overCount ? Math.max(0, maxEvents - pruneBatch) : maxEvents;
|
|
29445
|
-
let
|
|
29446
|
-
if (overBytes)
|
|
29447
|
-
|
|
29448
|
-
|
|
29449
|
-
|
|
29450
|
-
|
|
29451
|
-
|
|
29452
|
-
|
|
29446
|
+
let firstKept = freshSizes.length > keep ? freshSizes.length - keep : 0;
|
|
29447
|
+
if (overBytes) firstKept = firstWithinBytes(freshSizes, firstKept, pruneBytesTarget);
|
|
29448
|
+
await mkdir(dirname(path), { recursive: true });
|
|
29449
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
29450
|
+
let keptCount = 0;
|
|
29451
|
+
let keptBytes = 0;
|
|
29452
|
+
let oldestKeptAt = null;
|
|
29453
|
+
try {
|
|
29454
|
+
await pipeline(async function* () {
|
|
29455
|
+
let freshIdx = 0;
|
|
29456
|
+
for await (const line of streamFreshLines(path, cutoff)) {
|
|
29457
|
+
freshIdx += 1;
|
|
29458
|
+
if (freshIdx <= firstKept) continue;
|
|
29459
|
+
const text = JSON.stringify(line) + "\n";
|
|
29460
|
+
keptCount += 1;
|
|
29461
|
+
keptBytes += Buffer.byteLength(text);
|
|
29462
|
+
if (oldestKeptAt === null) oldestKeptAt = line.at;
|
|
29463
|
+
yield text;
|
|
29464
|
+
}
|
|
29465
|
+
}, createWriteStream(tmp, { encoding: "utf8" }));
|
|
29466
|
+
} catch (err) {
|
|
29467
|
+
await rm(tmp, { force: true });
|
|
29468
|
+
throw err;
|
|
29469
|
+
}
|
|
29470
|
+
await rename(tmp, path);
|
|
29471
|
+
const dropped = state.count - keptCount;
|
|
29472
|
+
state.count = keptCount;
|
|
29473
|
+
state.bytes = keptBytes;
|
|
29474
|
+
state.oldestAt = oldestKeptAt;
|
|
29453
29475
|
state.endsWithNewline = true;
|
|
29454
29476
|
if (dropped > 0) console.warn(`hub: coverage inbox for "${project}": dropped ${dropped} events past retention (${maxEvents} events / ${Math.round(maxBytes / 1048576)} MiB / ${Math.round(retentionMs / 864e5)} days)`);
|
|
29455
29477
|
}
|
|
@@ -29518,22 +29540,46 @@ function createFileCoverageEventStore(root, caps) {
|
|
|
29518
29540
|
}
|
|
29519
29541
|
};
|
|
29520
29542
|
}
|
|
29521
|
-
/**
|
|
29522
|
-
function
|
|
29523
|
-
let
|
|
29524
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
29525
|
-
total += Buffer.byteLength(JSON.stringify(lines[i])) + 1;
|
|
29526
|
-
if (total > budget) return lines.slice(i + 1);
|
|
29527
|
-
}
|
|
29528
|
-
return lines;
|
|
29529
|
-
}
|
|
29530
|
-
async function readRaw(path) {
|
|
29543
|
+
/** Size and trailing-newline state of the stream file, or null when it doesn't exist. */
|
|
29544
|
+
async function statTail(path) {
|
|
29545
|
+
let fh;
|
|
29531
29546
|
try {
|
|
29532
|
-
|
|
29547
|
+
fh = await open(path, "r");
|
|
29533
29548
|
} catch (err) {
|
|
29534
|
-
if (err
|
|
29549
|
+
if (isNotFound(err)) return null;
|
|
29535
29550
|
throw err;
|
|
29536
29551
|
}
|
|
29552
|
+
try {
|
|
29553
|
+
const { size } = await fh.stat();
|
|
29554
|
+
if (size === 0) return {
|
|
29555
|
+
size,
|
|
29556
|
+
endsWithNewline: true
|
|
29557
|
+
};
|
|
29558
|
+
const tail = Buffer.alloc(1);
|
|
29559
|
+
await fh.read(tail, 0, 1, size - 1);
|
|
29560
|
+
return {
|
|
29561
|
+
size,
|
|
29562
|
+
endsWithNewline: tail[0] === 10
|
|
29563
|
+
};
|
|
29564
|
+
} finally {
|
|
29565
|
+
await fh.close();
|
|
29566
|
+
}
|
|
29567
|
+
}
|
|
29568
|
+
/** Retention-window lines only, in file order (= append order = seq order). */
|
|
29569
|
+
async function* streamFreshLines(path, cutoff) {
|
|
29570
|
+
for await (const rawLine of streamLines(path)) {
|
|
29571
|
+
const line = parseLine(rawLine);
|
|
29572
|
+
if (line !== null && line.at >= cutoff) yield line;
|
|
29573
|
+
}
|
|
29574
|
+
}
|
|
29575
|
+
/** Start of the longest suffix of `sizes` that fits `budget` (never below `lower`). */
|
|
29576
|
+
function firstWithinBytes(sizes, lower, budget) {
|
|
29577
|
+
let total = 0;
|
|
29578
|
+
for (let i = sizes.length - 1; i >= lower; i -= 1) {
|
|
29579
|
+
total += sizes[i];
|
|
29580
|
+
if (total > budget) return i + 1;
|
|
29581
|
+
}
|
|
29582
|
+
return lower;
|
|
29537
29583
|
}
|
|
29538
29584
|
/**
|
|
29539
29585
|
* The stream's non-empty lines, one at a time. Streamed rather than read as one
|
|
@@ -29550,21 +29596,12 @@ async function* streamLines(path) {
|
|
|
29550
29596
|
try {
|
|
29551
29597
|
for await (const line of lines) if (line !== "") yield line;
|
|
29552
29598
|
} catch (err) {
|
|
29553
|
-
if (!(err
|
|
29599
|
+
if (!isNotFound(err)) throw err;
|
|
29554
29600
|
} finally {
|
|
29555
29601
|
lines.close();
|
|
29556
29602
|
input.destroy();
|
|
29557
29603
|
}
|
|
29558
29604
|
}
|
|
29559
|
-
/** Every parseable line of the log; partial or corrupt lines are silently omitted (the read side counts them). */
|
|
29560
|
-
async function readLines(path) {
|
|
29561
|
-
const lines = [];
|
|
29562
|
-
for await (const rawLine of streamLines(path)) {
|
|
29563
|
-
const line = parseLine(rawLine);
|
|
29564
|
-
if (line !== null) lines.push(line);
|
|
29565
|
-
}
|
|
29566
|
-
return lines;
|
|
29567
|
-
}
|
|
29568
29605
|
function parseLine(rawLine) {
|
|
29569
29606
|
let value;
|
|
29570
29607
|
try {
|
|
@@ -35,9 +35,9 @@ declare const RunSchema: z.ZodObject<{
|
|
|
35
35
|
running: "running";
|
|
36
36
|
}>;
|
|
37
37
|
kind: z.ZodDefault<z.ZodEnum<{
|
|
38
|
+
record: "record";
|
|
38
39
|
run: "run";
|
|
39
40
|
drift: "drift";
|
|
40
|
-
record: "record";
|
|
41
41
|
}>>;
|
|
42
42
|
drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
43
43
|
specs: z.ZodNumber;
|
|
@@ -640,8 +640,8 @@ declare const ReportSpecResultSchema: z.ZodObject<{
|
|
|
640
640
|
title: z.ZodNullable<z.ZodString>;
|
|
641
641
|
target: z.ZodOptional<z.ZodString>;
|
|
642
642
|
mode: z.ZodOptional<z.ZodEnum<{
|
|
643
|
-
live: "live";
|
|
644
643
|
deterministic: "deterministic";
|
|
644
|
+
live: "live";
|
|
645
645
|
}>>;
|
|
646
646
|
status: z.ZodEnum<{
|
|
647
647
|
passed: "passed";
|
|
@@ -811,9 +811,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
|
|
|
811
811
|
declare const RunReportDataSchema: z.ZodObject<{
|
|
812
812
|
schemaVersion: z.ZodLiteral<1>;
|
|
813
813
|
kind: z.ZodDefault<z.ZodEnum<{
|
|
814
|
+
record: "record";
|
|
814
815
|
run: "run";
|
|
815
816
|
drift: "drift";
|
|
816
|
-
record: "record";
|
|
817
817
|
}>>;
|
|
818
818
|
createdAt: z.ZodString;
|
|
819
819
|
runId: z.ZodNullable<z.ZodString>;
|
|
@@ -854,8 +854,8 @@ declare const RunReportDataSchema: z.ZodObject<{
|
|
|
854
854
|
title: z.ZodNullable<z.ZodString>;
|
|
855
855
|
target: z.ZodOptional<z.ZodString>;
|
|
856
856
|
mode: z.ZodOptional<z.ZodEnum<{
|
|
857
|
-
live: "live";
|
|
858
857
|
deterministic: "deterministic";
|
|
858
|
+
live: "live";
|
|
859
859
|
}>>;
|
|
860
860
|
status: z.ZodEnum<{
|
|
861
861
|
passed: "passed";
|
package/dist/package.json
CHANGED