@codyswann/lisa 4.54.5 → 4.54.7
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.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +13 -11
- 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/.codex-plugin/skills/lisa-github-validate-issue/SKILL.md +19 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-github-verify/SKILL.md +17 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-github-write-issue/SKILL.md +35 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-validate-ticket/SKILL.md +19 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-verify/SKILL.md +17 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-write-ticket/SKILL.md +35 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-validate-issue/SKILL.md +19 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-verify/SKILL.md +17 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-write-issue/SKILL.md +35 -0
- package/plugins/lisa/skills/lisa-github-validate-issue/SKILL.md +19 -0
- package/plugins/lisa/skills/lisa-github-verify/SKILL.md +17 -0
- package/plugins/lisa/skills/lisa-github-write-issue/SKILL.md +35 -0
- package/plugins/lisa/skills/lisa-jira-validate-ticket/SKILL.md +19 -0
- package/plugins/lisa/skills/lisa-jira-verify/SKILL.md +17 -0
- package/plugins/lisa/skills/lisa-jira-write-ticket/SKILL.md +35 -0
- package/plugins/lisa/skills/lisa-linear-validate-issue/SKILL.md +19 -0
- package/plugins/lisa/skills/lisa-linear-verify/SKILL.md +17 -0
- package/plugins/lisa/skills/lisa-linear-write-issue/SKILL.md +35 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-github-validate-issue/SKILL.md +19 -0
- package/plugins/lisa-agy/skills/lisa-github-verify/SKILL.md +17 -0
- package/plugins/lisa-agy/skills/lisa-github-write-issue/SKILL.md +35 -0
- package/plugins/lisa-agy/skills/lisa-jira-validate-ticket/SKILL.md +19 -0
- package/plugins/lisa-agy/skills/lisa-jira-verify/SKILL.md +17 -0
- package/plugins/lisa-agy/skills/lisa-jira-write-ticket/SKILL.md +35 -0
- package/plugins/lisa-agy/skills/lisa-linear-validate-issue/SKILL.md +19 -0
- package/plugins/lisa-agy/skills/lisa-linear-verify/SKILL.md +17 -0
- package/plugins/lisa-agy/skills/lisa-linear-write-issue/SKILL.md +35 -0
- 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-copilot/skills/lisa-github-validate-issue/SKILL.md +19 -0
- package/plugins/lisa-copilot/skills/lisa-github-verify/SKILL.md +17 -0
- package/plugins/lisa-copilot/skills/lisa-github-write-issue/SKILL.md +35 -0
- package/plugins/lisa-copilot/skills/lisa-jira-validate-ticket/SKILL.md +19 -0
- package/plugins/lisa-copilot/skills/lisa-jira-verify/SKILL.md +17 -0
- package/plugins/lisa-copilot/skills/lisa-jira-write-ticket/SKILL.md +35 -0
- package/plugins/lisa-copilot/skills/lisa-linear-validate-issue/SKILL.md +19 -0
- package/plugins/lisa-copilot/skills/lisa-linear-verify/SKILL.md +17 -0
- package/plugins/lisa-copilot/skills/lisa-linear-write-issue/SKILL.md +35 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-github-validate-issue/SKILL.md +19 -0
- package/plugins/lisa-cursor/skills/lisa-github-verify/SKILL.md +17 -0
- package/plugins/lisa-cursor/skills/lisa-github-write-issue/SKILL.md +35 -0
- package/plugins/lisa-cursor/skills/lisa-jira-validate-ticket/SKILL.md +19 -0
- package/plugins/lisa-cursor/skills/lisa-jira-verify/SKILL.md +17 -0
- package/plugins/lisa-cursor/skills/lisa-jira-write-ticket/SKILL.md +35 -0
- package/plugins/lisa-cursor/skills/lisa-linear-validate-issue/SKILL.md +19 -0
- package/plugins/lisa-cursor/skills/lisa-linear-verify/SKILL.md +17 -0
- package/plugins/lisa-cursor/skills/lisa-linear-write-issue/SKILL.md +35 -0
- 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/plugins/src/base/skills/lisa-github-validate-issue/SKILL.md +19 -0
- package/plugins/src/base/skills/lisa-github-verify/SKILL.md +17 -0
- package/plugins/src/base/skills/lisa-github-write-issue/SKILL.md +35 -0
- package/plugins/src/base/skills/lisa-jira-validate-ticket/SKILL.md +19 -0
- package/plugins/src/base/skills/lisa-jira-verify/SKILL.md +17 -0
- package/plugins/src/base/skills/lisa-jira-write-ticket/SKILL.md +35 -0
- package/plugins/src/base/skills/lisa-linear-validate-issue/SKILL.md +19 -0
- package/plugins/src/base/skills/lisa-linear-verify/SKILL.md +17 -0
- package/plugins/src/base/skills/lisa-linear-write-issue/SKILL.md +35 -0
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +326 -16
- package/typescript/create-only/.github/workflows/review-evidence.yml +29 -0
|
@@ -1632,6 +1632,16 @@ export const REVIEW_GATE_STATES = Object.freeze({
|
|
|
1632
1632
|
* | `unrecognised` | the reviewer said something new | classify the phrase |
|
|
1633
1633
|
* | `undetermined` | THE GATE STOPPED WAITING | re-run; read nothing into it |
|
|
1634
1634
|
*
|
|
1635
|
+
* `waived_with_review` is the eighth, and it is a WAIVER — same severity, same
|
|
1636
|
+
* `neutral`, same never-blocking kind. It exists because the waiver's sentence
|
|
1637
|
+
* was a claim rather than a label: "this pull request is UNREVIEWED". MEASURED
|
|
1638
|
+
* on this repository 2026-09-06 over the last 40 merged pull requests, 34 of
|
|
1639
|
+
* which reported `Review rate limited`: SEVEN of those 34 carried review
|
|
1640
|
+
* activity at the head. The waiver described one waived merge in five wrongly,
|
|
1641
|
+
* and it described it wrongly in the direction that matters — an operator
|
|
1642
|
+
* writing a merge disclosure from that sentence asserts nobody looked while a
|
|
1643
|
+
* finding sits on the pull request.
|
|
1644
|
+
*
|
|
1635
1645
|
* All of them published one word, so an operator handed `unsatisfied` had to
|
|
1636
1646
|
* guess between situations whose correct responses have nothing in common. That
|
|
1637
1647
|
* collapse is why CodySwannGT/lisa#3706, #3716 and #3600 each described a
|
|
@@ -1666,6 +1676,7 @@ export const REVIEW_GATE_STATES = Object.freeze({
|
|
|
1666
1676
|
export const REVIEW_GATE_CONDITIONS = Object.freeze({
|
|
1667
1677
|
satisfied: "satisfied",
|
|
1668
1678
|
waived: "waived",
|
|
1679
|
+
waivedWithReview: "waived_with_review",
|
|
1669
1680
|
absent: "absent",
|
|
1670
1681
|
objected: "objected",
|
|
1671
1682
|
pending: "pending",
|
|
@@ -1673,6 +1684,105 @@ export const REVIEW_GATE_CONDITIONS = Object.freeze({
|
|
|
1673
1684
|
undetermined: "undetermined",
|
|
1674
1685
|
});
|
|
1675
1686
|
|
|
1687
|
+
/**
|
|
1688
|
+
* What the pull request's OWN review surface says, independent of any check.
|
|
1689
|
+
*
|
|
1690
|
+
* ## Why a second input exists at all (#3706)
|
|
1691
|
+
*
|
|
1692
|
+
* The gate classified an evidence-bearing check from its `state` and its
|
|
1693
|
+
* `description`, and **the description is not a function of whether a review
|
|
1694
|
+
* happened.** Measured on CodySwannGT/lisa#3762: `success` with the
|
|
1695
|
+
* description `Review rate limited`, and the vendor HAD reviewed, posting
|
|
1696
|
+
* `CHANGES_REQUESTED` with a comment that found a real defect. The identical
|
|
1697
|
+
* string appears on pull requests nothing read. One string, two opposite
|
|
1698
|
+
* facts — and the waiver collapsed them PERMISSIVELY, so a pull request a
|
|
1699
|
+
* reviewer objected to published `neutral` and merged.
|
|
1700
|
+
*
|
|
1701
|
+
* No rearrangement of the description vocabulary can fix that, because the
|
|
1702
|
+
* information is not in the description. It is in `reviewDecision` and
|
|
1703
|
+
* `reviewThreads`, which `gh pr checks` cannot see and this gate did not read.
|
|
1704
|
+
*
|
|
1705
|
+
* ## Why `reviewDecision` rather than the reviews' own states
|
|
1706
|
+
*
|
|
1707
|
+
* `reviewDecision` is GitHub's own computation of the latest state per
|
|
1708
|
+
* reviewer, so a DISMISSED objection and a superseded one are already excluded.
|
|
1709
|
+
* Recomputing it here from `reviews[].state` would resurrect a stale
|
|
1710
|
+
* `CHANGES_REQUESTED` and block work that was already addressed — the false-red
|
|
1711
|
+
* direction tracked on #3720, which this must not manufacture while closing the
|
|
1712
|
+
* false-green one.
|
|
1713
|
+
*
|
|
1714
|
+
* ## COULD-NOT-ASK IS A THIRD STATE, NOT AN ABSENCE VALUE
|
|
1715
|
+
*
|
|
1716
|
+
* "No review activity" is what GRANTS the waiver, so a failed read resolving to
|
|
1717
|
+
* it would waive on evidence nobody obtained — this file's own thesis, one
|
|
1718
|
+
* square over, and the shape `scripts/check-probe-absence-direction.mjs`
|
|
1719
|
+
* refuses outright (#3848): a probe must not hand a failure back as an answer.
|
|
1720
|
+
* The first draft here returned `undefined` on a failed read, which the caller
|
|
1721
|
+
* did distinguish — and which is still the same token a caller elsewhere reads
|
|
1722
|
+
* as a legitimate negative. So the third state is carried in the value:
|
|
1723
|
+
* `read: false` is a fact about the READ, and `present` / `objected` are facts
|
|
1724
|
+
* about the pull request that only mean anything when `read` is true.
|
|
1725
|
+
*
|
|
1726
|
+
* @param {unknown} payload - `pullRequest` as the GraphQL read returns it
|
|
1727
|
+
* @returns {{read: boolean, present: boolean, objected: boolean}} What the review surface showed, and whether it was read at all
|
|
1728
|
+
*/
|
|
1729
|
+
export function reviewActivityFrom(payload) {
|
|
1730
|
+
if (typeof payload !== "object" || payload === null) {
|
|
1731
|
+
return UNREAD_REVIEW_ACTIVITY;
|
|
1732
|
+
}
|
|
1733
|
+
const reviews = /** @type {{totalCount?: unknown}} */ (
|
|
1734
|
+
/** @type {Record<string, unknown>} */ (payload).reviews
|
|
1735
|
+
);
|
|
1736
|
+
const threads = /** @type {{totalCount?: unknown, nodes?: unknown}} */ (
|
|
1737
|
+
/** @type {Record<string, unknown>} */ (payload).reviewThreads
|
|
1738
|
+
);
|
|
1739
|
+
if (typeof reviews?.totalCount !== "number") return UNREAD_REVIEW_ACTIVITY;
|
|
1740
|
+
if (typeof threads?.totalCount !== "number") return UNREAD_REVIEW_ACTIVITY;
|
|
1741
|
+
const nodes = Array.isArray(threads.nodes) ? threads.nodes : [];
|
|
1742
|
+
const unresolved = nodes.some(node => node?.isResolved === false);
|
|
1743
|
+
const decision = String(
|
|
1744
|
+
/** @type {Record<string, unknown>} */ (payload).reviewDecision ?? ""
|
|
1745
|
+
).toUpperCase();
|
|
1746
|
+
const objected = decision === "CHANGES_REQUESTED" || unresolved;
|
|
1747
|
+
// The query asks for the first 50 threads, and the two verdicts do not need
|
|
1748
|
+
// the same evidence.
|
|
1749
|
+
//
|
|
1750
|
+
// "Somebody objected" is sound from a partial page: a thread we did not read
|
|
1751
|
+
// cannot un-object the one we did. "Nobody objected" is not — it is a claim
|
|
1752
|
+
// about every thread, and a page of 50 resolved ones says nothing about the
|
|
1753
|
+
// fifty-first. A pull request with more than 50 threads whose only unresolved
|
|
1754
|
+
// one sorts late would otherwise report `objected: false`, and a neutral
|
|
1755
|
+
// waiver would be allowed to cover a live objection.
|
|
1756
|
+
//
|
|
1757
|
+
// `totalCount` is already in the response, so the shortfall is detectable
|
|
1758
|
+
// without a second request. Reporting it as UNREAD is the honest answer and
|
|
1759
|
+
// the one this module is built around: a failed read is NOT a negative, and
|
|
1760
|
+
// the cost lands on the waiver's corroboration rather than on the merge.
|
|
1761
|
+
if (!objected && nodes.length < threads.totalCount) {
|
|
1762
|
+
return UNREAD_REVIEW_ACTIVITY;
|
|
1763
|
+
}
|
|
1764
|
+
return {
|
|
1765
|
+
read: true,
|
|
1766
|
+
present: reviews.totalCount > 0 || threads.totalCount > 0,
|
|
1767
|
+
objected,
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/**
|
|
1772
|
+
* The review surface as it looks when nobody managed to read it.
|
|
1773
|
+
*
|
|
1774
|
+
* A named value rather than `undefined` on purpose: `undefined` is what a
|
|
1775
|
+
* caller elsewhere in this file reads as a legitimate negative, and the whole
|
|
1776
|
+
* point is that a failed read is NOT a negative. `present` and `objected` are
|
|
1777
|
+
* `false` here only because the shape requires values; `read: false` is the
|
|
1778
|
+
* field every consumer must branch on first.
|
|
1779
|
+
*/
|
|
1780
|
+
export const UNREAD_REVIEW_ACTIVITY = Object.freeze({
|
|
1781
|
+
read: false,
|
|
1782
|
+
present: false,
|
|
1783
|
+
objected: false,
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1676
1786
|
/**
|
|
1677
1787
|
* Normalises a description for whole-string comparison.
|
|
1678
1788
|
*
|
|
@@ -1719,7 +1829,13 @@ function normalizeDescription(description) {
|
|
|
1719
1829
|
* long" are different things to go and look at; what changes is that neither is
|
|
1720
1830
|
* asserted as a fact about the reviewer when the gate simply stopped waiting.
|
|
1721
1831
|
*
|
|
1722
|
-
*
|
|
1832
|
+
* A WAIVER IS NOW CONDITIONAL ON THE ABSENCE OF REVIEW (#3706). `reading.
|
|
1833
|
+
* reviewActivity` is what the pull request's own review surface showed, and it
|
|
1834
|
+
* only ever reaches the waiver branch — the satisfy, absent, pending and
|
|
1835
|
+
* unrecognised paths are untouched by it, deliberately, because none of them
|
|
1836
|
+
* grants permission to merge on a claim about who reviewed.
|
|
1837
|
+
*
|
|
1838
|
+
* @param {{present: boolean, state?: string, description?: string, waitExpired?: boolean, reviewActivity?: {read: boolean, present: boolean, objected: boolean}}} reading - One check, whether the settle wait expired, and what the pull request's own review surface showed
|
|
1723
1839
|
* @param {{waive?: readonly string[], satisfy?: readonly string[]}} [vocabulary] - Per-check extensions
|
|
1724
1840
|
* @returns {{state: string, condition: string, why: string}} Severity, the condition observed, and a one-line reason
|
|
1725
1841
|
*/
|
|
@@ -1734,7 +1850,12 @@ export function reviewGateState(reading, vocabulary = {}) {
|
|
|
1734
1850
|
if (state !== "SUCCESS") {
|
|
1735
1851
|
return pendingReviewGateState(state, reading.waitExpired);
|
|
1736
1852
|
}
|
|
1737
|
-
return successfulReviewGateState(
|
|
1853
|
+
return successfulReviewGateState(
|
|
1854
|
+
text,
|
|
1855
|
+
reading.description,
|
|
1856
|
+
vocabulary,
|
|
1857
|
+
reading.reviewActivity
|
|
1858
|
+
);
|
|
1738
1859
|
}
|
|
1739
1860
|
|
|
1740
1861
|
/**
|
|
@@ -1803,15 +1924,70 @@ function pendingReviewGateState(state, waitExpired) {
|
|
|
1803
1924
|
};
|
|
1804
1925
|
}
|
|
1805
1926
|
|
|
1927
|
+
/**
|
|
1928
|
+
* Classifies a check whose description matched an entitlement waiver.
|
|
1929
|
+
*
|
|
1930
|
+
* ## The waiver keeps its severity and loses its false claim (#3706)
|
|
1931
|
+
*
|
|
1932
|
+
* The owner's ruling on #3221, restated on #3706, is untouched: a pull request
|
|
1933
|
+
* author cannot fix a vendor entitlement, so a throttled reviewer must not
|
|
1934
|
+
* redden the author's pull request. That ruling is about the pull request
|
|
1935
|
+
* NOBODY REVIEWED, which is the only pull request it was ever an argument
|
|
1936
|
+
* about — and the third branch below is where it lives, unchanged.
|
|
1937
|
+
*
|
|
1938
|
+
* What the ruling never covered is a pull request where a review DID happen and
|
|
1939
|
+
* OBJECTED. Measured on #3762: `success`, description `Review rate limited`,
|
|
1940
|
+
* and a `CHANGES_REQUESTED` finding a real defect. The waiver covered it
|
|
1941
|
+
* anyway, because the gate read only the string — so the one condition the file
|
|
1942
|
+
* calls "the case this gate exists to let through to a human" was the condition
|
|
1943
|
+
* it merged. That is the first branch, and it is the fix.
|
|
1944
|
+
*
|
|
1945
|
+
* The second branch is the honesty half. Review activity WITHOUT an objection
|
|
1946
|
+
* still waives — the entitlement argument still applies and the severity does
|
|
1947
|
+
* not move — but the sentence stops asserting "this pull request is
|
|
1948
|
+
* UNREVIEWED", which was measured false on 7 of 34 waived merges here.
|
|
1949
|
+
*
|
|
1950
|
+
* @param {string|undefined} description - Original description
|
|
1951
|
+
* @param {{read: boolean, present: boolean, objected: boolean}|undefined} activity - What the pull request's own review surface showed
|
|
1952
|
+
* @returns {{state: string, condition: string, why: string}} Review gate verdict
|
|
1953
|
+
*/
|
|
1954
|
+
function waivedReviewGateState(description, activity) {
|
|
1955
|
+
const said = `reported ${JSON.stringify(description ?? "")}`;
|
|
1956
|
+
const read = activity !== undefined && activity.read !== false;
|
|
1957
|
+
if (read && activity.objected === true) {
|
|
1958
|
+
return {
|
|
1959
|
+
state: REVIEW_GATE_STATES.unsatisfied,
|
|
1960
|
+
condition: REVIEW_GATE_CONDITIONS.objected,
|
|
1961
|
+
why: `${said} — but a review HAPPENED on this pull request and OBJECTED to it: \`reviewDecision\` is CHANGES_REQUESTED, or a review thread is unresolved. NO WAIVER COVERS AN OBJECTION. The description said the reviewer could not review and the review surface says it did; the review surface is the authority, because a vendor's throttle message is not a function of whether anybody read the diff (CodySwannGT/lisa#3762). READ THE OBJECTION.`,
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
if (read && activity.present === true) {
|
|
1965
|
+
return {
|
|
1966
|
+
state: REVIEW_GATE_STATES.waived,
|
|
1967
|
+
condition: REVIEW_GATE_CONDITIONS.waivedWithReview,
|
|
1968
|
+
why: `${said} — the check saying, in its own words, that it could not review. WAIVED: merging is a decision taken on that basis. But this pull request DOES carry review activity that raised no objection, so it is not the unreviewed pull request the waiver's rationale is about — do not record it as one.`,
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
const unverified = read
|
|
1972
|
+
? ""
|
|
1973
|
+
: " The pull request's own review surface could not be read on this run, so nothing corroborated the waiver: it is granted on the description alone, which is the input measured to be wrong in both directions.";
|
|
1974
|
+
return {
|
|
1975
|
+
state: REVIEW_GATE_STATES.waived,
|
|
1976
|
+
condition: REVIEW_GATE_CONDITIONS.waived,
|
|
1977
|
+
why: `${said} — the check saying, in its own words, that it could not review. WAIVED, not satisfied: this pull request is UNREVIEWED and merging it is a decision taken on that basis.${unverified} The waiver clears the moment the entitlement behind it is fixed, at which point this gate starts biting with no code change.`,
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1806
1981
|
/**
|
|
1807
1982
|
* Classifies a successful check from its exact description vocabulary.
|
|
1808
1983
|
*
|
|
1809
1984
|
* @param {string} text - Normalized description
|
|
1810
1985
|
* @param {string|undefined} description - Original description
|
|
1811
1986
|
* @param {{waive?: readonly string[], satisfy?: readonly string[]}} vocabulary - Per-check extensions
|
|
1987
|
+
* @param {{read: boolean, present: boolean, objected: boolean}|undefined} [activity] - What the pull request's own review surface showed
|
|
1812
1988
|
* @returns {{state: string, condition: string, why: string}} Review gate verdict
|
|
1813
1989
|
*/
|
|
1814
|
-
function successfulReviewGateState(text, description, vocabulary) {
|
|
1990
|
+
function successfulReviewGateState(text, description, vocabulary, activity) {
|
|
1815
1991
|
const satisfies = [...REVIEW_SATISFACTIONS, ...(vocabulary.satisfy ?? [])];
|
|
1816
1992
|
if (satisfies.some(phrase => text === normalizeDescription(phrase))) {
|
|
1817
1993
|
return {
|
|
@@ -1822,11 +1998,7 @@ function successfulReviewGateState(text, description, vocabulary) {
|
|
|
1822
1998
|
}
|
|
1823
1999
|
const waivers = [...ENTITLEMENT_WAIVERS, ...(vocabulary.waive ?? [])];
|
|
1824
2000
|
if (waivers.some(phrase => text === normalizeDescription(phrase))) {
|
|
1825
|
-
return
|
|
1826
|
-
state: REVIEW_GATE_STATES.waived,
|
|
1827
|
-
condition: REVIEW_GATE_CONDITIONS.waived,
|
|
1828
|
-
why: `reported ${JSON.stringify(description ?? "")} — the check saying, in its own words, that it could not review. WAIVED, not satisfied: this pull request is UNREVIEWED and merging it is a decision taken on that basis. The waiver clears the moment the entitlement behind it is fixed, at which point this gate starts biting with no code change.`,
|
|
1829
|
-
};
|
|
2001
|
+
return waivedReviewGateState(description, activity);
|
|
1830
2002
|
}
|
|
1831
2003
|
return {
|
|
1832
2004
|
state: REVIEW_GATE_STATES.unsatisfied,
|
|
@@ -1845,7 +2017,7 @@ function successfulReviewGateState(text, description, vocabulary) {
|
|
|
1845
2017
|
*
|
|
1846
2018
|
* @param {object} declaration - The per-repo declaration
|
|
1847
2019
|
* @param {ReadonlyArray<{name: string, state: string, description?: string}>} checks - The checks
|
|
1848
|
-
* @param {{headSha?: string, waitExpired?: boolean}} [options] - `headSha` is cited in every finding; `waitExpired` says the settle loop hit its deadline
|
|
2020
|
+
* @param {{headSha?: string, waitExpired?: boolean, reviewActivity?: {read: boolean, present: boolean, objected: boolean}}} [options] - `headSha` is cited in every finding; `waitExpired` says the settle loop hit its deadline; `reviewActivity` is what the pull request's own review surface showed, absent or `read: false` when it was never read
|
|
1849
2021
|
* @returns {{violations: object[], states: Record<string, string>, conditions: Record<string, string>, descriptions: Record<string, string>, checked: number}} Findings, per-check state and condition, the description each verdict was read from, and how many were examined
|
|
1850
2022
|
*/
|
|
1851
2023
|
export function evaluateReviewGate(declaration, checks, options = {}) {
|
|
@@ -1877,6 +2049,7 @@ export function evaluateReviewGate(declaration, checks, options = {}) {
|
|
|
1877
2049
|
state: found.state,
|
|
1878
2050
|
description: found.description,
|
|
1879
2051
|
waitExpired: options.waitExpired === true,
|
|
2052
|
+
reviewActivity: options.reviewActivity,
|
|
1880
2053
|
},
|
|
1881
2054
|
vocabulary
|
|
1882
2055
|
);
|
|
@@ -2122,6 +2295,28 @@ function carriedSuffix(carried) {
|
|
|
2122
2295
|
* pull requests this one CARRIES
|
|
2123
2296
|
* @returns {{verdict: string, conclusion: string, title: string}} What to publish
|
|
2124
2297
|
*/
|
|
2298
|
+
/**
|
|
2299
|
+
* The lead sentence for an unsatisfied verdict, keyed on WHAT was observed.
|
|
2300
|
+
*
|
|
2301
|
+
* Three leads because the three conditions hand an operator three different
|
|
2302
|
+
* facts, and the title is the one surface a merge decision reads. Split out of
|
|
2303
|
+
* {@link reviewGateVerdict} so each lead is one expression rather than a nested
|
|
2304
|
+
* ternary nobody can read.
|
|
2305
|
+
*
|
|
2306
|
+
* @param {{allUndetermined: boolean, allObjected: boolean, unsatisfied: readonly string[], quote: (name: string) => string}} reading - The classification and how to name each check
|
|
2307
|
+
* @returns {string} The lead sentence, without the carried/waive-rate suffix
|
|
2308
|
+
*/
|
|
2309
|
+
function unsatisfiedTitle(reading) {
|
|
2310
|
+
const named = reading.unsatisfied.map(reading.quote).join("; ");
|
|
2311
|
+
if (reading.allUndetermined) {
|
|
2312
|
+
return `UNDETERMINED — the gate stopped waiting before review evidence settled, and is NOT reporting that nobody reviewed: ${named}. RE-RUN this job; do not investigate the change on the strength of this`;
|
|
2313
|
+
}
|
|
2314
|
+
if (reading.allObjected) {
|
|
2315
|
+
return `OBJECTED — a review HAPPENED on this pull request and objected to it; this is NOT an unreviewed pull request and no waiver covers it: ${named}. READ THE OBJECTION`;
|
|
2316
|
+
}
|
|
2317
|
+
return `UNREVIEWED — review evidence unsatisfied: ${named}`;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2125
2320
|
export function reviewGateVerdict(reading = {}) {
|
|
2126
2321
|
const states = reading.states ?? {};
|
|
2127
2322
|
const conditions = reading.conditions ?? {};
|
|
@@ -2179,13 +2374,23 @@ export function reviewGateVerdict(reading = {}) {
|
|
|
2179
2374
|
const allUndetermined = unsatisfied.every(
|
|
2180
2375
|
name => conditions[name] === REVIEW_GATE_CONDITIONS.undetermined
|
|
2181
2376
|
);
|
|
2377
|
+
// AND THE SAME MISTAKE ONE ROW DOWN (#3706). `objected` is the one
|
|
2378
|
+
// unsatisfying condition where a review DID happen, and this title called
|
|
2379
|
+
// it UNREVIEWED — the word an operator quotes into a merge disclosure. It
|
|
2380
|
+
// is the more expensive direction of the two: `undetermined` sends someone
|
|
2381
|
+
// to audit a change nobody had a finding about, while UNREVIEWED-on-an-
|
|
2382
|
+
// objection tells them no finding exists while one sits on the pull
|
|
2383
|
+
// request. Only when EVERY unsatisfied check objected, so a real silence
|
|
2384
|
+
// alongside an objection still leads with UNREVIEWED.
|
|
2385
|
+
const allObjected = unsatisfied.every(
|
|
2386
|
+
name => conditions[name] === REVIEW_GATE_CONDITIONS.objected
|
|
2387
|
+
);
|
|
2182
2388
|
return {
|
|
2183
2389
|
verdict: REVIEW_GATE_STATES.unsatisfied,
|
|
2184
2390
|
conclusion: REVIEW_VERDICT_CONCLUSIONS.unsatisfied,
|
|
2185
2391
|
title: fitTitle(
|
|
2186
|
-
allUndetermined
|
|
2187
|
-
|
|
2188
|
-
: `UNREVIEWED — review evidence unsatisfied: ${unsatisfied.map(quote).join("; ")}${suffix}`
|
|
2392
|
+
unsatisfiedTitle({ allUndetermined, allObjected, unsatisfied, quote }) +
|
|
2393
|
+
suffix
|
|
2189
2394
|
),
|
|
2190
2395
|
};
|
|
2191
2396
|
}
|
|
@@ -2194,11 +2399,24 @@ export function reviewGateVerdict(reading = {}) {
|
|
|
2194
2399
|
name => states[name] === REVIEW_GATE_STATES.waived
|
|
2195
2400
|
);
|
|
2196
2401
|
if (waived.length > 0) {
|
|
2402
|
+
// AND THE TITLE IS WHERE THE CLAIM IS READ (#3706). "This pull request is
|
|
2403
|
+
// UNREVIEWED" is the sentence an operator quotes into a merge disclosure,
|
|
2404
|
+
// and it was printed unconditionally — measured false on 7 of the last 34
|
|
2405
|
+
// waived merges here, every one of which carried review activity at the
|
|
2406
|
+
// head. Severity does not move: both leads publish `neutral`, and the
|
|
2407
|
+
// waiver's rationale is untouched. Only the claim changes, and only when
|
|
2408
|
+
// EVERY waived check saw review activity, so one genuinely silent check
|
|
2409
|
+
// alongside a reviewed one still leads with UNREVIEWED.
|
|
2410
|
+
const allWithReview = waived.every(
|
|
2411
|
+
name => conditions[name] === REVIEW_GATE_CONDITIONS.waivedWithReview
|
|
2412
|
+
);
|
|
2197
2413
|
return {
|
|
2198
2414
|
verdict: REVIEW_GATE_STATES.waived,
|
|
2199
2415
|
conclusion: REVIEW_VERDICT_CONCLUSIONS.waived,
|
|
2200
2416
|
title: fitTitle(
|
|
2201
|
-
|
|
2417
|
+
allWithReview
|
|
2418
|
+
? `WAIVED — the review check could not review, and merging is a decision taken on that basis; but this pull request DOES carry review activity that raised no objection, so do NOT record it as unreviewed: ${waived.map(quote).join("; ")}${suffix}`
|
|
2419
|
+
: `WAIVED — this pull request is UNREVIEWED and merging it is a decision taken on that basis: ${waived.map(quote).join("; ")}${suffix}`
|
|
2202
2420
|
),
|
|
2203
2421
|
};
|
|
2204
2422
|
}
|
|
@@ -2232,6 +2450,81 @@ export function reviewGateVerdict(reading = {}) {
|
|
|
2232
2450
|
};
|
|
2233
2451
|
}
|
|
2234
2452
|
|
|
2453
|
+
/**
|
|
2454
|
+
* The one call that answers "did a review actually happen on this pull request?"
|
|
2455
|
+
*
|
|
2456
|
+
* Everything else this file reads is a CHECK — a conclusion and a description
|
|
2457
|
+
* posted by a vendor about itself. This reads the pull request's own review
|
|
2458
|
+
* surface, which is the only place the answer exists and the one place
|
|
2459
|
+
* `gh pr checks` cannot reach: measured on a pull request in this repository,
|
|
2460
|
+
* `gh pr checks` printed 42 pass / 1 fail / 5 skipping and said nothing about a
|
|
2461
|
+
* blocking `CHANGES_REQUESTED` or an unresolved thread, both of which block a
|
|
2462
|
+
* merge here.
|
|
2463
|
+
*
|
|
2464
|
+
* `first: 50` on the threads because only `isResolved` is read and only whether
|
|
2465
|
+
* ANY is false matters; a pull request with more than fifty threads is already
|
|
2466
|
+
* blocked by the first unresolved one in the page.
|
|
2467
|
+
*/
|
|
2468
|
+
const REVIEW_ACTIVITY_QUERY = [
|
|
2469
|
+
"query($owner: String!, $name: String!, $number: Int!) {",
|
|
2470
|
+
" repository(owner: $owner, name: $name) {",
|
|
2471
|
+
" pullRequest(number: $number) {",
|
|
2472
|
+
" reviewDecision",
|
|
2473
|
+
" reviews(first: 1) { totalCount }",
|
|
2474
|
+
" reviewThreads(first: 50) { totalCount nodes { isResolved } }",
|
|
2475
|
+
" }",
|
|
2476
|
+
" }",
|
|
2477
|
+
"}",
|
|
2478
|
+
].join("\n");
|
|
2479
|
+
|
|
2480
|
+
/**
|
|
2481
|
+
* Reads what the pull request's own review surface shows, or nothing.
|
|
2482
|
+
*
|
|
2483
|
+
* BEST-EFFORT AND NEVER FATAL, in the direction that costs a waiver its
|
|
2484
|
+
* corroboration rather than a pull request its merge: a failed read returns
|
|
2485
|
+
* {@link UNREAD_REVIEW_ACTIVITY}, which {@link waivedReviewGateState} renders
|
|
2486
|
+
* as a waiver that says it was not corroborated. Throwing here would convert a
|
|
2487
|
+
* transient GitHub error into a red review gate on a pull request nobody has
|
|
2488
|
+
* any finding about, on 85% of pull requests here — the "gate that reddens
|
|
2489
|
+
* every pull request and then gets deleted" failure this file names twice.
|
|
2490
|
+
*
|
|
2491
|
+
* probe-direction: fail-closed — a failed read cannot produce
|
|
2492
|
+
* `{read: true, present: false}`, which is the only value that grants an
|
|
2493
|
+
* uncorroborated waiver its silence. It produces `read: false` instead, and the
|
|
2494
|
+
* verdict published then says out loud that nothing corroborated the waiver. No
|
|
2495
|
+
* outcome is quieter for the failure than it would have been for a real read.
|
|
2496
|
+
*
|
|
2497
|
+
* @param {string|number} pr - Pull request number
|
|
2498
|
+
* @param {string} [repo] - `OWNER/NAME`; defaults to the current repository
|
|
2499
|
+
* @returns {{read: boolean, present: boolean, objected: boolean}} What the review surface showed, and whether it was read at all
|
|
2500
|
+
*/
|
|
2501
|
+
export function fetchReviewActivity(pr, repo) {
|
|
2502
|
+
const slug = resolveRepoSlug(repo);
|
|
2503
|
+
if (slug === undefined) return UNREAD_REVIEW_ACTIVITY;
|
|
2504
|
+
const [owner, name] = slug.split("/");
|
|
2505
|
+
try {
|
|
2506
|
+
const raw = boundedExecFileSync(
|
|
2507
|
+
"gh",
|
|
2508
|
+
[
|
|
2509
|
+
"api",
|
|
2510
|
+
"graphql",
|
|
2511
|
+
"-f",
|
|
2512
|
+
`query=${REVIEW_ACTIVITY_QUERY}`,
|
|
2513
|
+
"-F",
|
|
2514
|
+
`owner=${owner}`,
|
|
2515
|
+
"-F",
|
|
2516
|
+
`name=${name}`,
|
|
2517
|
+
"-F",
|
|
2518
|
+
`number=${pr}`,
|
|
2519
|
+
],
|
|
2520
|
+
{ encoding: "utf8" }
|
|
2521
|
+
);
|
|
2522
|
+
return reviewActivityFrom(JSON.parse(raw)?.data?.repository?.pullRequest);
|
|
2523
|
+
} catch {
|
|
2524
|
+
return UNREAD_REVIEW_ACTIVITY;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2235
2528
|
/**
|
|
2236
2529
|
* Counts how a sample of pull requests ended up, worst-wins per pull request.
|
|
2237
2530
|
*
|
|
@@ -3205,7 +3498,7 @@ function readSecondsFlag(argv, name, fallback) {
|
|
|
3205
3498
|
*
|
|
3206
3499
|
* @param {ReadonlyArray<string>} argv - CLI arguments
|
|
3207
3500
|
* @param {object} declaration - The per-repo declaration
|
|
3208
|
-
* @param {{trustRequiredContexts?: boolean, env?: NodeJS.ProcessEnv, fetch?: Function, probeBranch?: Function, now?: Function, sleep?: Function, headSha?: Function}} [options] -
|
|
3501
|
+
* @param {{trustRequiredContexts?: boolean, env?: NodeJS.ProcessEnv, fetch?: Function, probeBranch?: Function, now?: Function, sleep?: Function, headSha?: Function, reviewActivity?: Function}} [options] -
|
|
3209
3502
|
* Injection seams for the suite
|
|
3210
3503
|
* @returns {{pr: string|undefined, prSource: string|null, headSha: string|undefined, checked: number, violations: object[], settled: boolean, refusal: {kind: string, reason: string}|null}|undefined} The inspection
|
|
3211
3504
|
*/
|
|
@@ -3288,14 +3581,31 @@ export function inspectVacuity(argv, declaration, options = {}) {
|
|
|
3288
3581
|
// block. Its findings are what make a waiver visible, and a waiver that is
|
|
3289
3582
|
// only computed when somebody opted in would be invisible on exactly the
|
|
3290
3583
|
// repositories that have not opted in yet.
|
|
3291
|
-
const
|
|
3584
|
+
const gateOptions = {
|
|
3292
3585
|
headSha: read.headSha,
|
|
3293
3586
|
// The settle loop exits on one of two things: everything declared reached a
|
|
3294
3587
|
// terminal read, or the deadline passed. So `settled === false` IS "the
|
|
3295
3588
|
// wait expired", and it is the only place that fact exists — every reader
|
|
3296
3589
|
// downstream sees a check row that looks identical either way (#3716).
|
|
3297
3590
|
waitExpired: read.settled === false,
|
|
3298
|
-
}
|
|
3591
|
+
};
|
|
3592
|
+
// TWO PASSES, AND THE SECOND IS PAID FOR ONLY BY A WAIVER (#3706). The review
|
|
3593
|
+
// surface is read exactly when a waiver is on the table, which is the only
|
|
3594
|
+
// condition whose verdict it can change: `satisfied`, `absent`, `pending`,
|
|
3595
|
+
// `unrecognised` and `undetermined` all reach the same answer without it, and
|
|
3596
|
+
// making them pay a network call would put a new failure mode on paths that
|
|
3597
|
+
// work. Same shape, and the same reason, as the waive-rate sample below.
|
|
3598
|
+
const gate = Object.values(
|
|
3599
|
+
evaluateReviewGate(declaration, read.checks, gateOptions).states
|
|
3600
|
+
).includes(REVIEW_GATE_STATES.waived)
|
|
3601
|
+
? evaluateReviewGate(declaration, read.checks, {
|
|
3602
|
+
...gateOptions,
|
|
3603
|
+
reviewActivity: (options.reviewActivity ?? fetchReviewActivity)(
|
|
3604
|
+
pr,
|
|
3605
|
+
repo
|
|
3606
|
+
),
|
|
3607
|
+
})
|
|
3608
|
+
: evaluateReviewGate(declaration, read.checks, gateOptions);
|
|
3299
3609
|
// #3658. Everything above judged ONE commit — this pull request's head — and
|
|
3300
3610
|
// that is the one commit a batch integration pull request does not speak for.
|
|
3301
3611
|
// Armed by the same flag as the gate itself, because this IS the gate,
|
|
@@ -43,6 +43,35 @@
|
|
|
43
43
|
# a gate that reddens every pull request on a vendor's billing state gets
|
|
44
44
|
# deleted, and then nothing is watching at all.
|
|
45
45
|
#
|
|
46
|
+
# AND A WAIVER MAY NOT COVER A REVIEW THAT HAPPENED (#3706). Everything above
|
|
47
|
+
# classifies the review check from its `state` and its `description`, and the
|
|
48
|
+
# description is NOT a function of whether a review happened. Measured on
|
|
49
|
+
# CodySwannGT/lisa#3762: `success`, description `Review rate limited`, and the
|
|
50
|
+
# vendor HAD reviewed — it posted `CHANGES_REQUESTED` and found a real defect.
|
|
51
|
+
# The identical string appears on pull requests nothing read, so one string
|
|
52
|
+
# carried two opposite facts and the waiver collapsed them PERMISSIVELY: the one
|
|
53
|
+
# condition this gate exists to let through to a human was the condition it
|
|
54
|
+
# merged. Re-measured on the repository that ships this, 2026-09-06, over the
|
|
55
|
+
# last 40 merged pull requests: 34 reported `Review rate limited`, and 7 of those
|
|
56
|
+
# 34 carried review activity at the head while the published verdict said "this
|
|
57
|
+
# pull request is UNREVIEWED".
|
|
58
|
+
#
|
|
59
|
+
# So the waiver is now CONDITIONAL ON THE ABSENCE OF REVIEW. When a waiver is on
|
|
60
|
+
# the table — and only then, so no other path pays a network call — the guard
|
|
61
|
+
# reads the pull request's own review surface, `reviewDecision` and
|
|
62
|
+
# `reviewThreads`, which is the only surface that answers the question and the
|
|
63
|
+
# one `gh pr checks` cannot see at all. An objection there is `unsatisfied`,
|
|
64
|
+
# never waived. Review activity without an objection stays WAIVED at unchanged
|
|
65
|
+
# severity and merely stops asserting that nobody reviewed. A surface that
|
|
66
|
+
# cannot be read stays WAIVED too, and says the waiver is uncorroborated —
|
|
67
|
+
# refusing there would convert a transient API error into a red gate on a pull
|
|
68
|
+
# request nobody has a finding about.
|
|
69
|
+
#
|
|
70
|
+
# `reviewDecision` rather than the reviews' own states, deliberately: GitHub
|
|
71
|
+
# already computes the latest state per reviewer there, so a DISMISSED objection
|
|
72
|
+
# is not resurrected. Recomputing it here would manufacture the false red
|
|
73
|
+
# tracked on #3720 while closing the false green tracked here.
|
|
74
|
+
#
|
|
46
75
|
# AND A PULL REQUEST SPEAKS FOR ONE COMMIT ONLY (#3658). Everything above judges
|
|
47
76
|
# this pull request's HEAD, which is the one commit a batch integration pull
|
|
48
77
|
# request does not speak for: its head carries the MERGES of every constituent
|