@codyswann/lisa 4.84.0 → 4.84.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/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +6 -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.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +2 -1
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- 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-cdk-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-devin/.devin-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-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/.devin-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-expo-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-devin/.devin-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-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.devin-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-nestjs-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-devin/.devin-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-openclaw-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-devin/.devin-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-phaser-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.devin-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-rails-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.devin-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-typescript-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-devin/.devin-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/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +276 -6
|
@@ -2129,6 +2129,12 @@ export const REVIEW_GATE_CONDITIONS = Object.freeze({
|
|
|
2129
2129
|
pending: "pending",
|
|
2130
2130
|
unrecognised: "unrecognised",
|
|
2131
2131
|
undetermined: "undetermined",
|
|
2132
|
+
// A SATISFYING condition, and only ever reached when the reviewer posted NO
|
|
2133
|
+
// status row at all (`absent`, or `undetermined` while still unreported —
|
|
2134
|
+
// never a row still in flight), and a writer posted a local
|
|
2135
|
+
// adversarial review for this exact head in its place. Never from
|
|
2136
|
+
// `objected` — a substitute does not overrule a review that ran and said no.
|
|
2137
|
+
substituted: "substituted",
|
|
2132
2138
|
});
|
|
2133
2139
|
|
|
2134
2140
|
/**
|
|
@@ -2500,6 +2506,17 @@ export function evaluateReviewGate(declaration, checks, options = {}) {
|
|
|
2500
2506
|
},
|
|
2501
2507
|
vocabulary
|
|
2502
2508
|
);
|
|
2509
|
+
// Only a reviewer that NEVER REPORTED can be stood in for, keyed on the
|
|
2510
|
+
// missing row rather than on the condition: `undetermined` is also what a
|
|
2511
|
+
// reviewer STILL RUNNING reads as once the wait expires, and a substitute
|
|
2512
|
+
// must not race a review in flight whose objection would land after the
|
|
2513
|
+
// merge. One that is pending, said something unrecognised, or objected
|
|
2514
|
+
// has spoken, and a substitute does not get to overrule it.
|
|
2515
|
+
if (found === undefined && options.substitutes?.has(name) === true) {
|
|
2516
|
+
states[name] = REVIEW_GATE_STATES.satisfied;
|
|
2517
|
+
conditions[name] = REVIEW_GATE_CONDITIONS.substituted;
|
|
2518
|
+
continue;
|
|
2519
|
+
}
|
|
2503
2520
|
states[name] = verdict.state;
|
|
2504
2521
|
conditions[name] = verdict.condition;
|
|
2505
2522
|
// Assigned ONLY when the check reported. An absent check with a `""`
|
|
@@ -2542,6 +2559,191 @@ export function evaluateReviewGate(declaration, checks, options = {}) {
|
|
|
2542
2559
|
*/
|
|
2543
2560
|
export const REVIEW_OBJECTION_STATE = "CHANGES_REQUESTED";
|
|
2544
2561
|
|
|
2562
|
+
/**
|
|
2563
|
+
* The marker a local adversarial review carries when it stands in for a
|
|
2564
|
+
* declared reviewer that never reported.
|
|
2565
|
+
*
|
|
2566
|
+
* Byte-identical to `substituteMarker` in
|
|
2567
|
+
* `check-third-party-review-evidence.mjs`, which is the format
|
|
2568
|
+
* `lisa-drive-pr-to-merge` posts. Duplicated rather than imported because the
|
|
2569
|
+
* two scripts ship from different template trees, and a host that has one need
|
|
2570
|
+
* not have the other.
|
|
2571
|
+
*
|
|
2572
|
+
* WHY THIS GATE READS IT AT ALL. A repository that declares a reviewer it has
|
|
2573
|
+
* not installed — or whose reviewer is down — saw this gate red on EVERY pull
|
|
2574
|
+
* request, with no route to green that did not also delete the declaration.
|
|
2575
|
+
* The response arm already accepted a posted substitute; this arm did not, so
|
|
2576
|
+
* the two published opposite verdicts for the same head.
|
|
2577
|
+
*/
|
|
2578
|
+
export const REVIEW_SUBSTITUTE_MARKER = "lisa:review-substitute";
|
|
2579
|
+
|
|
2580
|
+
/** Repository permissions whose holder may stand in for a reviewer. */
|
|
2581
|
+
const SUBSTITUTE_WRITER_PERMISSIONS = new Set(["admin", "maintain", "write"]);
|
|
2582
|
+
|
|
2583
|
+
/**
|
|
2584
|
+
* The lead of a satisfied verdict, which says whether a substitute was involved.
|
|
2585
|
+
*
|
|
2586
|
+
* @param {ReadonlyArray<string>} names - The declared reviewers
|
|
2587
|
+
* @param {Record<string, string>} conditions - Each reviewer's condition
|
|
2588
|
+
* @returns {string} `REVIEWED`, or a SUBSTITUTED lead accurate for all or some
|
|
2589
|
+
*/
|
|
2590
|
+
function substitutedLead(names, conditions) {
|
|
2591
|
+
const substituted = names.filter(
|
|
2592
|
+
name => conditions[name] === REVIEW_GATE_CONDITIONS.substituted
|
|
2593
|
+
);
|
|
2594
|
+
if (substituted.length === 0) return "REVIEWED";
|
|
2595
|
+
if (substituted.length === names.length) {
|
|
2596
|
+
return "SUBSTITUTED — no third-party review; a local adversarial review stands in";
|
|
2597
|
+
}
|
|
2598
|
+
return `SUBSTITUTED IN PART — a local adversarial review stands in for ${substituted.length} of ${names.length} declared reviewers; the rest reviewed`;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
/**
|
|
2602
|
+
* The marker line a substitute carries for one reviewer at one commit.
|
|
2603
|
+
*
|
|
2604
|
+
* @param {string} context - The declared evidence-bearing check name
|
|
2605
|
+
* @param {string} sha - The head commit the substitute reviewed
|
|
2606
|
+
* @returns {string} The marker line
|
|
2607
|
+
*/
|
|
2608
|
+
export function reviewSubstituteMarker(context, sha) {
|
|
2609
|
+
return `<!-- ${REVIEW_SUBSTITUTE_MARKER} context="${context}" head="${sha}" -->`;
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
/**
|
|
2613
|
+
* Which declared checks a WRITER posted a substitute for at exactly this head.
|
|
2614
|
+
*
|
|
2615
|
+
* Stricter than the response arm, on purpose, because here a match turns a
|
|
2616
|
+
* blocking verdict green:
|
|
2617
|
+
*
|
|
2618
|
+
* - The marker must be the comment's FIRST LINE. A comment that merely quotes
|
|
2619
|
+
* the marker — instructions, a pasted log — is not a review.
|
|
2620
|
+
* - The author must hold write access. Anyone who can comment can type the
|
|
2621
|
+
* marker; only someone who could have merged anyway can vouch for a review.
|
|
2622
|
+
* - Evidence is per reviewer and per head: a substitute for another check, or
|
|
2623
|
+
* for an earlier push, satisfies nothing.
|
|
2624
|
+
*
|
|
2625
|
+
* @param {ReadonlyArray<{body?: string, author?: string}>} comments - Issue comments on the pull request
|
|
2626
|
+
* @param {ReadonlyArray<string>} names - Declared evidence-bearing check names
|
|
2627
|
+
* @param {string|undefined} headSha - The head commit under inspection
|
|
2628
|
+
* @param {(login: string) => boolean} canWrite - Whether a login holds write access
|
|
2629
|
+
* @returns {Set<string>} The names a writer's substitute stands in for
|
|
2630
|
+
*/
|
|
2631
|
+
export function substitutesAtHead(comments, names, headSha, canWrite) {
|
|
2632
|
+
const found = new Set();
|
|
2633
|
+
if (headSha === undefined || headSha === "") return found;
|
|
2634
|
+
for (const name of names) {
|
|
2635
|
+
const marker = reviewSubstituteMarker(name, headSha);
|
|
2636
|
+
const posted = (comments ?? []).some(
|
|
2637
|
+
comment =>
|
|
2638
|
+
String(comment?.body ?? "")
|
|
2639
|
+
.split("\n")[0]
|
|
2640
|
+
.replace(/\r$/, "") === marker &&
|
|
2641
|
+
canWrite(String(comment?.author ?? ""))
|
|
2642
|
+
);
|
|
2643
|
+
if (posted) found.add(name);
|
|
2644
|
+
}
|
|
2645
|
+
return found;
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
/**
|
|
2649
|
+
* Reads the issue comments on one pull request.
|
|
2650
|
+
*
|
|
2651
|
+
* @param {string|number} pr - The pull request
|
|
2652
|
+
* @param {string} [repo] - `OWNER/NAME`; defaults to the current repository
|
|
2653
|
+
* @returns {Array<{body: string, author: string}>} The comments
|
|
2654
|
+
*/
|
|
2655
|
+
export function fetchIssueComments(pr, repo) {
|
|
2656
|
+
const slug = resolveRepoSlug(repo);
|
|
2657
|
+
if (slug === undefined) {
|
|
2658
|
+
throw new Error(
|
|
2659
|
+
`check-skipped-required-checks: cannot read the comments on PR ${pr} without an OWNER/NAME. Pass \`--repo=OWNER/NAME\`, or set GITHUB_REPOSITORY.`
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
return ghApiPaginatedArray(
|
|
2663
|
+
`repos/${slug}/issues/${pr}/comments?per_page=100`,
|
|
2664
|
+
"[.[] | {body: .body, author: .user.login}]"
|
|
2665
|
+
);
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
/**
|
|
2669
|
+
* Whether a login holds write access to the repository.
|
|
2670
|
+
*
|
|
2671
|
+
* probe-direction: fail-closed — any failure to read the permission answers
|
|
2672
|
+
* `false`, which can only leave a substitute uncounted, never count one.
|
|
2673
|
+
*
|
|
2674
|
+
* Two known shapes never count, both in the safe direction: a login the
|
|
2675
|
+
* pattern below rejects (Enterprise Managed User logins carry `_shortcode`),
|
|
2676
|
+
* and a bot author such as `github-actions[bot]`, which is not a collaborator.
|
|
2677
|
+
* A substitute posted from CI therefore needs a human writer's comment.
|
|
2678
|
+
*
|
|
2679
|
+
* @param {string} login - The comment author
|
|
2680
|
+
* @param {string} [repo] - `OWNER/NAME`; defaults to the current repository
|
|
2681
|
+
* @returns {boolean} Whether the login may stand in for a reviewer
|
|
2682
|
+
*/
|
|
2683
|
+
export function fetchWritePermission(login, repo) {
|
|
2684
|
+
const slug = resolveRepoSlug(repo);
|
|
2685
|
+
if (slug === undefined || !/^[A-Za-z0-9-]+(\[bot\])?$/.test(login)) {
|
|
2686
|
+
return false;
|
|
2687
|
+
}
|
|
2688
|
+
try {
|
|
2689
|
+
const permission = boundedExecFileSync(
|
|
2690
|
+
"gh",
|
|
2691
|
+
[
|
|
2692
|
+
"api",
|
|
2693
|
+
`repos/${slug}/collaborators/${encodeURIComponent(login)}/permission`,
|
|
2694
|
+
"--jq",
|
|
2695
|
+
".permission",
|
|
2696
|
+
],
|
|
2697
|
+
{ encoding: "utf8" }
|
|
2698
|
+
).trim();
|
|
2699
|
+
return SUBSTITUTE_WRITER_PERMISSIONS.has(permission);
|
|
2700
|
+
} catch {
|
|
2701
|
+
return false;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
/**
|
|
2706
|
+
* Reads which declared checks a writer's substitute stands in for at a head.
|
|
2707
|
+
*
|
|
2708
|
+
* probe-direction: fail-closed — an unreadable comment list returns an empty
|
|
2709
|
+
* set, which leaves the gate exactly where it would have been without this
|
|
2710
|
+
* read. Nothing here can redden a pull request; it can only fail to green one.
|
|
2711
|
+
*
|
|
2712
|
+
* @param {object} declaration - The per-repo declaration
|
|
2713
|
+
* @param {string|number} pr - The pull request under inspection
|
|
2714
|
+
* @param {string|undefined} repo - `OWNER/NAME`; defaults to the current repository
|
|
2715
|
+
* @param {string|undefined} headSha - The head commit under inspection
|
|
2716
|
+
* @param {{fetchComments?: Function, canWrite?: Function}} [options] - Injection seams
|
|
2717
|
+
* @returns {Set<string>} The names a writer's substitute stands in for
|
|
2718
|
+
*/
|
|
2719
|
+
export function readReviewSubstitutes(
|
|
2720
|
+
declaration,
|
|
2721
|
+
pr,
|
|
2722
|
+
repo,
|
|
2723
|
+
headSha,
|
|
2724
|
+
options = {}
|
|
2725
|
+
) {
|
|
2726
|
+
const names = Object.keys(declaration.evidence_bearing_checks ?? {});
|
|
2727
|
+
if (names.length === 0 || headSha === undefined || headSha === "") {
|
|
2728
|
+
return new Set();
|
|
2729
|
+
}
|
|
2730
|
+
let comments;
|
|
2731
|
+
try {
|
|
2732
|
+
comments = (options.fetchComments ?? fetchIssueComments)(pr, repo);
|
|
2733
|
+
} catch {
|
|
2734
|
+
return new Set();
|
|
2735
|
+
}
|
|
2736
|
+
const permission =
|
|
2737
|
+
options.canWrite ?? (login => fetchWritePermission(login, repo));
|
|
2738
|
+
const seen = new Map();
|
|
2739
|
+
const canWrite = login => {
|
|
2740
|
+
if (login === "") return false;
|
|
2741
|
+
if (!seen.has(login)) seen.set(login, permission(login) === true);
|
|
2742
|
+
return seen.get(login);
|
|
2743
|
+
};
|
|
2744
|
+
return substitutesAtHead(comments, names, headSha, canWrite);
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2545
2747
|
/**
|
|
2546
2748
|
* Whether an objection STANDS at this head, from the review objects.
|
|
2547
2749
|
*
|
|
@@ -3001,6 +3203,9 @@ export function reviewGateVerdict(reading = {}) {
|
|
|
3001
3203
|
* @returns {string} `<name> reported "<description>"`, or what the wait did
|
|
3002
3204
|
*/
|
|
3003
3205
|
const quote = name => {
|
|
3206
|
+
if (conditions[name] === REVIEW_GATE_CONDITIONS.substituted) {
|
|
3207
|
+
return `${name} posted NO review status; a writer's ${REVIEW_SUBSTITUTE_MARKER} local adversarial review stands in for it at this head`;
|
|
3208
|
+
}
|
|
3004
3209
|
if (conditions[name] === REVIEW_GATE_CONDITIONS.undetermined) {
|
|
3005
3210
|
return `${name} had not settled when the gate's wait EXPIRED — not observed to be unreviewed`;
|
|
3006
3211
|
}
|
|
@@ -3118,7 +3323,14 @@ export function reviewGateVerdict(reading = {}) {
|
|
|
3118
3323
|
return {
|
|
3119
3324
|
verdict: REVIEW_GATE_STATES.satisfied,
|
|
3120
3325
|
conclusion: REVIEW_VERDICT_CONCLUSIONS.satisfied,
|
|
3121
|
-
|
|
3326
|
+
// SUBSTITUTED is not REVIEWED. A self-review standing in for a third-party
|
|
3327
|
+
// one is a weaker claim, and the title is what gets quoted into a merge
|
|
3328
|
+
// disclosure, so it says which one this was.
|
|
3329
|
+
// And it is only "no third-party review" when that is true of EVERY
|
|
3330
|
+
// declared reviewer: a mixed result had a real review alongside the stand-in.
|
|
3331
|
+
title: fitTitle(
|
|
3332
|
+
`${substitutedLead(names, conditions)} — ${names.map(quote).join("; ")}${suffix}`
|
|
3333
|
+
),
|
|
3122
3334
|
};
|
|
3123
3335
|
}
|
|
3124
3336
|
|
|
@@ -4207,13 +4419,38 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
4207
4419
|
}
|
|
4208
4420
|
|
|
4209
4421
|
const repo = readFlagValue(argv, "--repo");
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4422
|
+
// Substitutes are read only when the gate is armed, like every other review
|
|
4423
|
+
// read here. Read BEFORE the settle wait as well as after it: when a writer
|
|
4424
|
+
// has already stood in for every declared reviewer, waiting five minutes for
|
|
4425
|
+
// a reviewer that is not coming proves nothing and only delays the green.
|
|
4426
|
+
const readSubstitutes = sha =>
|
|
4427
|
+
requireReviewEvidence
|
|
4428
|
+
? (options.readSubstitutes ?? readReviewSubstitutes)(
|
|
4429
|
+
declaration,
|
|
4430
|
+
pr,
|
|
4431
|
+
repo,
|
|
4432
|
+
sha,
|
|
4433
|
+
options
|
|
4434
|
+
)
|
|
4435
|
+
: new Set();
|
|
4436
|
+
const declaredReviewers = Object.keys(
|
|
4437
|
+
declaration.evidence_bearing_checks ?? {}
|
|
4438
|
+
);
|
|
4439
|
+
const earlyHead = requireReviewEvidence
|
|
4440
|
+
? (options.headSha ?? resolveHeadSha)(pr, repo)
|
|
4441
|
+
: undefined;
|
|
4442
|
+
const earlySubstitutes = readSubstitutes(earlyHead);
|
|
4443
|
+
const allStoodIn =
|
|
4444
|
+
declaredReviewers.length > 0 &&
|
|
4445
|
+
declaredReviewers.every(name => earlySubstitutes.has(name));
|
|
4446
|
+
const settle = skipWait =>
|
|
4447
|
+
fetchSettledChecks(declaration, pr, repo, {
|
|
4213
4448
|
timeoutSeconds: readSecondsFlag(
|
|
4214
4449
|
argv,
|
|
4215
4450
|
"--settle-timeout",
|
|
4216
|
-
wired || requireReviewEvidence
|
|
4451
|
+
(wired || requireReviewEvidence) && !skipWait
|
|
4452
|
+
? SETTLE_TIMEOUT_SECONDS
|
|
4453
|
+
: 0
|
|
4217
4454
|
),
|
|
4218
4455
|
intervalSeconds: readSecondsFlag(
|
|
4219
4456
|
argv,
|
|
@@ -4225,6 +4462,22 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
4225
4462
|
sleep: options.sleep,
|
|
4226
4463
|
headSha: options.headSha,
|
|
4227
4464
|
});
|
|
4465
|
+
let read;
|
|
4466
|
+
try {
|
|
4467
|
+
read = settle(allStoodIn);
|
|
4468
|
+
// The wait is skipped only for a reviewer that is NOT COMING. A declared
|
|
4469
|
+
// reviewer whose row is already there but still in flight is coming, and
|
|
4470
|
+
// a substitute does not stand in for it — so wait for its answer exactly
|
|
4471
|
+
// as an ordinary run would, rather than going red on one snapshot.
|
|
4472
|
+
if (
|
|
4473
|
+
allStoodIn &&
|
|
4474
|
+
!read.settled &&
|
|
4475
|
+
declaredReviewers.some(name =>
|
|
4476
|
+
read.checks.some(check => check.name === name)
|
|
4477
|
+
)
|
|
4478
|
+
) {
|
|
4479
|
+
read = settle(false);
|
|
4480
|
+
}
|
|
4228
4481
|
} catch (error) {
|
|
4229
4482
|
return refused(vacuityRefusal({ declaration, pr, error }));
|
|
4230
4483
|
}
|
|
@@ -4263,6 +4516,14 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
4263
4516
|
// wait expired", and it is the only place that fact exists — every reader
|
|
4264
4517
|
// downstream sees a check row that looks identical either way (#3716).
|
|
4265
4518
|
waitExpired: read.settled === false,
|
|
4519
|
+
// Bound to the head the checks were read at. A push between the early read
|
|
4520
|
+
// and the settled one invalidates every substitute for the old head.
|
|
4521
|
+
// Reused only when it was COMPLETE: a substitute posted while the gate
|
|
4522
|
+
// waited is only seen by reading the comments again.
|
|
4523
|
+
substitutes:
|
|
4524
|
+
allStoodIn && read.headSha !== undefined && read.headSha === earlyHead
|
|
4525
|
+
? earlySubstitutes
|
|
4526
|
+
: readSubstitutes(read.headSha),
|
|
4266
4527
|
};
|
|
4267
4528
|
// TWO PASSES, AND THE SECOND IS PAID FOR ONLY BY A WAIVER (#3706). The review
|
|
4268
4529
|
// surface is read exactly when a waiver is on the table, which is the only
|
|
@@ -4317,7 +4578,16 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
4317
4578
|
headSha: read.headSha,
|
|
4318
4579
|
checked: evaluated.checked,
|
|
4319
4580
|
violations: [
|
|
4320
|
-
|
|
4581
|
+
// "Did not report… NOBODY LOOKED" is false once a writer's substitute
|
|
4582
|
+
// stands in for that exact check at this head, so it is not repeated.
|
|
4583
|
+
...evaluated.violations.filter(
|
|
4584
|
+
violation =>
|
|
4585
|
+
!(
|
|
4586
|
+
violation.kind === VIOLATIONS.unproven &&
|
|
4587
|
+
gate.conditions[violation.token] ===
|
|
4588
|
+
REVIEW_GATE_CONDITIONS.substituted
|
|
4589
|
+
)
|
|
4590
|
+
),
|
|
4321
4591
|
...absent.violations,
|
|
4322
4592
|
...gate.violations,
|
|
4323
4593
|
...(carried?.violations ?? []),
|