@codyswann/lisa 4.33.5 → 4.33.6
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/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +8 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.js +2 -2
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +414 -6
- package/typescript/create-only/.github/workflows/review-evidence.yml +78 -3
|
@@ -487,6 +487,40 @@ export const SETTLE_INTERVAL_SECONDS = 15;
|
|
|
487
487
|
/** Enables the blocking review-evidence policy. */
|
|
488
488
|
const REQUIRE_REVIEW_EVIDENCE_FLAG = "--require-review-evidence";
|
|
489
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Selects how many recently merged pull requests the waive-rate sample reads.
|
|
492
|
+
*
|
|
493
|
+
* Zero — the shipped default — disables the sample entirely, so no repository
|
|
494
|
+
* acquires a network read it did not ask for. The workflow that ships with Lisa
|
|
495
|
+
* passes a value, because the number is the point: `.github/workflows/
|
|
496
|
+
* review-evidence.yml` recorded "of the last 40 merged pull requests, 39 waive"
|
|
497
|
+
* in a COMMENT, where it changed nothing for eight months. A rate that an
|
|
498
|
+
* operator reads at merge time is a different artefact from a rate somebody
|
|
499
|
+
* once measured.
|
|
500
|
+
*/
|
|
501
|
+
const WAIVE_RATE_SAMPLE_FLAG = "--waive-rate-sample";
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* The one call the waive-rate sample makes.
|
|
505
|
+
*
|
|
506
|
+
* Kept beside the flag rather than inline in the reader so the network shape is
|
|
507
|
+
* visible without reading a function: merged pull requests, newest first, each
|
|
508
|
+
* with its head commit's rollup contexts. `StatusContext` and `CheckRun` are
|
|
509
|
+
* both selected because a review bot may report as either and the description —
|
|
510
|
+
* `description` on one, `title` on the other — is the only field that separates
|
|
511
|
+
* a review from a waiver.
|
|
512
|
+
*/
|
|
513
|
+
const RECENT_MERGED_CHECKS_QUERY = [
|
|
514
|
+
"query($owner:String!,$name:String!,$limit:Int!){",
|
|
515
|
+
"repository(owner:$owner,name:$name){",
|
|
516
|
+
"pullRequests(states:MERGED,first:$limit,orderBy:{field:UPDATED_AT,direction:DESC}){",
|
|
517
|
+
"nodes{number commits(last:1){nodes{commit{statusCheckRollup{contexts(last:100){",
|
|
518
|
+
"nodes{__typename",
|
|
519
|
+
" ... on StatusContext{context state description}",
|
|
520
|
+
" ... on CheckRun{name conclusion title}",
|
|
521
|
+
"}}}}}}}}}}",
|
|
522
|
+
].join("");
|
|
523
|
+
|
|
490
524
|
/**
|
|
491
525
|
* The flag that used to select the live-ruleset drift mode, retired with it.
|
|
492
526
|
*
|
|
@@ -1284,13 +1318,18 @@ export function reviewGateState(reading, vocabulary = {}) {
|
|
|
1284
1318
|
* @param {object} declaration - The per-repo declaration
|
|
1285
1319
|
* @param {ReadonlyArray<{name: string, state: string, description?: string}>} checks - The checks
|
|
1286
1320
|
* @param {{headSha?: string}} [options] - `headSha` is cited in every finding
|
|
1287
|
-
* @returns {{violations: object[], states: Record<string, string>, checked: number}} Findings, per-check state, and how many were examined
|
|
1321
|
+
* @returns {{violations: object[], states: Record<string, string>, descriptions: Record<string, string>, checked: number}} Findings, per-check state, the description each verdict was read from, and how many were examined
|
|
1288
1322
|
*/
|
|
1289
1323
|
export function evaluateReviewGate(declaration, checks, options = {}) {
|
|
1290
1324
|
const declared = declaration.evidence_bearing_checks ?? {};
|
|
1291
1325
|
const at = citeHeadSha(options.headSha);
|
|
1292
1326
|
const violations = [];
|
|
1293
1327
|
const states = {};
|
|
1328
|
+
// Carried out with the states because the RENDERED verdict quotes it. A title
|
|
1329
|
+
// that said only "WAIVED" would tell an operator that something was waived
|
|
1330
|
+
// and not which vendor sentence did the waiving, which is the fact that says
|
|
1331
|
+
// whether to wait for the entitlement or merge on the waiver.
|
|
1332
|
+
const descriptions = {};
|
|
1294
1333
|
let checked = 0;
|
|
1295
1334
|
|
|
1296
1335
|
for (const [name, entry] of Object.entries(declared)) {
|
|
@@ -1308,6 +1347,13 @@ export function evaluateReviewGate(declaration, checks, options = {}) {
|
|
|
1308
1347
|
vocabulary
|
|
1309
1348
|
);
|
|
1310
1349
|
states[name] = verdict.state;
|
|
1350
|
+
// Assigned ONLY when the check reported. An absent check with a `""`
|
|
1351
|
+
// description entry renders as `CodeRabbit reported ""` — a sentence that
|
|
1352
|
+
// says it reported. MEASURED on this fix's own first commit, where the
|
|
1353
|
+
// published verdict read exactly that. Absence is not a quiet report, and
|
|
1354
|
+
// the whole file turns on the two being different facts.
|
|
1355
|
+
if (found !== undefined)
|
|
1356
|
+
descriptions[name] = String(found.description ?? "");
|
|
1311
1357
|
if (verdict.state === REVIEW_GATE_STATES.satisfied) continue;
|
|
1312
1358
|
violations.push({
|
|
1313
1359
|
kind:
|
|
@@ -1320,7 +1366,295 @@ export function evaluateReviewGate(declaration, checks, options = {}) {
|
|
|
1320
1366
|
});
|
|
1321
1367
|
}
|
|
1322
1368
|
|
|
1323
|
-
return { violations, states, checked };
|
|
1369
|
+
return { violations, states, descriptions, checked };
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* The check-run conclusion each review-gate verdict is published under.
|
|
1374
|
+
*
|
|
1375
|
+
* THIS IS THE WHOLE POINT OF #3639. Before this map existed the gate's three
|
|
1376
|
+
* verdicts all reached a merge decision through the same rendering: the job
|
|
1377
|
+
* exited 0 for `satisfied` AND for `waived`, so `gh pr checks` printed
|
|
1378
|
+
*
|
|
1379
|
+
* 🕵️ Did the required review checks do any work? pass
|
|
1380
|
+
*
|
|
1381
|
+
* for a pull request nothing had read, character for character what it prints
|
|
1382
|
+
* for one a reviewer completed. The distinction lived in the run log, and the
|
|
1383
|
+
* file's own header had already measured how little that is worth: of the last
|
|
1384
|
+
* 40 merged pull requests, 39 waived. A waiver that applies to 39 of 40 merges
|
|
1385
|
+
* is the default path, and the default path rendering as the success path
|
|
1386
|
+
* reproduces — on the guard's own green — the exact indistinguishability the
|
|
1387
|
+
* guard exists to remove.
|
|
1388
|
+
*
|
|
1389
|
+
* `neutral` is the mechanism because it is the one conclusion that is NOT a
|
|
1390
|
+
* pass and NOT a failure. `gh pr checks` buckets NEUTRAL as `skipping`, so the
|
|
1391
|
+
* row reads differently at a glance, while contributing neither a failure to
|
|
1392
|
+
* gh's exit code nor a satisfied context to branch protection. The waiver keeps
|
|
1393
|
+
* its rationale — a pull request author cannot fix a vendor's billing state,
|
|
1394
|
+
* and reddening every PR on it would be a worse gate — and loses only its
|
|
1395
|
+
* disguise.
|
|
1396
|
+
*/
|
|
1397
|
+
export const REVIEW_VERDICT_CONCLUSIONS = Object.freeze({
|
|
1398
|
+
satisfied: "success",
|
|
1399
|
+
waived: "neutral",
|
|
1400
|
+
unsatisfied: "failure",
|
|
1401
|
+
uninspected: "failure",
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
/** GitHub truncates a check-run output title past this many characters. */
|
|
1405
|
+
export const REVIEW_VERDICT_TITLE_LIMIT = 255;
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* Flattens a title to one line and fits it in a check-run output title.
|
|
1409
|
+
*
|
|
1410
|
+
* Single-line is a correctness requirement, not tidiness: this string is
|
|
1411
|
+
* written to `$GITHUB_OUTPUT` as `key=value`, where a newline in the value ends
|
|
1412
|
+
* the assignment and the remainder is parsed as further output keys. A vendor
|
|
1413
|
+
* description is arbitrary text from outside this repository.
|
|
1414
|
+
*
|
|
1415
|
+
* @param {string} text - The composed title
|
|
1416
|
+
* @returns {string} One line, at most {@link REVIEW_VERDICT_TITLE_LIMIT} chars
|
|
1417
|
+
*/
|
|
1418
|
+
function fitTitle(text) {
|
|
1419
|
+
const flat = String(text).replace(/\s+/gu, " ").trim();
|
|
1420
|
+
return flat.length <= REVIEW_VERDICT_TITLE_LIMIT
|
|
1421
|
+
? flat
|
|
1422
|
+
: `${flat.slice(0, REVIEW_VERDICT_TITLE_LIMIT - 1)}…`;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* The waive-rate sentence appended to a verdict title, or nothing.
|
|
1427
|
+
*
|
|
1428
|
+
* @param {{waived: number, sampled: number}|undefined} rate - A sampled tally
|
|
1429
|
+
* @returns {string} The sentence, or an empty string when nothing was sampled
|
|
1430
|
+
*/
|
|
1431
|
+
function waiveRateSuffix(rate) {
|
|
1432
|
+
return rate === undefined || rate.sampled === 0
|
|
1433
|
+
? ""
|
|
1434
|
+
: ` · waived on ${rate.waived} of the last ${rate.sampled} merged pull requests`;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Renders one merge-time verdict for every declared evidence-bearing check.
|
|
1439
|
+
*
|
|
1440
|
+
* Worst-wins across the declared checks, in the order `unsatisfied` > `waived`
|
|
1441
|
+
* > `satisfied`. One waived check among four satisfied ones still means this
|
|
1442
|
+
* pull request has an unreviewed arm, and a verdict that averaged them would be
|
|
1443
|
+
* the summary that hides the finding.
|
|
1444
|
+
*
|
|
1445
|
+
* Pure, and deliberately separate from the publishing step, so the property
|
|
1446
|
+
* that matters — a waiver and a satisfaction never render the same — is
|
|
1447
|
+
* testable without a GitHub API.
|
|
1448
|
+
*
|
|
1449
|
+
* @param {{states?: Record<string, string>, descriptions?: Record<string, string>, refusal?: {kind: string}|null, waiveRate?: {waived: number, sampled: number}}} reading -
|
|
1450
|
+
* The gate's per-check states, the descriptions they were read from, any
|
|
1451
|
+
* refusal, and an optional sampled waive rate
|
|
1452
|
+
* @returns {{verdict: string, conclusion: string, title: string}} What to publish
|
|
1453
|
+
*/
|
|
1454
|
+
export function reviewGateVerdict(reading = {}) {
|
|
1455
|
+
const states = reading.states ?? {};
|
|
1456
|
+
const descriptions = reading.descriptions ?? {};
|
|
1457
|
+
const names = Object.keys(states);
|
|
1458
|
+
const suffix = waiveRateSuffix(reading.waiveRate);
|
|
1459
|
+
|
|
1460
|
+
if (reading.refusal || names.length === 0) {
|
|
1461
|
+
return {
|
|
1462
|
+
verdict: "uninspected",
|
|
1463
|
+
conclusion: REVIEW_VERDICT_CONCLUSIONS.uninspected,
|
|
1464
|
+
title: fitTitle(
|
|
1465
|
+
`NOT INSPECTED — no review evidence was read${reading.refusal ? ` (${reading.refusal.kind})` : ""}. NOBODY LOOKED is not the same fact as a clean review.`
|
|
1466
|
+
),
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Names one check alongside the exact description its verdict was read from.
|
|
1472
|
+
*
|
|
1473
|
+
* A check with no entry never REPORTED, and says so. `reported ""` for a
|
|
1474
|
+
* check that posted nothing is the one phrasing this whole file exists to
|
|
1475
|
+
* refuse: it makes absence look like a quiet answer.
|
|
1476
|
+
*
|
|
1477
|
+
* @param {string} name - The declared check name
|
|
1478
|
+
* @returns {string} `<name> reported "<description>"`, or that it posted none
|
|
1479
|
+
*/
|
|
1480
|
+
const quote = name =>
|
|
1481
|
+
Object.hasOwn(descriptions, name)
|
|
1482
|
+
? `${name} reported ${JSON.stringify(descriptions[name])}`
|
|
1483
|
+
: `${name} posted NO review status at all`;
|
|
1484
|
+
|
|
1485
|
+
const unsatisfied = names.filter(
|
|
1486
|
+
name => states[name] === REVIEW_GATE_STATES.unsatisfied
|
|
1487
|
+
);
|
|
1488
|
+
if (unsatisfied.length > 0) {
|
|
1489
|
+
return {
|
|
1490
|
+
verdict: REVIEW_GATE_STATES.unsatisfied,
|
|
1491
|
+
conclusion: REVIEW_VERDICT_CONCLUSIONS.unsatisfied,
|
|
1492
|
+
title: fitTitle(
|
|
1493
|
+
`UNREVIEWED — review evidence unsatisfied: ${unsatisfied.map(quote).join("; ")}${suffix}`
|
|
1494
|
+
),
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const waived = names.filter(
|
|
1499
|
+
name => states[name] === REVIEW_GATE_STATES.waived
|
|
1500
|
+
);
|
|
1501
|
+
if (waived.length > 0) {
|
|
1502
|
+
return {
|
|
1503
|
+
verdict: REVIEW_GATE_STATES.waived,
|
|
1504
|
+
conclusion: REVIEW_VERDICT_CONCLUSIONS.waived,
|
|
1505
|
+
title: fitTitle(
|
|
1506
|
+
`WAIVED — this pull request is UNREVIEWED and merging it is a decision taken on that basis: ${waived.map(quote).join("; ")}${suffix}`
|
|
1507
|
+
),
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
return {
|
|
1512
|
+
verdict: REVIEW_GATE_STATES.satisfied,
|
|
1513
|
+
conclusion: REVIEW_VERDICT_CONCLUSIONS.satisfied,
|
|
1514
|
+
title: fitTitle(`REVIEWED — ${names.map(quote).join("; ")}${suffix}`),
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Counts how a sample of pull requests ended up, worst-wins per pull request.
|
|
1520
|
+
*
|
|
1521
|
+
* A pull request whose declared checks produced no state at all is not counted
|
|
1522
|
+
* as reviewed OR waived — it is not counted. A denominator that quietly absorbs
|
|
1523
|
+
* unreadable pull requests reports a lower waive rate than the truth, which is
|
|
1524
|
+
* the direction that makes the number reassuring rather than useful.
|
|
1525
|
+
*
|
|
1526
|
+
* @param {ReadonlyArray<Record<string, string>>} samples - One states map per pull request
|
|
1527
|
+
* @returns {{sampled: number, waived: number, satisfied: number, unsatisfied: number}} The tally
|
|
1528
|
+
*/
|
|
1529
|
+
export function summarizeWaiveRate(samples) {
|
|
1530
|
+
const tally = { sampled: 0, waived: 0, satisfied: 0, unsatisfied: 0 };
|
|
1531
|
+
for (const states of samples) {
|
|
1532
|
+
const values = Object.values(states ?? {});
|
|
1533
|
+
if (values.length === 0) continue;
|
|
1534
|
+
tally.sampled += 1;
|
|
1535
|
+
if (values.includes(REVIEW_GATE_STATES.unsatisfied)) tally.unsatisfied += 1;
|
|
1536
|
+
else if (values.includes(REVIEW_GATE_STATES.waived)) tally.waived += 1;
|
|
1537
|
+
else tally.satisfied += 1;
|
|
1538
|
+
}
|
|
1539
|
+
return tally;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* Reads the declared evidence-bearing rows for recently merged pull requests.
|
|
1544
|
+
*
|
|
1545
|
+
* ONE GraphQL call, not one REST call per pull request. A per-PR loop is ~2N
|
|
1546
|
+
* network reads inside a job that is already spending five minutes waiting for
|
|
1547
|
+
* a review bot to settle, and a sampler that can time the job out would be
|
|
1548
|
+
* deleted rather than read.
|
|
1549
|
+
*
|
|
1550
|
+
* `orderBy: UPDATED_AT` rather than a merge timestamp, and the difference is
|
|
1551
|
+
* worth naming: this is "recently touched merged pull requests", a proxy for
|
|
1552
|
+
* "recently merged" that the single-call shape buys. The number it produces is
|
|
1553
|
+
* an order-of-magnitude reading of how often the waiver is the path — which is
|
|
1554
|
+
* what an operator needs — not an audit figure.
|
|
1555
|
+
*
|
|
1556
|
+
* @param {number} limit - How many merged pull requests to read
|
|
1557
|
+
* @param {string} [repo] - `OWNER/NAME`; defaults to the current repository
|
|
1558
|
+
* @returns {Array<Array<{name: string, state: string, description: string}>>} Rows per pull request
|
|
1559
|
+
* @throws {Error} When the slug cannot be resolved or `gh` cannot answer
|
|
1560
|
+
*/
|
|
1561
|
+
export function fetchRecentMergedChecks(limit, repo) {
|
|
1562
|
+
const slug = resolveRepoSlug(repo);
|
|
1563
|
+
if (slug === undefined) {
|
|
1564
|
+
throw new Error(
|
|
1565
|
+
"check-skipped-required-checks: the waive-rate sample needs an OWNER/NAME. Pass `--repo=OWNER/NAME`, or set GITHUB_REPOSITORY."
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
const [owner, name] = slug.split("/");
|
|
1569
|
+
const raw = boundedExecFileSync(
|
|
1570
|
+
"gh",
|
|
1571
|
+
[
|
|
1572
|
+
"api",
|
|
1573
|
+
"graphql",
|
|
1574
|
+
"-f",
|
|
1575
|
+
`query=${RECENT_MERGED_CHECKS_QUERY}`,
|
|
1576
|
+
"-F",
|
|
1577
|
+
`owner=${owner}`,
|
|
1578
|
+
"-F",
|
|
1579
|
+
`name=${name}`,
|
|
1580
|
+
"-F",
|
|
1581
|
+
`limit=${limit}`,
|
|
1582
|
+
],
|
|
1583
|
+
{ encoding: "utf8" }
|
|
1584
|
+
);
|
|
1585
|
+
const nodes = JSON.parse(raw)?.data?.repository?.pullRequests?.nodes ?? [];
|
|
1586
|
+
return nodes.map(node =>
|
|
1587
|
+
(
|
|
1588
|
+
node?.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes ??
|
|
1589
|
+
[]
|
|
1590
|
+
).map(context =>
|
|
1591
|
+
context?.__typename === "CheckRun"
|
|
1592
|
+
? normalizeCheckRow(context.name, context.conclusion, context.title)
|
|
1593
|
+
: normalizeCheckRow(
|
|
1594
|
+
context?.context,
|
|
1595
|
+
context?.state,
|
|
1596
|
+
context?.description
|
|
1597
|
+
)
|
|
1598
|
+
)
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
/**
|
|
1603
|
+
* Samples how often the review gate waives, best-effort and never fatal.
|
|
1604
|
+
*
|
|
1605
|
+
* A failed sample must never change the verdict. The rate is CONTEXT for a
|
|
1606
|
+
* decision the gate has already made from this pull request's own evidence, so
|
|
1607
|
+
* an unreachable API costs an operator one sentence rather than a red build —
|
|
1608
|
+
* and the absent sentence is the honest rendering of "not measured", which is
|
|
1609
|
+
* what the caller prints in its place.
|
|
1610
|
+
*
|
|
1611
|
+
* @param {object} declaration - The per-repo declaration
|
|
1612
|
+
* @param {number} limit - How many merged pull requests to read; 0 disables
|
|
1613
|
+
* @param {string} [repo] - `OWNER/NAME`; defaults to the current repository
|
|
1614
|
+
* @param {(limit: number, repo?: string) => Array<Array<object>>} [fetch] - Injection seam
|
|
1615
|
+
* @returns {{sampled: number, waived: number, satisfied: number, unsatisfied: number}|undefined} The tally, or undefined when it could not be taken
|
|
1616
|
+
*/
|
|
1617
|
+
export function sampleWaiveRate(declaration, limit, repo, fetch) {
|
|
1618
|
+
if (!Number.isInteger(limit) || limit <= 0) return undefined;
|
|
1619
|
+
try {
|
|
1620
|
+
const perPullRequest = (fetch ?? fetchRecentMergedChecks)(limit, repo);
|
|
1621
|
+
return summarizeWaiveRate(
|
|
1622
|
+
perPullRequest.map(
|
|
1623
|
+
checks => evaluateReviewGate(declaration, checks).states
|
|
1624
|
+
)
|
|
1625
|
+
);
|
|
1626
|
+
} catch {
|
|
1627
|
+
return undefined;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/**
|
|
1632
|
+
* Publishes the verdict where a workflow step can read it.
|
|
1633
|
+
*
|
|
1634
|
+
* `$GITHUB_OUTPUT` rather than stdout because the consumer is the next step in
|
|
1635
|
+
* the job, which turns it into a check run — the surface a merge decision
|
|
1636
|
+
* actually consults. Best-effort for the same reason the step summary is:
|
|
1637
|
+
* losing the rendering must not invert the exit code the guard worked to earn.
|
|
1638
|
+
*
|
|
1639
|
+
* @param {{verdict: string, conclusion: string, title: string}} verdict - What to publish
|
|
1640
|
+
* @param {NodeJS.ProcessEnv} [env] - Environment, injectable for tests
|
|
1641
|
+
* @returns {boolean} True when the outputs were written
|
|
1642
|
+
*/
|
|
1643
|
+
export function writeVerdictOutputs(verdict, env = process.env) {
|
|
1644
|
+
const target = env.GITHUB_OUTPUT;
|
|
1645
|
+
if (target === undefined || target === "") return false;
|
|
1646
|
+
try {
|
|
1647
|
+
appendFileSync(
|
|
1648
|
+
target,
|
|
1649
|
+
`review_evidence_verdict=${verdict.verdict}\nreview_evidence_conclusion=${verdict.conclusion}\nreview_evidence_title=${verdict.title}\n`
|
|
1650
|
+
);
|
|
1651
|
+
return true;
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
console.error(
|
|
1654
|
+
`[skipped-required-checks] review verdict outputs not written: ${error.message}`
|
|
1655
|
+
);
|
|
1656
|
+
return false;
|
|
1657
|
+
}
|
|
1324
1658
|
}
|
|
1325
1659
|
|
|
1326
1660
|
/**
|
|
@@ -1917,6 +2251,26 @@ export function vacuityRefusal(input) {
|
|
|
1917
2251
|
return null;
|
|
1918
2252
|
}
|
|
1919
2253
|
|
|
2254
|
+
/**
|
|
2255
|
+
* Reads a `--name=<integer>` flag, falling back when absent or unreadable.
|
|
2256
|
+
*
|
|
2257
|
+
* Separate from the seconds reader because the fallback for an unreadable value
|
|
2258
|
+
* differs in kind: a bad timeout should still wait, while a bad sample size must
|
|
2259
|
+
* NOT silently become the shipped default and start making network calls a
|
|
2260
|
+
* caller did not ask for. Both clamp at zero.
|
|
2261
|
+
*
|
|
2262
|
+
* @param {ReadonlyArray<string>} argv - CLI arguments
|
|
2263
|
+
* @param {string} name - The flag, including its leading dashes
|
|
2264
|
+
* @param {number} fallback - Value to use when the flag is absent or unreadable
|
|
2265
|
+
* @returns {number} A non-negative integer
|
|
2266
|
+
*/
|
|
2267
|
+
function readIntegerFlag(argv, name, fallback) {
|
|
2268
|
+
const raw = readFlagValue(argv, name);
|
|
2269
|
+
if (raw === undefined) return fallback;
|
|
2270
|
+
const value = Number(raw);
|
|
2271
|
+
return Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
1920
2274
|
/**
|
|
1921
2275
|
* Reads a `--name=<seconds>` flag, falling back when absent or unreadable.
|
|
1922
2276
|
*
|
|
@@ -1968,8 +2322,19 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
1968
2322
|
violations: [],
|
|
1969
2323
|
settled: false,
|
|
1970
2324
|
};
|
|
2325
|
+
/**
|
|
2326
|
+
* Attaches the published verdict to a refusal, which must not render as green.
|
|
2327
|
+
*
|
|
2328
|
+
* @param {{kind: string, reason: string}|null} refusal - Why nothing was read
|
|
2329
|
+
* @returns {object} The refusal result, verdict included
|
|
2330
|
+
*/
|
|
2331
|
+
const refused = refusal => ({
|
|
2332
|
+
...empty,
|
|
2333
|
+
refusal,
|
|
2334
|
+
verdict: reviewGateVerdict({ refusal }),
|
|
2335
|
+
});
|
|
1971
2336
|
if (pr === undefined) {
|
|
1972
|
-
return
|
|
2337
|
+
return refused(vacuityRefusal({ declaration, pr }));
|
|
1973
2338
|
}
|
|
1974
2339
|
|
|
1975
2340
|
const repo = readFlagValue(argv, "--repo");
|
|
@@ -1992,16 +2357,15 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
1992
2357
|
headSha: options.headSha,
|
|
1993
2358
|
});
|
|
1994
2359
|
} catch (error) {
|
|
1995
|
-
return
|
|
2360
|
+
return refused(vacuityRefusal({ declaration, pr, error }));
|
|
1996
2361
|
}
|
|
1997
2362
|
|
|
1998
2363
|
const refusal = vacuityRefusal({ declaration, pr, checks: read.checks });
|
|
1999
2364
|
if (refusal !== null) {
|
|
2000
2365
|
return {
|
|
2001
|
-
...
|
|
2366
|
+
...refused(refusal),
|
|
2002
2367
|
headSha: read.headSha,
|
|
2003
2368
|
settled: read.settled,
|
|
2004
|
-
refusal,
|
|
2005
2369
|
};
|
|
2006
2370
|
}
|
|
2007
2371
|
const evaluated = evaluateVacuousChecks(declaration, read.checks, {
|
|
@@ -2015,6 +2379,20 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
2015
2379
|
const gate = evaluateReviewGate(declaration, read.checks, {
|
|
2016
2380
|
headSha: read.headSha,
|
|
2017
2381
|
});
|
|
2382
|
+
// Sampled ONLY when this pull request is itself waived. The rate answers
|
|
2383
|
+
// "is this the exception or the rule?", a question that only arises once a
|
|
2384
|
+
// waiver is on the table, and every other run keeps the sampler's network
|
|
2385
|
+
// call — and its failure modes — out of the job entirely.
|
|
2386
|
+
const waiveRate = Object.values(gate.states).includes(
|
|
2387
|
+
REVIEW_GATE_STATES.waived
|
|
2388
|
+
)
|
|
2389
|
+
? sampleWaiveRate(
|
|
2390
|
+
declaration,
|
|
2391
|
+
readIntegerFlag(argv, WAIVE_RATE_SAMPLE_FLAG, 0),
|
|
2392
|
+
repo,
|
|
2393
|
+
options.sampleChecks
|
|
2394
|
+
)
|
|
2395
|
+
: undefined;
|
|
2018
2396
|
return {
|
|
2019
2397
|
pr,
|
|
2020
2398
|
prSource: source,
|
|
@@ -2022,6 +2400,13 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
2022
2400
|
checked: evaluated.checked,
|
|
2023
2401
|
violations: [...evaluated.violations, ...gate.violations],
|
|
2024
2402
|
gateStates: gate.states,
|
|
2403
|
+
gateDescriptions: gate.descriptions,
|
|
2404
|
+
waiveRate,
|
|
2405
|
+
verdict: reviewGateVerdict({
|
|
2406
|
+
states: gate.states,
|
|
2407
|
+
descriptions: gate.descriptions,
|
|
2408
|
+
waiveRate,
|
|
2409
|
+
}),
|
|
2025
2410
|
settled: read.settled,
|
|
2026
2411
|
refusal: null,
|
|
2027
2412
|
};
|
|
@@ -2081,6 +2466,8 @@ export function runGuard(argv, options = {}) {
|
|
|
2081
2466
|
recipe: transcriptionRecipe(declaration),
|
|
2082
2467
|
pr: vacuity?.pr,
|
|
2083
2468
|
evidenceChecked: vacuity?.checked ?? 0,
|
|
2469
|
+
verdict: vacuity?.verdict,
|
|
2470
|
+
waiveRate: vacuity?.waiveRate,
|
|
2084
2471
|
vacuity,
|
|
2085
2472
|
};
|
|
2086
2473
|
}
|
|
@@ -2141,6 +2528,11 @@ function main(argv) {
|
|
|
2141
2528
|
: !warnOnly || ALWAYS_BLOCKING.includes(violation.kind);
|
|
2142
2529
|
};
|
|
2143
2530
|
const blocking = result.violations.filter(blocks);
|
|
2531
|
+
// Published BEFORE the report and before the exit code, because it is the only
|
|
2532
|
+
// artefact of this run that a merge decision reads. A verdict computed after a
|
|
2533
|
+
// `return` on some branch is a verdict that exists on the happy path only.
|
|
2534
|
+
const verdict = result.verdict;
|
|
2535
|
+
if (verdict !== undefined) writeVerdictOutputs(verdict);
|
|
2144
2536
|
const refusal = result.vacuity?.refusal ?? null;
|
|
2145
2537
|
const refusalBlocks =
|
|
2146
2538
|
refusal !== null && (!warnOnly || requireReviewEvidence);
|
|
@@ -2169,6 +2561,22 @@ function main(argv) {
|
|
|
2169
2561
|
|
|
2170
2562
|
const lines = ["## 🔒 Required checks that prove nothing", ""];
|
|
2171
2563
|
|
|
2564
|
+
// The verdict is the FIRST line of the report and not a footnote, and it is
|
|
2565
|
+
// the same string published to the check run, so the two surfaces cannot
|
|
2566
|
+
// drift into disagreeing about what this run concluded.
|
|
2567
|
+
if (verdict !== undefined) {
|
|
2568
|
+
lines.push(
|
|
2569
|
+
`**${verdict.title}**`,
|
|
2570
|
+
"",
|
|
2571
|
+
`Published as check-run conclusion \`${verdict.conclusion}\`. A \`neutral\` conclusion is how a WAIVED pull request is kept out of the shape a satisfied one prints — the waiver still blocks nothing, and it no longer looks like a review.`,
|
|
2572
|
+
"",
|
|
2573
|
+
result.waiveRate === undefined
|
|
2574
|
+
? "The waive rate over recently merged pull requests was NOT sampled on this run, so no rate is claimed here."
|
|
2575
|
+
: `Recently merged pull requests sampled: ${result.waiveRate.sampled} — ${result.waiveRate.satisfied} reviewed, ${result.waiveRate.waived} waived, ${result.waiveRate.unsatisfied} unsatisfied.`,
|
|
2576
|
+
""
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2172
2580
|
// The refusal comes FIRST and replaces the verdict. Printing "✅ none
|
|
2173
2581
|
// silences a required check" from a snapshot nobody transcribed is the one
|
|
2174
2582
|
// outcome that is worse than never running: it is a confident wrong answer,
|