@patronage/software-factory 1.0.0-alpha.31 → 1.0.0-alpha.32
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/README.md +2 -2
- package/dist/index.d.ts +253 -49
- package/dist/index.js +2430 -945
- package/dist/schemas.d.ts +109 -14
- package/dist/schemas.js +263 -295
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,7 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
7
|
-
import
|
|
7
|
+
import * as nodeFs from "node:fs";
|
|
8
|
+
import { appendFileSync, closeSync, constants, cpSync, existsSync, fsyncSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
9
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
9
10
|
import picomatch from "picomatch";
|
|
10
11
|
import { link, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "node:fs/promises";
|
|
@@ -19,7 +20,7 @@ import http from "node:http";
|
|
|
19
20
|
import { pathToFileURL } from "node:url";
|
|
20
21
|
//#region package.json
|
|
21
22
|
var name = "@patronage/software-factory";
|
|
22
|
-
var version = "1.0.0-alpha.
|
|
23
|
+
var version = "1.0.0-alpha.32";
|
|
23
24
|
//#endregion
|
|
24
25
|
//#region src/cli-entry.ts
|
|
25
26
|
/**
|
|
@@ -1648,20 +1649,854 @@ const authorizeDemandWaiver = ({ authenticatedLogin, session }) => {
|
|
|
1648
1649
|
} };
|
|
1649
1650
|
};
|
|
1650
1651
|
//#endregion
|
|
1652
|
+
//#region src/sha256-hex.ts
|
|
1653
|
+
const ROUND_CONSTANTS = new Uint32Array([
|
|
1654
|
+
1116352408,
|
|
1655
|
+
1899447441,
|
|
1656
|
+
3049323471,
|
|
1657
|
+
3921009573,
|
|
1658
|
+
961987163,
|
|
1659
|
+
1508970993,
|
|
1660
|
+
2453635748,
|
|
1661
|
+
2870763221,
|
|
1662
|
+
3624381080,
|
|
1663
|
+
310598401,
|
|
1664
|
+
607225278,
|
|
1665
|
+
1426881987,
|
|
1666
|
+
1925078388,
|
|
1667
|
+
2162078206,
|
|
1668
|
+
2614888103,
|
|
1669
|
+
3248222580,
|
|
1670
|
+
3835390401,
|
|
1671
|
+
4022224774,
|
|
1672
|
+
264347078,
|
|
1673
|
+
604807628,
|
|
1674
|
+
770255983,
|
|
1675
|
+
1249150122,
|
|
1676
|
+
1555081692,
|
|
1677
|
+
1996064986,
|
|
1678
|
+
2554220882,
|
|
1679
|
+
2821834349,
|
|
1680
|
+
2952996808,
|
|
1681
|
+
3210313671,
|
|
1682
|
+
3336571891,
|
|
1683
|
+
3584528711,
|
|
1684
|
+
113926993,
|
|
1685
|
+
338241895,
|
|
1686
|
+
666307205,
|
|
1687
|
+
773529912,
|
|
1688
|
+
1294757372,
|
|
1689
|
+
1396182291,
|
|
1690
|
+
1695183700,
|
|
1691
|
+
1986661051,
|
|
1692
|
+
2177026350,
|
|
1693
|
+
2456956037,
|
|
1694
|
+
2730485921,
|
|
1695
|
+
2820302411,
|
|
1696
|
+
3259730800,
|
|
1697
|
+
3345764771,
|
|
1698
|
+
3516065817,
|
|
1699
|
+
3600352804,
|
|
1700
|
+
4094571909,
|
|
1701
|
+
275423344,
|
|
1702
|
+
430227734,
|
|
1703
|
+
506948616,
|
|
1704
|
+
659060556,
|
|
1705
|
+
883997877,
|
|
1706
|
+
958139571,
|
|
1707
|
+
1322822218,
|
|
1708
|
+
1537002063,
|
|
1709
|
+
1747873779,
|
|
1710
|
+
1955562222,
|
|
1711
|
+
2024104815,
|
|
1712
|
+
2227730452,
|
|
1713
|
+
2361852424,
|
|
1714
|
+
2428436474,
|
|
1715
|
+
2756734187,
|
|
1716
|
+
3204031479,
|
|
1717
|
+
3329325298
|
|
1718
|
+
]);
|
|
1719
|
+
const INITIAL_STATE = new Uint32Array([
|
|
1720
|
+
1779033703,
|
|
1721
|
+
3144134277,
|
|
1722
|
+
1013904242,
|
|
1723
|
+
2773480762,
|
|
1724
|
+
1359893119,
|
|
1725
|
+
2600822924,
|
|
1726
|
+
528734635,
|
|
1727
|
+
1541459225
|
|
1728
|
+
]);
|
|
1729
|
+
const BLOCK_BYTES = 64;
|
|
1730
|
+
const LENGTH_FIELD_BYTES = 8;
|
|
1731
|
+
const WORD_BITS = 32;
|
|
1732
|
+
const rotateRight = (value, bits) => (value >>> bits | value << WORD_BITS - bits) >>> 0;
|
|
1733
|
+
/** Message bytes plus the FIPS 180-4 padding and 64-bit length suffix. */
|
|
1734
|
+
const padMessage = (message) => {
|
|
1735
|
+
const paddedLength = Math.ceil((message.length + 1 + LENGTH_FIELD_BYTES) / BLOCK_BYTES) * BLOCK_BYTES;
|
|
1736
|
+
const padded = new Uint8Array(paddedLength);
|
|
1737
|
+
padded.set(message);
|
|
1738
|
+
padded[message.length] = 128;
|
|
1739
|
+
const bitLength = BigInt(message.length) * 8n;
|
|
1740
|
+
new DataView(padded.buffer).setBigUint64(paddedLength - LENGTH_FIELD_BYTES, bitLength);
|
|
1741
|
+
return padded;
|
|
1742
|
+
};
|
|
1743
|
+
const ROUNDS = 64;
|
|
1744
|
+
const SCHEDULE_SEED_WORDS = 16;
|
|
1745
|
+
const STATE_WORDS = 8;
|
|
1746
|
+
const wordAt = (words, index) => words[index];
|
|
1747
|
+
/** One 64-byte block folded into the running state, in place. */
|
|
1748
|
+
const compress = (state, view, offset) => {
|
|
1749
|
+
const schedule = new Uint32Array(ROUNDS);
|
|
1750
|
+
for (let index = 0; index < SCHEDULE_SEED_WORDS; index += 1) schedule[index] = view.getUint32(offset + index * 4);
|
|
1751
|
+
for (let index = SCHEDULE_SEED_WORDS; index < ROUNDS; index += 1) {
|
|
1752
|
+
const previous = wordAt(schedule, index - 15);
|
|
1753
|
+
const recent = wordAt(schedule, index - 2);
|
|
1754
|
+
const sigma0 = rotateRight(previous, 7) ^ rotateRight(previous, 18) ^ previous >>> 3;
|
|
1755
|
+
const sigma1 = rotateRight(recent, 17) ^ rotateRight(recent, 19) ^ recent >>> 10;
|
|
1756
|
+
schedule[index] = wordAt(schedule, index - 16) + sigma0 + wordAt(schedule, index - 7) + sigma1 >>> 0;
|
|
1757
|
+
}
|
|
1758
|
+
const working = new Uint32Array(state);
|
|
1759
|
+
for (let index = 0; index < ROUNDS; index += 1) {
|
|
1760
|
+
const a = wordAt(working, 0);
|
|
1761
|
+
const b = wordAt(working, 1);
|
|
1762
|
+
const c = wordAt(working, 2);
|
|
1763
|
+
const d = wordAt(working, 3);
|
|
1764
|
+
const e = wordAt(working, 4);
|
|
1765
|
+
const f = wordAt(working, 5);
|
|
1766
|
+
const g = wordAt(working, 6);
|
|
1767
|
+
const h = wordAt(working, 7);
|
|
1768
|
+
const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
1769
|
+
const choose = e & f ^ ~e & g;
|
|
1770
|
+
const temp1 = h + sum1 + choose + wordAt(ROUND_CONSTANTS, index) + wordAt(schedule, index) >>> 0;
|
|
1771
|
+
const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
|
1772
|
+
const majority = a & b ^ a & c ^ b & c;
|
|
1773
|
+
working[7] = g;
|
|
1774
|
+
working[6] = f;
|
|
1775
|
+
working[5] = e;
|
|
1776
|
+
working[4] = d + temp1 >>> 0;
|
|
1777
|
+
working[3] = c;
|
|
1778
|
+
working[2] = b;
|
|
1779
|
+
working[1] = a;
|
|
1780
|
+
working[0] = temp1 + (sum0 + majority >>> 0) >>> 0;
|
|
1781
|
+
}
|
|
1782
|
+
for (let index = 0; index < STATE_WORDS; index += 1) state[index] = wordAt(state, index) + wordAt(working, index) >>> 0;
|
|
1783
|
+
};
|
|
1784
|
+
const toHex = (state) => {
|
|
1785
|
+
let hex = "";
|
|
1786
|
+
for (const word of state) hex += word.toString(16).padStart(8, "0");
|
|
1787
|
+
return hex;
|
|
1788
|
+
};
|
|
1789
|
+
/** The lowercase hex SHA-256 digest of a UTF-8 string. */
|
|
1790
|
+
const sha256Hex = (input) => {
|
|
1791
|
+
const padded = padMessage(new TextEncoder().encode(input));
|
|
1792
|
+
const view = new DataView(padded.buffer);
|
|
1793
|
+
const state = new Uint32Array(INITIAL_STATE);
|
|
1794
|
+
for (let offset = 0; offset < padded.length; offset += BLOCK_BYTES) compress(state, view, offset);
|
|
1795
|
+
return toHex(state);
|
|
1796
|
+
};
|
|
1797
|
+
//#endregion
|
|
1798
|
+
//#region src/review-finding-identity.ts
|
|
1799
|
+
const normalizeCitedSpan = (text) => text.split(/\r?\n/u).map((line) => line.trim().replaceAll(/\s+/gu, " ")).filter((line) => line.length > 0).join("\n");
|
|
1800
|
+
const PRESCRIBED_ACTION_STOPWORDS = new Set([
|
|
1801
|
+
"a",
|
|
1802
|
+
"an",
|
|
1803
|
+
"the",
|
|
1804
|
+
"to",
|
|
1805
|
+
"of",
|
|
1806
|
+
"in",
|
|
1807
|
+
"on",
|
|
1808
|
+
"and",
|
|
1809
|
+
"or",
|
|
1810
|
+
"is",
|
|
1811
|
+
"are",
|
|
1812
|
+
"be",
|
|
1813
|
+
"this",
|
|
1814
|
+
"that",
|
|
1815
|
+
"it",
|
|
1816
|
+
"for",
|
|
1817
|
+
"with",
|
|
1818
|
+
"as",
|
|
1819
|
+
"by",
|
|
1820
|
+
"from",
|
|
1821
|
+
"at",
|
|
1822
|
+
"into",
|
|
1823
|
+
"should",
|
|
1824
|
+
"must",
|
|
1825
|
+
"will",
|
|
1826
|
+
"would",
|
|
1827
|
+
"can",
|
|
1828
|
+
"could",
|
|
1829
|
+
"not",
|
|
1830
|
+
"but",
|
|
1831
|
+
"so",
|
|
1832
|
+
"if",
|
|
1833
|
+
"then",
|
|
1834
|
+
"than",
|
|
1835
|
+
"when",
|
|
1836
|
+
"which",
|
|
1837
|
+
"who",
|
|
1838
|
+
"whom",
|
|
1839
|
+
"its",
|
|
1840
|
+
"their",
|
|
1841
|
+
"there",
|
|
1842
|
+
"here",
|
|
1843
|
+
"also",
|
|
1844
|
+
"use",
|
|
1845
|
+
"using",
|
|
1846
|
+
"instead",
|
|
1847
|
+
"rather",
|
|
1848
|
+
"over",
|
|
1849
|
+
"up",
|
|
1850
|
+
"out",
|
|
1851
|
+
"have",
|
|
1852
|
+
"has",
|
|
1853
|
+
"had",
|
|
1854
|
+
"been"
|
|
1855
|
+
]);
|
|
1856
|
+
const tokenizePrescribedAction = (text) => {
|
|
1857
|
+
const words = text.toLowerCase().replaceAll(/[^a-z0-9\s]/gu, " ").split(/\s+/u).filter((word) => word.length > 0 && !PRESCRIBED_ACTION_STOPWORDS.has(word));
|
|
1858
|
+
return [...new Set(words)].toSorted();
|
|
1859
|
+
};
|
|
1860
|
+
const normalizeTitle = (title) => title.trim().toLowerCase().replaceAll(/\s+/gu, " ");
|
|
1861
|
+
const normalizeFile = (file) => file.trim();
|
|
1862
|
+
const findingKey = (finding) => {
|
|
1863
|
+
const path = finding.file ?? "";
|
|
1864
|
+
const normalizedSpan = normalizeCitedSpan(finding.citedSpan ?? "");
|
|
1865
|
+
const actionTokens = tokenizePrescribedAction(finding.prescribedAction ?? "");
|
|
1866
|
+
const legacyDiscriminator = normalizedSpan.length > 0 || actionTokens.length > 0 ? "" : normalizeTitle(finding.title ?? "");
|
|
1867
|
+
return sha256Hex(`${path}\0${sha256Hex(normalizedSpan)}\0${actionTokens.join(" ")}\0${legacyDiscriminator}`);
|
|
1868
|
+
};
|
|
1869
|
+
const declarationMatchesEntry = (entry, target) => normalizeTitle(entry.title) === normalizeTitle(target.title) && (target.file === void 0 || entry.file !== void 0 && normalizeFile(entry.file) === normalizeFile(target.file));
|
|
1870
|
+
//#endregion
|
|
1871
|
+
//#region src/review-ladder-policy.ts
|
|
1872
|
+
const REVIEW_SEVERITIES = [
|
|
1873
|
+
"critical",
|
|
1874
|
+
"high",
|
|
1875
|
+
"medium",
|
|
1876
|
+
"low",
|
|
1877
|
+
"unknown"
|
|
1878
|
+
];
|
|
1879
|
+
const REVIEW_CATEGORIES = [
|
|
1880
|
+
"correctness",
|
|
1881
|
+
"safety",
|
|
1882
|
+
"coordination",
|
|
1883
|
+
"maintainability",
|
|
1884
|
+
"unknown"
|
|
1885
|
+
];
|
|
1886
|
+
const resolveReviewFindingSeverity = (priority) => {
|
|
1887
|
+
const normalized = priority?.trim().toLowerCase();
|
|
1888
|
+
if (!normalized) return "unknown";
|
|
1889
|
+
if ([
|
|
1890
|
+
"p0",
|
|
1891
|
+
"critical",
|
|
1892
|
+
"crit",
|
|
1893
|
+
"blocker",
|
|
1894
|
+
"blocking"
|
|
1895
|
+
].includes(normalized)) return "critical";
|
|
1896
|
+
if ([
|
|
1897
|
+
"p1",
|
|
1898
|
+
"high",
|
|
1899
|
+
"severe",
|
|
1900
|
+
"major"
|
|
1901
|
+
].includes(normalized)) return "high";
|
|
1902
|
+
if ([
|
|
1903
|
+
"p2",
|
|
1904
|
+
"medium",
|
|
1905
|
+
"moderate",
|
|
1906
|
+
"normal",
|
|
1907
|
+
"refinement"
|
|
1908
|
+
].includes(normalized)) return "medium";
|
|
1909
|
+
if ([
|
|
1910
|
+
"p3",
|
|
1911
|
+
"p4",
|
|
1912
|
+
"low",
|
|
1913
|
+
"minor",
|
|
1914
|
+
"info",
|
|
1915
|
+
"informational"
|
|
1916
|
+
].includes(normalized)) return "low";
|
|
1917
|
+
return "unknown";
|
|
1918
|
+
};
|
|
1919
|
+
const countFindingsBySeverity = (findings) => {
|
|
1920
|
+
const counts = {
|
|
1921
|
+
critical: 0,
|
|
1922
|
+
high: 0,
|
|
1923
|
+
low: 0,
|
|
1924
|
+
medium: 0,
|
|
1925
|
+
unknown: 0
|
|
1926
|
+
};
|
|
1927
|
+
for (const finding of findings) counts[resolveReviewFindingSeverity(finding.priority)] += 1;
|
|
1928
|
+
return counts;
|
|
1929
|
+
};
|
|
1930
|
+
const resolveReviewFindingCategory = (category) => REVIEW_CATEGORIES.find((value) => value === category?.trim().toLowerCase()) ?? "unknown";
|
|
1931
|
+
const resolveFindingBlocking = (finding, disposition = "open") => {
|
|
1932
|
+
if (disposition !== "open") return false;
|
|
1933
|
+
if (resolveReviewFindingCategory(finding.category) !== "maintainability") return true;
|
|
1934
|
+
if (finding.blockingAfterCap !== void 0) return finding.blockingAfterCap;
|
|
1935
|
+
const severity = resolveReviewFindingSeverity(finding.priority);
|
|
1936
|
+
return severity === "critical" || severity === "high" || severity === "unknown";
|
|
1937
|
+
};
|
|
1938
|
+
const FACTORY_REVIEW_CYCLE_CAP = 5;
|
|
1939
|
+
const resolveReviewLadderPolicy = () => ({ gate: { cap: FACTORY_REVIEW_CYCLE_CAP } });
|
|
1940
|
+
//#endregion
|
|
1941
|
+
//#region src/review-ladder-ledger.ts
|
|
1942
|
+
const OCCURRENCE_ADDRESS_PATTERN = /^c[1-9][0-9]*f[1-9][0-9]*$/u;
|
|
1943
|
+
const occurrenceAddress = (cycle, finding) => `c${cycle}f${finding}`;
|
|
1944
|
+
/** The 1-based recording position an address names. */
|
|
1945
|
+
const cycleOfAddress = (address) => Number(/^c(?<cycle>\d+)f/u.exec(address)?.groups?.cycle);
|
|
1946
|
+
const POLICY_DECLARATION_RULES = [
|
|
1947
|
+
"cycle-2-severity-floor",
|
|
1948
|
+
"identity-repeat",
|
|
1949
|
+
"schema-2-import"
|
|
1950
|
+
];
|
|
1951
|
+
const definedAttributes = (finding) => Object.fromEntries([
|
|
1952
|
+
"blockingAfterCap",
|
|
1953
|
+
"category",
|
|
1954
|
+
"citedSpan",
|
|
1955
|
+
"file",
|
|
1956
|
+
"prescribedAction",
|
|
1957
|
+
"priority"
|
|
1958
|
+
].flatMap((attribute) => finding[attribute] === void 0 ? [] : [[attribute, finding[attribute]]]));
|
|
1959
|
+
const toLadderFindingReport = (finding) => ({
|
|
1960
|
+
...definedAttributes(finding),
|
|
1961
|
+
...finding.supersedes ? { supersedes: finding.supersedes } : {},
|
|
1962
|
+
title: finding.title
|
|
1963
|
+
});
|
|
1964
|
+
const AUTHORITY_RANK = {
|
|
1965
|
+
operator: 3,
|
|
1966
|
+
policy: 1,
|
|
1967
|
+
reviewer: 2
|
|
1968
|
+
};
|
|
1969
|
+
const outranked = (occurrence, declaration) => occurrence.authority !== void 0 && AUTHORITY_RANK[occurrence.authority] > AUTHORITY_RANK[declaration.authority];
|
|
1970
|
+
const IDENTITY_REPEAT_DISPOSITION = {
|
|
1971
|
+
"fixed-in-thread": void 0,
|
|
1972
|
+
"follow-up-filed": "stale-repeat",
|
|
1973
|
+
open: void 0,
|
|
1974
|
+
"policy-accepted": "stale-repeat",
|
|
1975
|
+
"stale-repeat": "stale-repeat",
|
|
1976
|
+
waived: "stale-repeat"
|
|
1977
|
+
};
|
|
1978
|
+
const projectedGroupDisposition = (members) => members.some((member) => member.disposition === "open") ? "open" : members.at(-1)?.disposition ?? "open";
|
|
1979
|
+
const invalid = (message) => {
|
|
1980
|
+
throw new TypeError(`Invalid ladder history: ${message}`);
|
|
1981
|
+
};
|
|
1982
|
+
const entryFor = (members, reopenedCount) => {
|
|
1983
|
+
const [root] = members;
|
|
1984
|
+
const latest = members.at(-1);
|
|
1985
|
+
if (!root || !latest) return invalid("a finding group has no members.");
|
|
1986
|
+
const disposition = projectedGroupDisposition(members);
|
|
1987
|
+
const settled = members.findLast((member) => member.disposition !== "stale-repeat" && member.disposition !== "open");
|
|
1988
|
+
const referenced = disposition === "open" ? void 0 : settled ?? latest;
|
|
1989
|
+
return {
|
|
1990
|
+
...definedAttributes(latest.finding),
|
|
1991
|
+
address: root.address,
|
|
1992
|
+
disposition,
|
|
1993
|
+
firstFlagged: root.cycle,
|
|
1994
|
+
flagCount: members.length,
|
|
1995
|
+
lastFlagged: latest.cycle,
|
|
1996
|
+
occurrences: [...members],
|
|
1997
|
+
...disposition === "stale-repeat" && settled ? { priorDisposition: settled.disposition } : {},
|
|
1998
|
+
...referenced === void 0 ? {} : { reference: referenced.reference ?? referenced.rule },
|
|
1999
|
+
reopenedCount,
|
|
2000
|
+
staleRepeatCount: members.filter((member) => member.disposition === "stale-repeat").length,
|
|
2001
|
+
title: root.finding.title.trim()
|
|
2002
|
+
};
|
|
2003
|
+
};
|
|
2004
|
+
const recordOccurrences = (cycle, cycleIndex, ref, occurrences, recordedIn, groups, reopened) => {
|
|
2005
|
+
const { recording } = cycle;
|
|
2006
|
+
if (!recording) return invalid(`cycle ${cycleIndex + 1} carries no recording; the ledger reads recorded occurrences and declarations only.`);
|
|
2007
|
+
if (recording.occurrences.length !== cycle.findings.length) return invalid(`cycle ${cycleIndex + 1} records ${recording.occurrences.length} occurrence(s) for ${cycle.findings.length} finding(s).`);
|
|
2008
|
+
for (const [findingIndex, record] of recording.occurrences.entries()) {
|
|
2009
|
+
const address = occurrenceAddress(cycleIndex + 1, findingIndex + 1);
|
|
2010
|
+
const finding = cycle.findings[findingIndex];
|
|
2011
|
+
if (record.address !== address || finding === void 0) return invalid(`cycle ${cycleIndex + 1} finding ${findingIndex + 1} is recorded at ${record.address}, not ${address}.`);
|
|
2012
|
+
let root = address;
|
|
2013
|
+
if (record.repeatOf !== void 0) {
|
|
2014
|
+
const target = occurrences.get(record.repeatOf);
|
|
2015
|
+
if (!target || (recordedIn.get(record.repeatOf) ?? cycleIndex) >= cycleIndex) return invalid(`${address} repeats ${record.repeatOf}, which names no occurrence recorded in an earlier cycle.`);
|
|
2016
|
+
({root} = target);
|
|
2017
|
+
}
|
|
2018
|
+
const members = groups.get(root) ?? [];
|
|
2019
|
+
if (members.length > 0 && projectedGroupDisposition(members) === "fixed-in-thread") reopened.set(root, (reopened.get(root) ?? 0) + 1);
|
|
2020
|
+
const occurrence = {
|
|
2021
|
+
address,
|
|
2022
|
+
cycle: ref,
|
|
2023
|
+
disposition: "open",
|
|
2024
|
+
finding,
|
|
2025
|
+
historical: cycle.historical === true,
|
|
2026
|
+
root
|
|
2027
|
+
};
|
|
2028
|
+
occurrences.set(address, occurrence);
|
|
2029
|
+
recordedIn.set(address, cycleIndex);
|
|
2030
|
+
groups.set(root, [...members, occurrence]);
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
const applyDeclaration = (declaration, cycleIndex, occurrences) => {
|
|
2034
|
+
const target = occurrences.get(declaration.target);
|
|
2035
|
+
if (!target) return invalid(`cycle ${cycleIndex + 1} declares ${declaration.disposition} on ${declaration.target}, which names no occurrence recorded at or before that cycle.`);
|
|
2036
|
+
if (outranked(target, declaration)) return;
|
|
2037
|
+
target.disposition = declaration.disposition;
|
|
2038
|
+
target.authority = declaration.authority;
|
|
2039
|
+
if (declaration.authority === "policy") {
|
|
2040
|
+
target.rule = declaration.rule;
|
|
2041
|
+
delete target.reference;
|
|
2042
|
+
} else {
|
|
2043
|
+
delete target.rule;
|
|
2044
|
+
if (declaration.reference === void 0) delete target.reference;
|
|
2045
|
+
else target.reference = declaration.reference;
|
|
2046
|
+
}
|
|
2047
|
+
};
|
|
2048
|
+
const foldLedger = (cycles, refs) => {
|
|
2049
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
2050
|
+
const recordedIn = /* @__PURE__ */ new Map();
|
|
2051
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2052
|
+
const reopened = /* @__PURE__ */ new Map();
|
|
2053
|
+
for (const [cycleIndex, cycle] of cycles.entries()) {
|
|
2054
|
+
const ref = refs[cycleIndex];
|
|
2055
|
+
if (!ref) return invalid(`cycle ${cycleIndex + 1} has no ladder position.`);
|
|
2056
|
+
const declarations = cycle.recording?.declarations ?? [];
|
|
2057
|
+
for (const declaration of declarations) if (cycleOfAddress(declaration.target) <= cycleIndex) applyDeclaration(declaration, cycleIndex, occurrences);
|
|
2058
|
+
recordOccurrences(cycle, cycleIndex, ref, occurrences, recordedIn, groups, reopened);
|
|
2059
|
+
for (const declaration of declarations) if (cycleOfAddress(declaration.target) > cycleIndex) applyDeclaration(declaration, cycleIndex, occurrences);
|
|
2060
|
+
}
|
|
2061
|
+
const ledger = /* @__PURE__ */ new Map();
|
|
2062
|
+
for (const [root, members] of groups) ledger.set(root, entryFor(members, reopened.get(root) ?? 0));
|
|
2063
|
+
return {
|
|
2064
|
+
ledger,
|
|
2065
|
+
occurrences: [...occurrences.values()]
|
|
2066
|
+
};
|
|
2067
|
+
};
|
|
2068
|
+
const openLadderFindings = (ledger) => ledger.filter((entry) => entry.disposition === "open");
|
|
2069
|
+
const occurrenceBlocks = (occurrence) => occurrence.disposition === "open" && resolveFindingBlocking(occurrence.finding, "open");
|
|
2070
|
+
const blockingLadderFindings = (ledger) => ledger.filter((entry) => entry.occurrences.some(occurrenceBlocks));
|
|
2071
|
+
const staleRepeatLadderFindings = (ledger) => ledger.filter((entry) => entry.disposition === "stale-repeat");
|
|
2072
|
+
//#endregion
|
|
2073
|
+
//#region src/review-recording-schema.ts
|
|
2074
|
+
const ABSENT_PROOF_DIGEST = "absent";
|
|
2075
|
+
const sha256Pattern = /^[0-9a-f]{64}$/u;
|
|
2076
|
+
const occurrenceAddressSchema$1 = z.string().regex(OCCURRENCE_ADDRESS_PATTERN);
|
|
2077
|
+
const declarationRecordSchema = z.union([z.object({
|
|
2078
|
+
authority: z.enum(["operator", "reviewer"]),
|
|
2079
|
+
disposition: z.enum([
|
|
2080
|
+
"fixed-in-thread",
|
|
2081
|
+
"follow-up-filed",
|
|
2082
|
+
"waived"
|
|
2083
|
+
]),
|
|
2084
|
+
reference: z.string().min(1).optional(),
|
|
2085
|
+
target: occurrenceAddressSchema$1
|
|
2086
|
+
}).strict(), z.object({
|
|
2087
|
+
authority: z.literal("policy"),
|
|
2088
|
+
disposition: z.enum(["policy-accepted", "stale-repeat"]),
|
|
2089
|
+
rule: z.enum(POLICY_DECLARATION_RULES),
|
|
2090
|
+
target: occurrenceAddressSchema$1
|
|
2091
|
+
}).strict()]);
|
|
2092
|
+
const recordsShape = {
|
|
2093
|
+
declarations: z.array(declarationRecordSchema),
|
|
2094
|
+
occurrences: z.array(z.object({
|
|
2095
|
+
address: occurrenceAddressSchema$1,
|
|
2096
|
+
declaredCycle: z.number().int().positive(),
|
|
2097
|
+
repeatOf: occurrenceAddressSchema$1.optional()
|
|
2098
|
+
}).strict())
|
|
2099
|
+
};
|
|
2100
|
+
const reviewCycleRecordingSchema = z.union([z.object({
|
|
2101
|
+
...recordsShape,
|
|
2102
|
+
appliedTo: z.union([z.string().regex(sha256Pattern), z.literal(ABSENT_PROOF_DIGEST)]),
|
|
2103
|
+
base: z.object({
|
|
2104
|
+
ref: z.string().min(1),
|
|
2105
|
+
sha: z.string().regex(/^[0-9a-f]{40}$/u)
|
|
2106
|
+
}).strict(),
|
|
2107
|
+
dispositionsDigest: z.string().regex(sha256Pattern).optional(),
|
|
2108
|
+
findingsDigest: z.string().regex(sha256Pattern),
|
|
2109
|
+
recordingId: z.string().regex(sha256Pattern)
|
|
2110
|
+
}).strict(), z.object({
|
|
2111
|
+
...recordsShape,
|
|
2112
|
+
imported: z.object({
|
|
2113
|
+
proofDigest: z.string().regex(sha256Pattern),
|
|
2114
|
+
schemaVersion: z.literal(2)
|
|
2115
|
+
}).strict()
|
|
2116
|
+
}).strict()]);
|
|
2117
|
+
//#endregion
|
|
2118
|
+
//#region src/review-recording.ts
|
|
2119
|
+
/**
|
|
2120
|
+
* The occurrences of the cycles already on the ladder, in recording order,
|
|
2121
|
+
* with the write-time key each carries for grouping the next cycle. A cycle
|
|
2122
|
+
* whose `repeatOf` is unknown indexes every occurrence as its own root.
|
|
2123
|
+
*/
|
|
2124
|
+
const occurrenceIndexFor = (cycles) => cycles.flatMap((cycle, cycleIndex) => cycle.findings.map((finding, findingIndex) => {
|
|
2125
|
+
const address = occurrenceAddress(cycleIndex + 1, findingIndex + 1);
|
|
2126
|
+
return {
|
|
2127
|
+
address,
|
|
2128
|
+
finding,
|
|
2129
|
+
key: findingKey(finding),
|
|
2130
|
+
root: cycle.recording?.occurrences[findingIndex]?.repeatOf ?? address
|
|
2131
|
+
};
|
|
2132
|
+
}));
|
|
2133
|
+
const soleRoot = (candidates) => {
|
|
2134
|
+
const roots = new Set(candidates.map((candidate) => candidate.root));
|
|
2135
|
+
return roots.size === 1 ? [...roots][0] : void 0;
|
|
2136
|
+
};
|
|
2137
|
+
/**
|
|
2138
|
+
* The occurrence records for a cycle about to be recorded at position
|
|
2139
|
+
* `cycleIndex` (1-based), grouped against the prior cycles once, at write
|
|
2140
|
+
* time, by the contract's two identity arms: exact `findingKey` equality,
|
|
2141
|
+
* then an explicit `supersedes` citation matched on title and file. A
|
|
2142
|
+
* citation that names occurrences from more than one group is ignored — the
|
|
2143
|
+
* occurrence becomes its own root — rather than guessed at. Span overlap is
|
|
2144
|
+
* never consulted (decision 3).
|
|
2145
|
+
*/
|
|
2146
|
+
const occurrencesForCycle = ({ cycleIndex, declaredCycle, findings, priorCycles }) => {
|
|
2147
|
+
const prior = occurrenceIndexFor(priorCycles);
|
|
2148
|
+
return findings.map((finding, findingIndex) => {
|
|
2149
|
+
const key = findingKey(finding);
|
|
2150
|
+
const { supersedes } = finding;
|
|
2151
|
+
const exact = prior.filter((candidate) => candidate.key === key);
|
|
2152
|
+
const cited = exact.length === 0 && supersedes !== void 0 ? prior.filter((candidate) => declarationMatchesEntry(candidate.finding, supersedes)) : [];
|
|
2153
|
+
const repeatOf = soleRoot(exact.length > 0 ? exact : cited);
|
|
2154
|
+
return {
|
|
2155
|
+
address: occurrenceAddress(cycleIndex, findingIndex + 1),
|
|
2156
|
+
declaredCycle,
|
|
2157
|
+
...repeatOf === void 0 ? {} : { repeatOf }
|
|
2158
|
+
};
|
|
2159
|
+
});
|
|
2160
|
+
};
|
|
2161
|
+
const occurrenceAddressSchema = z.string().regex(OCCURRENCE_ADDRESS_PATTERN);
|
|
2162
|
+
const declarableDisposition = z.enum([
|
|
2163
|
+
"fixed-in-thread",
|
|
2164
|
+
"follow-up-filed",
|
|
2165
|
+
"waived"
|
|
2166
|
+
]);
|
|
2167
|
+
/**
|
|
2168
|
+
* Wire schema for `pr:review --dispositions`. A declaration names one
|
|
2169
|
+
* occurrence by address (`target`), or, as a convenience for the readout
|
|
2170
|
+
* operators have used so far, a finding by title and file; the latter is
|
|
2171
|
+
* translated to addresses at recording time and refused when it is ambiguous.
|
|
2172
|
+
*/
|
|
2173
|
+
const dispositionDeclarationsFileSchema = z.array(z.union([z.object({
|
|
2174
|
+
disposition: declarableDisposition,
|
|
2175
|
+
reference: z.string().min(1).optional(),
|
|
2176
|
+
target: occurrenceAddressSchema
|
|
2177
|
+
}).strict(), z.object({
|
|
2178
|
+
disposition: declarableDisposition,
|
|
2179
|
+
finding: z.object({
|
|
2180
|
+
file: z.string().min(1).optional(),
|
|
2181
|
+
title: z.string().min(1)
|
|
2182
|
+
}).strict(),
|
|
2183
|
+
reference: z.string().min(1).optional()
|
|
2184
|
+
}).strict()]));
|
|
2185
|
+
const describeOccurrences = (occurrences) => occurrences.map((occurrence) => `${occurrence.address} (${occurrence.finding.title}${occurrence.finding.file === void 0 ? "" : ` in ${occurrence.finding.file}`})`).join("; ");
|
|
2186
|
+
/**
|
|
2187
|
+
* Translate this recording's declarations into the records it stores.
|
|
2188
|
+
* `index` covers the prior cycles and, for a historical recording, this
|
|
2189
|
+
* cycle's own findings (#1099). An address must name a recorded occurrence.
|
|
2190
|
+
* A title-plus-file declaration must resolve to exactly one repeat group; it
|
|
2191
|
+
* is then stored once per occurrence in that group, which is what an
|
|
2192
|
+
* entry-wide disposition means in the contract's per-occurrence vocabulary.
|
|
2193
|
+
* More than one group is the ambiguity decision 7 refuses, and the refusal
|
|
2194
|
+
* prints the candidate addresses so the operator can name one.
|
|
2195
|
+
*/
|
|
2196
|
+
const resolveDeclaredDispositions = ({ declarations, index, resolutions }) => {
|
|
2197
|
+
const records = [];
|
|
2198
|
+
const available = () => index.length === 0 ? "no occurrence is recorded" : describeOccurrences(index);
|
|
2199
|
+
const assertAddress = (target, what) => {
|
|
2200
|
+
if (!index.some((candidate) => candidate.address === target)) throw new Error(`${what} targets ${target}, which names no recorded occurrence. Recorded occurrences: ${available()}.`);
|
|
2201
|
+
};
|
|
2202
|
+
for (const declaration of declarations) {
|
|
2203
|
+
const reference = declaration.reference === void 0 ? {} : { reference: declaration.reference };
|
|
2204
|
+
if ("target" in declaration) {
|
|
2205
|
+
assertAddress(declaration.target, "--dispositions entry");
|
|
2206
|
+
records.push({
|
|
2207
|
+
authority: "operator",
|
|
2208
|
+
disposition: declaration.disposition,
|
|
2209
|
+
...reference,
|
|
2210
|
+
target: declaration.target
|
|
2211
|
+
});
|
|
2212
|
+
continue;
|
|
2213
|
+
}
|
|
2214
|
+
const matches = index.filter((candidate) => declarationMatchesEntry(candidate.finding, declaration.finding));
|
|
2215
|
+
if (matches.length === 0) throw new Error(`--dispositions entry "${declaration.finding.title}" matches no recorded finding. Recorded occurrences: ${available()}.`);
|
|
2216
|
+
if (soleRoot(matches) === void 0) throw new Error(`--dispositions entry "${declaration.finding.title}" names more than one recorded finding; name one by address instead. Candidates: ${describeOccurrences(matches)}.`);
|
|
2217
|
+
for (const occurrence of matches) records.push({
|
|
2218
|
+
authority: "operator",
|
|
2219
|
+
disposition: declaration.disposition,
|
|
2220
|
+
...reference,
|
|
2221
|
+
target: occurrence.address
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
for (const resolution of resolutions) {
|
|
2225
|
+
assertAddress(resolution.target, "findings resolution");
|
|
2226
|
+
records.push({
|
|
2227
|
+
authority: "reviewer",
|
|
2228
|
+
disposition: "fixed-in-thread",
|
|
2229
|
+
reference: resolution.reference,
|
|
2230
|
+
target: resolution.target
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
return records;
|
|
2234
|
+
};
|
|
2235
|
+
//#endregion
|
|
1651
2236
|
//#region src/pr-proof-io.ts
|
|
1652
2237
|
const proofPathFor = (descriptor, cwd, filePath = descriptor.defaultPath) => path.resolve(cwd, filePath);
|
|
1653
2238
|
const readProofFromPath = (parse, filePath) => parse(JSON.parse(readFileSync(filePath, "utf-8")));
|
|
1654
|
-
const readProofFor = (descriptor, cwd, filePath) =>
|
|
2239
|
+
const readProofFor = (descriptor, cwd, filePath) => {
|
|
2240
|
+
const target = proofPathFor(descriptor, cwd, filePath);
|
|
2241
|
+
return descriptor.parseBytes ? descriptor.parseBytes(readFileSync(target, "utf-8")) : readProofFromPath(descriptor.parse, target);
|
|
2242
|
+
};
|
|
1655
2243
|
const readOptionalProofFor = (descriptor, cwd, filePath) => readOptionalProof((p) => readProofFor(descriptor, cwd, filePath ?? p), proofPathFor(descriptor, cwd, filePath));
|
|
1656
2244
|
const writeProofFor = (descriptor, cwd, value, filePath) => writeProofJson(value, proofPathFor(descriptor, cwd, filePath));
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
2245
|
+
const nodeProofWriteFileSystem = {
|
|
2246
|
+
closeSync,
|
|
2247
|
+
fsyncSync,
|
|
2248
|
+
mkdirSync,
|
|
2249
|
+
openSync,
|
|
2250
|
+
readFileSync,
|
|
2251
|
+
renameSync,
|
|
2252
|
+
statSync,
|
|
2253
|
+
unlinkSync,
|
|
2254
|
+
writeFileSync
|
|
2255
|
+
};
|
|
2256
|
+
const fsyncPath = (fs, target) => {
|
|
2257
|
+
const fd = fs.openSync(target, "r");
|
|
2258
|
+
try {
|
|
2259
|
+
fs.fsyncSync(fd);
|
|
2260
|
+
} finally {
|
|
2261
|
+
fs.closeSync(fd);
|
|
2262
|
+
}
|
|
2263
|
+
};
|
|
2264
|
+
/**
|
|
2265
|
+
* Write a proof so that the canonical path is never observable half-written:
|
|
2266
|
+
* the bytes land in a sibling temp file, are fsynced, are renamed over the
|
|
2267
|
+
* target in one step, the directory entry is fsynced, and the target is read
|
|
2268
|
+
* back and compared to what was meant to be written (#1131). A fault at any
|
|
2269
|
+
* step leaves the previous file (or its absence) intact, removes the temp
|
|
2270
|
+
* file, and throws. A read-back mismatch throws too, so a "success" is never
|
|
2271
|
+
* reported for bytes that are not on disk.
|
|
2272
|
+
*
|
|
2273
|
+
* The temp file sits beside the target because rename is only atomic within
|
|
2274
|
+
* one file system. The directory fsync is skipped on Windows, which cannot
|
|
2275
|
+
* open a directory for fsync.
|
|
2276
|
+
*/
|
|
2277
|
+
function writeProofJson(proof, output, fs = nodeProofWriteFileSystem) {
|
|
2278
|
+
const serialized = `${JSON.stringify(proof, null, 2)}\n`;
|
|
2279
|
+
const directory = path.dirname(output);
|
|
2280
|
+
const temp = path.join(directory, `.${path.basename(output)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
2281
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
2282
|
+
let renamed = false;
|
|
2283
|
+
try {
|
|
2284
|
+
fs.writeFileSync(temp, serialized);
|
|
2285
|
+
fsyncPath(fs, temp);
|
|
2286
|
+
fs.renameSync(temp, output);
|
|
2287
|
+
renamed = true;
|
|
2288
|
+
if (process.platform !== "win32") fsyncPath(fs, directory);
|
|
2289
|
+
} finally {
|
|
2290
|
+
if (!renamed) try {
|
|
2291
|
+
fs.unlinkSync(temp);
|
|
2292
|
+
} catch {}
|
|
2293
|
+
}
|
|
2294
|
+
const readBack = fs.readFileSync(output, "utf-8");
|
|
2295
|
+
if (readBack !== serialized) throw new Error(`Proof write to ${output} did not read back as written (${readBack.length} bytes on disk, ${serialized.length} expected); the file cannot be trusted.`);
|
|
1660
2296
|
}
|
|
1661
2297
|
const writeProofToPaths = (writeOne, canonical, additional) => {
|
|
1662
2298
|
writeOne(canonical);
|
|
1663
2299
|
if (additional !== void 0 && additional !== canonical) writeOne(additional);
|
|
1664
2300
|
};
|
|
2301
|
+
const readProofFileState = (filePath, fs = nodeProofWriteFileSystem) => {
|
|
2302
|
+
try {
|
|
2303
|
+
const bytes = fs.readFileSync(filePath, "utf-8");
|
|
2304
|
+
return {
|
|
2305
|
+
bytes,
|
|
2306
|
+
digest: sha256Hex(bytes),
|
|
2307
|
+
present: true
|
|
2308
|
+
};
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
if (error?.code === "ENOENT") return {
|
|
2311
|
+
digest: ABSENT_PROOF_DIGEST,
|
|
2312
|
+
present: false
|
|
2313
|
+
};
|
|
2314
|
+
throw error;
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
/**
|
|
2318
|
+
* The retryable `concurrent-write` refusal: nothing was written, and the
|
|
2319
|
+
* recording must be retried against the current proof. Either the proof's
|
|
2320
|
+
* digest moved between the recording's read and its write, another writer
|
|
2321
|
+
* held the proof lock for the whole wait window, or this writer's lock was
|
|
2322
|
+
* removed or replaced before its rename (#1132).
|
|
2323
|
+
*/
|
|
2324
|
+
var ConcurrentProofWriteError = class extends Error {
|
|
2325
|
+
constructor(output, cause) {
|
|
2326
|
+
super(concurrentWriteMessage(output, cause));
|
|
2327
|
+
this.name = "ConcurrentProofWriteError";
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
const concurrentWriteMessage = (output, cause) => {
|
|
2331
|
+
if ("expected" in cause) return `concurrent-write: ${output} changed while this recording was being prepared (applied to proof state ${cause.expected}, found ${cause.found}). Nothing was written; retry the recording against the current proof.`;
|
|
2332
|
+
if ("held" in cause) return `concurrent-write: ${describeHeldLock(cause.held)}; ${LOCK_REMOVAL_INSTRUCTION}. Nothing was written to ${output}.`;
|
|
2333
|
+
return `concurrent-write: proof lock ${cause.lockPath} is no longer this writer's (it was removed or replaced while the recording was being written). Nothing was written to ${output}; retry the recording against the current proof.`;
|
|
2334
|
+
};
|
|
2335
|
+
/**
|
|
2336
|
+
* The operator step for a crashed writer's lock. It is the same sentence in
|
|
2337
|
+
* the refusal, the release report and the doctor preflight warning, so a
|
|
2338
|
+
* grep finds every place it is stated.
|
|
2339
|
+
*/
|
|
2340
|
+
const LOCK_REMOVAL_INSTRUCTION = "if that writer is gone, remove the file and retry";
|
|
2341
|
+
const DEFAULT_LOCK_WAIT_MS = 5e3;
|
|
2342
|
+
const LOCK_POLL_MS = 20;
|
|
2343
|
+
const errorCode = (error) => error?.code;
|
|
2344
|
+
const sleepSync = (ms) => {
|
|
2345
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
2346
|
+
};
|
|
2347
|
+
const lockPathFor = (output) => `${output}.lock`;
|
|
2348
|
+
const lockContents = (token) => `${JSON.stringify({
|
|
2349
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2350
|
+
pid: process.pid,
|
|
2351
|
+
token
|
|
2352
|
+
})}\n`;
|
|
2353
|
+
const parseLockContents = (bytes) => {
|
|
2354
|
+
try {
|
|
2355
|
+
const parsed = JSON.parse(bytes);
|
|
2356
|
+
if (parsed && typeof parsed === "object") {
|
|
2357
|
+
const { pid, token } = parsed;
|
|
2358
|
+
return {
|
|
2359
|
+
pid: typeof pid === "number" ? String(pid) : "unknown",
|
|
2360
|
+
token: typeof token === "string" && token !== "" ? token : "unknown"
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
} catch {}
|
|
2364
|
+
return {
|
|
2365
|
+
pid: "unknown",
|
|
2366
|
+
token: "unknown"
|
|
2367
|
+
};
|
|
2368
|
+
};
|
|
2369
|
+
const describeHeldLock = (held) => `proof lock ${held.lockPath} held since ${held.since} (token ${held.token}, pid ${held.pid})`;
|
|
2370
|
+
/**
|
|
2371
|
+
* The proof lock beside `output`, if one exists, as a holder description
|
|
2372
|
+
* (#1132). Used by the refusal message and by `doctor --preflight`, which
|
|
2373
|
+
* warns about a lock older than the wait window because no writer will ever
|
|
2374
|
+
* remove it on the operator's behalf. `undefined` when there is no lock.
|
|
2375
|
+
*/
|
|
2376
|
+
const describeHeldProofLock = (output, fs = nodeProofWriteFileSystem, now = Date.now()) => {
|
|
2377
|
+
const lockPath = lockPathFor(output);
|
|
2378
|
+
let mtimeMs;
|
|
2379
|
+
try {
|
|
2380
|
+
({mtimeMs} = fs.statSync(lockPath));
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
if (errorCode(error) === "ENOENT") return;
|
|
2383
|
+
throw error;
|
|
2384
|
+
}
|
|
2385
|
+
let contents = "";
|
|
2386
|
+
try {
|
|
2387
|
+
contents = fs.readFileSync(lockPath, "utf-8");
|
|
2388
|
+
} catch {}
|
|
2389
|
+
return {
|
|
2390
|
+
ageMs: Math.max(0, now - mtimeMs),
|
|
2391
|
+
lockPath,
|
|
2392
|
+
since: new Date(mtimeMs).toISOString(),
|
|
2393
|
+
...parseLockContents(contents)
|
|
2394
|
+
};
|
|
2395
|
+
};
|
|
2396
|
+
/** True when `doctor --preflight` should warn about this lock (#1132). */
|
|
2397
|
+
const proofLockOutlivesWait = (held) => held.ageMs > DEFAULT_LOCK_WAIT_MS;
|
|
2398
|
+
/**
|
|
2399
|
+
* Take the exclusive proof lock — a sibling `<proof>.lock` created with
|
|
2400
|
+
* O_EXCL and stamped with a fresh token, so exactly one writer holds it and
|
|
2401
|
+
* the holder can recognise its own file (#1132). A held lock is waited on for
|
|
2402
|
+
* `lockWaitMs`, then refused naming the holder. Nothing here ever removes a
|
|
2403
|
+
* lock it did not create. Returns the token and the release.
|
|
2404
|
+
*/
|
|
2405
|
+
const acquireProofLock = (output, fs, { lockWaitMs = DEFAULT_LOCK_WAIT_MS, report = (line) => process.stderr.write(`${line}\n`) }) => {
|
|
2406
|
+
const lockPath = lockPathFor(output);
|
|
2407
|
+
const deadline = Date.now() + lockWaitMs;
|
|
2408
|
+
const created = () => {
|
|
2409
|
+
try {
|
|
2410
|
+
fs.closeSync(fs.openSync(lockPath, "wx"));
|
|
2411
|
+
return true;
|
|
2412
|
+
} catch (error) {
|
|
2413
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
2414
|
+
return false;
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
while (!created()) if (Date.now() < deadline) sleepSync(LOCK_POLL_MS);
|
|
2418
|
+
else {
|
|
2419
|
+
const held = describeHeldProofLock(output, fs);
|
|
2420
|
+
if (held !== void 0) throw new ConcurrentProofWriteError(output, { held });
|
|
2421
|
+
}
|
|
2422
|
+
const mine = lockContents(randomUUID());
|
|
2423
|
+
fs.writeFileSync(lockPath, mine);
|
|
2424
|
+
return {
|
|
2425
|
+
mine,
|
|
2426
|
+
release: () => {
|
|
2427
|
+
let current;
|
|
2428
|
+
try {
|
|
2429
|
+
current = fs.readFileSync(lockPath, "utf-8");
|
|
2430
|
+
} catch (error) {
|
|
2431
|
+
report(errorCode(error) === "ENOENT" ? `pr-proof: lock ${lockPath} was removed by someone else before this writer released it; nothing to release.` : `pr-proof: lock ${lockPath} could not be read on release (${String(error)}); left in place.`);
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
if (current !== mine) {
|
|
2435
|
+
const { pid, token } = parseLockContents(current);
|
|
2436
|
+
report(`pr-proof: lock ${lockPath} is not this writer's (token ${token}, pid ${pid}); left in place.`);
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
try {
|
|
2440
|
+
fs.unlinkSync(lockPath);
|
|
2441
|
+
} catch (error) {
|
|
2442
|
+
report(`pr-proof: lock ${lockPath} could not be removed on release (${String(error)}); ${LOCK_REMOVAL_INSTRUCTION}.`);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
};
|
|
2446
|
+
};
|
|
2447
|
+
/**
|
|
2448
|
+
* The file system a locked write renames through: immediately before the
|
|
2449
|
+
* proof rename it re-reads the lock and refuses unless the contents are this
|
|
2450
|
+
* writer's token. Nothing is renamed on a mismatch, and `writeProofJson`
|
|
2451
|
+
* removes the temp file as for any other fault.
|
|
2452
|
+
*/
|
|
2453
|
+
const ownershipCheckedFileSystem = (fs, output, mine) => ({
|
|
2454
|
+
...fs,
|
|
2455
|
+
renameSync: (from, to) => {
|
|
2456
|
+
if (to === output) {
|
|
2457
|
+
let current;
|
|
2458
|
+
try {
|
|
2459
|
+
current = fs.readFileSync(lockPathFor(output), "utf-8");
|
|
2460
|
+
} catch {
|
|
2461
|
+
current = void 0;
|
|
2462
|
+
}
|
|
2463
|
+
if (current !== mine) throw new ConcurrentProofWriteError(output, {
|
|
2464
|
+
lockPath: lockPathFor(output),
|
|
2465
|
+
replaced: true
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
fs.renameSync(from, to);
|
|
2469
|
+
}
|
|
2470
|
+
});
|
|
2471
|
+
/**
|
|
2472
|
+
* Compare-and-swap proof write (#1131, decision 6; serialized by #1132). The
|
|
2473
|
+
* file is re-read under an exclusive lock and replaced only when its bytes
|
|
2474
|
+
* still digest to `appliedTo`, the state the recording was computed against.
|
|
2475
|
+
* The lock covers read → compare → temp write → fsync → ownership check →
|
|
2476
|
+
* rename → directory fsync → read-back, so two writers that both read the
|
|
2477
|
+
* same prior state cannot both succeed: the second one in waits, then gets
|
|
2478
|
+
* the retryable `concurrent-write` refusal before any temp file exists.
|
|
2479
|
+
*
|
|
2480
|
+
* Scope: writers on one host sharing one file system (the O_EXCL create is
|
|
2481
|
+
* the mutual exclusion; no cross-host guarantee). A writer that dies holding
|
|
2482
|
+
* the lock leaves it behind, and every later writer refuses naming it: the
|
|
2483
|
+
* lock is removed by an operator, never reclaimed (see the invariants above).
|
|
2484
|
+
* The canonical path is never observable half-written (`writeProofJson`).
|
|
2485
|
+
*/
|
|
2486
|
+
function writeProofJsonApplied(proof, output, appliedTo, fs = nodeProofWriteFileSystem, options = {}) {
|
|
2487
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
2488
|
+
const { mine, release } = acquireProofLock(output, fs, options);
|
|
2489
|
+
try {
|
|
2490
|
+
const current = readProofFileState(output, fs);
|
|
2491
|
+
if (current.digest !== appliedTo) throw new ConcurrentProofWriteError(output, {
|
|
2492
|
+
expected: appliedTo,
|
|
2493
|
+
found: current.digest
|
|
2494
|
+
});
|
|
2495
|
+
writeProofJson(proof, output, ownershipCheckedFileSystem(fs, output, mine));
|
|
2496
|
+
} finally {
|
|
2497
|
+
release();
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
1665
2500
|
function tryReadProof(read, filePath) {
|
|
1666
2501
|
try {
|
|
1667
2502
|
return { proof: read(filePath) };
|
|
@@ -1713,6 +2548,249 @@ function withPinnedNodeOnPath(command, nodeBinDir) {
|
|
|
1713
2548
|
return `export PATH=${singleQuote(nodeBinDir)}:"$PATH"\n${command}`;
|
|
1714
2549
|
}
|
|
1715
2550
|
//#endregion
|
|
2551
|
+
//#region src/github-issue-comments.ts
|
|
2552
|
+
const isCommentPage = (value) => Array.isArray(value[0]);
|
|
2553
|
+
const normalizeIssueComments = (value) => {
|
|
2554
|
+
return (isCommentPage(value) ? value.flat() : value).map((comment) => ({
|
|
2555
|
+
author: { login: comment.user?.login },
|
|
2556
|
+
body: comment.body,
|
|
2557
|
+
createdAt: comment.created_at,
|
|
2558
|
+
id: comment.node_id,
|
|
2559
|
+
updatedAt: comment.updated_at,
|
|
2560
|
+
url: comment.html_url
|
|
2561
|
+
}));
|
|
2562
|
+
};
|
|
2563
|
+
//#endregion
|
|
2564
|
+
//#region src/github-client.ts
|
|
2565
|
+
/**
|
|
2566
|
+
* Shared `gh` CLI client for the factory.
|
|
2567
|
+
*
|
|
2568
|
+
* Every factory `gh` invocation routes through {@link runCapture} so it inherits
|
|
2569
|
+
* the shared-stash and worker-control-checkout guards plus the git-env scrub,
|
|
2570
|
+
* and so spawn configuration + JSON parsing live in exactly one place. The
|
|
2571
|
+
* guards are inert for `gh` (they only trip on `git stash` / worker-checkout
|
|
2572
|
+
* mutations), and `runCapture`'s failure message is byte-identical to the
|
|
2573
|
+
* hand-rolled wrappers this module replaces (`gh <args> failed:\n<stderr>`).
|
|
2574
|
+
*
|
|
2575
|
+
* Field projections (`PR_VIEW_JSON_FIELDS`) and telemetry-wrapped GraphQL
|
|
2576
|
+
* queries deliberately stay with their consumers — they are per-consumer
|
|
2577
|
+
* shapes, not shared plumbing.
|
|
2578
|
+
*/
|
|
2579
|
+
const runGhCapture = (args, cwd = process.cwd(), acceptableExitCodes = [0]) => runCapture("gh", args, cwd, { acceptableExitCodes });
|
|
2580
|
+
const runGhJsonAt = (args, cwd) => JSON.parse(runGhCapture(args, cwd).stdout);
|
|
2581
|
+
const runGhJson = (args) => runGhJsonAt(args, process.cwd());
|
|
2582
|
+
const gitReferenceSchema = z.object({ object: z.object({ sha: z.string().regex(/^[0-9a-f]{40}$/u) }) });
|
|
2583
|
+
/** Parse GitHub's git-ref JSON (`GET .../git/ref/heads/<name>`). */
|
|
2584
|
+
const parseGitHubBranchTipResponse = (value) => gitReferenceSchema.parse(value).object.sha;
|
|
2585
|
+
const githubBranchTipApiPath = (repository, branch) => `/repos/${repository.owner}/${repository.name}/git/ref/heads/${encodeURIComponent(branch)}`;
|
|
2586
|
+
/**
|
|
2587
|
+
* Read a branch tip from GitHub rather than a checkout's cached remote ref.
|
|
2588
|
+
* `gh` is injectable so a test can hand the real read a fake GitHub.
|
|
2589
|
+
*/
|
|
2590
|
+
const fetchGitHubBranchTipSha = (repository, branch, cwd, gh = runGhJsonAt) => parseGitHubBranchTipResponse(gh(["api", githubBranchTipApiPath(repository, branch)], cwd));
|
|
2591
|
+
const repositoryFileSchema = z.object({
|
|
2592
|
+
content: z.string(),
|
|
2593
|
+
encoding: z.literal("base64"),
|
|
2594
|
+
type: z.literal("file")
|
|
2595
|
+
});
|
|
2596
|
+
/**
|
|
2597
|
+
* Read one file's content at an exact commit from GitHub.
|
|
2598
|
+
*
|
|
2599
|
+
* #922: the authoritative base policy is the profile GitHub serves at the base
|
|
2600
|
+
* tip. A checkout is candidate-controlled, so it is not an authority for it.
|
|
2601
|
+
* There is exactly one implementation on purpose — no local-first read with an
|
|
2602
|
+
* API fallback, which would let a checkout answer the authority's question.
|
|
2603
|
+
*
|
|
2604
|
+
* Throws for every failure — an absent path, a directory, a file too large for
|
|
2605
|
+
* the contents endpoint to inline, an unreachable or unauthorized API. The
|
|
2606
|
+
* caller decides what an unreadable authority means; it is never an absent one.
|
|
2607
|
+
*/
|
|
2608
|
+
const fetchGitHubFileAtRef = (repository, filePath, ref, cwd) => {
|
|
2609
|
+
const encodedPath = filePath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
2610
|
+
const response = repositoryFileSchema.parse(runGhJsonAt(["api", `/repos/${repository.owner}/${repository.name}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`], cwd));
|
|
2611
|
+
return Buffer$1.from(response.content, "base64").toString("utf-8");
|
|
2612
|
+
};
|
|
2613
|
+
/**
|
|
2614
|
+
* Fetch and normalize the issue/PR comment thread for `<owner>/<repo>`.
|
|
2615
|
+
*
|
|
2616
|
+
* `--slurp` postdates gh 2.46; with `--paginate` alone gh returns a single
|
|
2617
|
+
* merged array. `normalizeIssueComments` accepts both the merged and slurped
|
|
2618
|
+
* (array-of-arrays) shapes, so behavior is identical on either gh version
|
|
2619
|
+
* (patronage/internal#380).
|
|
2620
|
+
*/
|
|
2621
|
+
const fetchIssueComments = (fullName, issueNumber) => normalizeIssueComments(runGhJson([
|
|
2622
|
+
"api",
|
|
2623
|
+
"--paginate",
|
|
2624
|
+
`/repos/${fullName}/issues/${issueNumber}/comments`
|
|
2625
|
+
]));
|
|
2626
|
+
const livePullRequestBaseSchema = z.object({
|
|
2627
|
+
baseRefName: z.string().min(1),
|
|
2628
|
+
isCrossRepository: z.unknown(),
|
|
2629
|
+
number: z.number().int().positive()
|
|
2630
|
+
});
|
|
2631
|
+
const failureText$3 = (error) => error instanceof Error ? error.message : String(error);
|
|
2632
|
+
/** `gh pr view` exits non-zero when the branch has no pull request (#323). */
|
|
2633
|
+
const isNoPullRequestYet$1 = (message) => /no pull requests found for branch/iu.test(message);
|
|
2634
|
+
/**
|
|
2635
|
+
* The pull request GitHub associates with the checkout's current branch.
|
|
2636
|
+
* Only its number, base ref name and fork status are read: the base SHA
|
|
2637
|
+
* comes from the live ref, never from this projection.
|
|
2638
|
+
*/
|
|
2639
|
+
const lookupLivePullRequestBase = (cwd, gh = runGhJsonAt) => {
|
|
2640
|
+
try {
|
|
2641
|
+
const parsed = livePullRequestBaseSchema.safeParse(gh([
|
|
2642
|
+
"pr",
|
|
2643
|
+
"view",
|
|
2644
|
+
"--json",
|
|
2645
|
+
"number,baseRefName,isCrossRepository"
|
|
2646
|
+
], cwd));
|
|
2647
|
+
return parsed.success ? {
|
|
2648
|
+
pullRequest: parsed.data,
|
|
2649
|
+
status: "found"
|
|
2650
|
+
} : {
|
|
2651
|
+
reason: `GitHub did not report a usable base ref for this branch's pull request (${parsed.error.issues.map((issue) => issue.message).join("; ")}).`,
|
|
2652
|
+
status: "unavailable"
|
|
2653
|
+
};
|
|
2654
|
+
} catch (error) {
|
|
2655
|
+
const message = failureText$3(error);
|
|
2656
|
+
return isNoPullRequestYet$1(message) ? { status: "none" } : {
|
|
2657
|
+
reason: `the pull request lookup for this branch failed: ${message}`,
|
|
2658
|
+
status: "unavailable"
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
};
|
|
2662
|
+
/**
|
|
2663
|
+
* The live tip of a base branch, read from GitHub for the trusted checkout's
|
|
2664
|
+
* repository through the reader `pr:ready` uses.
|
|
2665
|
+
*/
|
|
2666
|
+
const liveBranchTipSha = (cwd, branch, gh = runGhJsonAt, repository = checkoutRepository(cwd)) => fetchGitHubBranchTipSha(repository, branch, cwd, gh);
|
|
2667
|
+
const fetchOriginRef = (cwd, refName) => {
|
|
2668
|
+
runCapture("git", [
|
|
2669
|
+
"fetch",
|
|
2670
|
+
"--no-tags",
|
|
2671
|
+
"origin",
|
|
2672
|
+
refName
|
|
2673
|
+
], cwd);
|
|
2674
|
+
};
|
|
2675
|
+
const resolveCommitSha = (cwd, ref) => {
|
|
2676
|
+
try {
|
|
2677
|
+
return runCapture("git", [
|
|
2678
|
+
"rev-parse",
|
|
2679
|
+
"--verify",
|
|
2680
|
+
`${ref}^{commit}`
|
|
2681
|
+
], cwd).stdout.trim();
|
|
2682
|
+
} catch {
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
};
|
|
2686
|
+
/**
|
|
2687
|
+
* The production dependencies, over one `gh` reader so a test can hand the
|
|
2688
|
+
* real lookups a fake GitHub and prove which read the binding trusts.
|
|
2689
|
+
*/
|
|
2690
|
+
const candidateBaseDependencies = ({ gh = runGhJsonAt, repository } = {}) => ({
|
|
2691
|
+
fetchOriginRef,
|
|
2692
|
+
liveBranchTipSha: (cwd, branch) => liveBranchTipSha(cwd, branch, gh, repository ?? checkoutRepository(cwd)),
|
|
2693
|
+
lookupPullRequestBase: (cwd) => lookupLivePullRequestBase(cwd, gh),
|
|
2694
|
+
resolveCommitSha
|
|
2695
|
+
});
|
|
2696
|
+
const ORIGIN_PREFIX = "origin/";
|
|
2697
|
+
/**
|
|
2698
|
+
* The GitHub branch a candidate base ref names. Proofs and `--base` carry the
|
|
2699
|
+
* local remote-tracking form (`origin/<branch>`); GitHub creates and compares
|
|
2700
|
+
* pull requests by `<branch>`. A ref without the remote qualifier is returned
|
|
2701
|
+
* as given.
|
|
2702
|
+
*/
|
|
2703
|
+
const candidateBaseBranch = (ref) => ref.startsWith(ORIGIN_PREFIX) ? ref.slice(7) : ref;
|
|
2704
|
+
const bindToPullRequest = ({ cwd, dependencies, pullRequest, requestedBase }) => {
|
|
2705
|
+
const ref = `${ORIGIN_PREFIX}${pullRequest.baseRefName}`;
|
|
2706
|
+
let tip;
|
|
2707
|
+
try {
|
|
2708
|
+
tip = dependencies.liveBranchTipSha(cwd, pullRequest.baseRefName);
|
|
2709
|
+
} catch (error) {
|
|
2710
|
+
throw new Error(`PR #${pullRequest.number} targets ${pullRequest.baseRefName}, but its live tip could not be read from GitHub: ${failureText$3(error)} pr:ready measures against that tip, so the candidate is not bound to a guessed base; restore ${pullRequest.baseRefName} or rerun once GitHub is reachable.`, { cause: error });
|
|
2711
|
+
}
|
|
2712
|
+
try {
|
|
2713
|
+
dependencies.fetchOriginRef(cwd, pullRequest.baseRefName);
|
|
2714
|
+
} catch {}
|
|
2715
|
+
if (!dependencies.resolveCommitSha(cwd, tip)) throw new Error(`The live base of PR #${pullRequest.number} (${ref} at ${tip}) is not available locally; fetch it and rerun.`);
|
|
2716
|
+
if (requestedBase !== void 0) {
|
|
2717
|
+
const requestedSha = dependencies.resolveCommitSha(cwd, requestedBase);
|
|
2718
|
+
if (requestedSha !== tip) throw new Error(`--base ${requestedBase} (${requestedSha ?? "unresolvable"}) does not resolve to the live base of PR #${pullRequest.number}, ${ref} at ${tip}. Omit --base to measure against the pull request's base, or pass ${ref}.`);
|
|
2719
|
+
}
|
|
2720
|
+
return {
|
|
2721
|
+
ref,
|
|
2722
|
+
sha: tip,
|
|
2723
|
+
source: {
|
|
2724
|
+
kind: "pull-request",
|
|
2725
|
+
number: pullRequest.number
|
|
2726
|
+
}
|
|
2727
|
+
};
|
|
2728
|
+
};
|
|
2729
|
+
const bindToRef = ({ cwd, dependencies, pullRequest, ref }) => {
|
|
2730
|
+
let fetched = false;
|
|
2731
|
+
if (ref.startsWith(ORIGIN_PREFIX)) try {
|
|
2732
|
+
dependencies.fetchOriginRef(cwd, ref.slice(7));
|
|
2733
|
+
fetched = true;
|
|
2734
|
+
} catch {}
|
|
2735
|
+
const sha = dependencies.resolveCommitSha(cwd, ref);
|
|
2736
|
+
if (!sha) throw new Error(`Base ${ref} does not resolve to a commit in this checkout; fetch it or pass --base <ref>.`);
|
|
2737
|
+
return {
|
|
2738
|
+
ref,
|
|
2739
|
+
sha,
|
|
2740
|
+
source: {
|
|
2741
|
+
fetched,
|
|
2742
|
+
kind: "ref",
|
|
2743
|
+
pullRequest
|
|
2744
|
+
}
|
|
2745
|
+
};
|
|
2746
|
+
};
|
|
2747
|
+
/**
|
|
2748
|
+
* Bind the candidate to its base. A pull request for the branch wins and its
|
|
2749
|
+
* live tip is the base; a `requestedBase` that disagrees with it is refused.
|
|
2750
|
+
* Without one, the requested or default ref is fetched fresh and resolved.
|
|
2751
|
+
* A blank `requestedBase` asserts nothing: `pr:publish` forwards its own
|
|
2752
|
+
* omitted `--base` as the empty string to the stages it runs (#1132).
|
|
2753
|
+
*/
|
|
2754
|
+
const resolveCandidateBase = ({ cwd, dependencies = candidateBaseDependencies(), requestedBase: requestedBaseInput }) => {
|
|
2755
|
+
const requestedBase = requestedBaseInput === void 0 || requestedBaseInput.trim() === "" ? void 0 : requestedBaseInput.trim();
|
|
2756
|
+
const lookup = dependencies.lookupPullRequestBase(cwd);
|
|
2757
|
+
return lookup.status === "found" ? bindToPullRequest({
|
|
2758
|
+
cwd,
|
|
2759
|
+
dependencies,
|
|
2760
|
+
pullRequest: lookup.pullRequest,
|
|
2761
|
+
requestedBase
|
|
2762
|
+
}) : bindToRef({
|
|
2763
|
+
cwd,
|
|
2764
|
+
dependencies,
|
|
2765
|
+
pullRequest: lookup,
|
|
2766
|
+
ref: requestedBase ?? "origin/main"
|
|
2767
|
+
});
|
|
2768
|
+
};
|
|
2769
|
+
/**
|
|
2770
|
+
* The policy-authority lookup, read from the pull request the base binding
|
|
2771
|
+
* already found (#1131): one GitHub read, one pull request, so the base the
|
|
2772
|
+
* candidate is measured against and the policy it is judged under can never
|
|
2773
|
+
* come from two different pull requests. `pr:review` and `pr:verify` both
|
|
2774
|
+
* feed `resolveStageReviewDemand` through this.
|
|
2775
|
+
*/
|
|
2776
|
+
const policyLookupFromBaseLookup = (lookup) => lookup.status === "found" ? {
|
|
2777
|
+
pullRequest: {
|
|
2778
|
+
baseRefName: lookup.pullRequest.baseRefName,
|
|
2779
|
+
isCrossRepository: lookup.pullRequest.isCrossRepository
|
|
2780
|
+
},
|
|
2781
|
+
status: "found"
|
|
2782
|
+
} : {
|
|
2783
|
+
reason: lookup.status === "none" ? "no pull request is open for this branch." : lookup.reason,
|
|
2784
|
+
status: "unavailable"
|
|
2785
|
+
};
|
|
2786
|
+
/** One line stating which base was bound and why. */
|
|
2787
|
+
const describeCandidateBase = (binding) => {
|
|
2788
|
+
if (binding.source.kind === "pull-request") return `base ${binding.ref} at ${binding.sha}, the live base of PR #${binding.source.number}`;
|
|
2789
|
+
const fetched = binding.source.fetched ? "fetched from origin" : "local ref (origin not fetched)";
|
|
2790
|
+
const pullRequest = binding.source.pullRequest.status === "none" ? "no pull request is open for this branch" : `the pull request lookup was unavailable, so no live PR base could be read (${binding.source.pullRequest.reason})`;
|
|
2791
|
+
return `base ${binding.ref} at ${binding.sha}, ${fetched}; ${pullRequest}`;
|
|
2792
|
+
};
|
|
2793
|
+
//#endregion
|
|
1716
2794
|
//#region src/candidate-identity.ts
|
|
1717
2795
|
/**
|
|
1718
2796
|
* The same fallback for a patch id taken at an explicit ref rather than at
|
|
@@ -1924,102 +3002,29 @@ function resolveCwdOption(cwd) {
|
|
|
1924
3002
|
return path.resolve(value);
|
|
1925
3003
|
}
|
|
1926
3004
|
function collectCwdOption(rawValue, previous) {
|
|
1927
|
-
if (previous === void 0) return rawValue;
|
|
1928
|
-
const previousResolved = resolveCwdOption(previous);
|
|
1929
|
-
const currentResolved = resolveCwdOption(rawValue);
|
|
1930
|
-
if (previousResolved !== currentResolved) throw new Error(`--cwd was passed multiple times with conflicting values: "${previous}" (resolves to ${previousResolved}) and "${rawValue}" (resolves to ${currentResolved})`);
|
|
1931
|
-
return rawValue;
|
|
1932
|
-
}
|
|
1933
|
-
function markCwdOptionDefault(command) {
|
|
1934
|
-
const option = command.options.find((candidate) => candidate.attributeName() === "cwd");
|
|
1935
|
-
if (option) {
|
|
1936
|
-
option.defaultValue = ".";
|
|
1937
|
-
option.defaultValueDescription = ".";
|
|
1938
|
-
}
|
|
1939
|
-
return command;
|
|
1940
|
-
}
|
|
1941
|
-
function resolveCheckoutPath(cwd, target) {
|
|
1942
|
-
return path.resolve(cwd, target);
|
|
1943
|
-
}
|
|
1944
|
-
function resolveProofOutputPaths(cwd, defaultPath, requested) {
|
|
1945
|
-
return {
|
|
1946
|
-
additional: requested ? resolveCheckoutPath(cwd, requested) : void 0,
|
|
1947
|
-
canonical: resolveCheckoutPath(cwd, defaultPath)
|
|
1948
|
-
};
|
|
1949
|
-
}
|
|
1950
|
-
//#endregion
|
|
1951
|
-
//#region src/github-issue-comments.ts
|
|
1952
|
-
const isCommentPage = (value) => Array.isArray(value[0]);
|
|
1953
|
-
const normalizeIssueComments = (value) => {
|
|
1954
|
-
return (isCommentPage(value) ? value.flat() : value).map((comment) => ({
|
|
1955
|
-
author: { login: comment.user?.login },
|
|
1956
|
-
body: comment.body,
|
|
1957
|
-
createdAt: comment.created_at,
|
|
1958
|
-
id: comment.node_id,
|
|
1959
|
-
updatedAt: comment.updated_at,
|
|
1960
|
-
url: comment.html_url
|
|
1961
|
-
}));
|
|
1962
|
-
};
|
|
1963
|
-
//#endregion
|
|
1964
|
-
//#region src/github-client.ts
|
|
1965
|
-
/**
|
|
1966
|
-
* Shared `gh` CLI client for the factory.
|
|
1967
|
-
*
|
|
1968
|
-
* Every factory `gh` invocation routes through {@link runCapture} so it inherits
|
|
1969
|
-
* the shared-stash and worker-control-checkout guards plus the git-env scrub,
|
|
1970
|
-
* and so spawn configuration + JSON parsing live in exactly one place. The
|
|
1971
|
-
* guards are inert for `gh` (they only trip on `git stash` / worker-checkout
|
|
1972
|
-
* mutations), and `runCapture`'s failure message is byte-identical to the
|
|
1973
|
-
* hand-rolled wrappers this module replaces (`gh <args> failed:\n<stderr>`).
|
|
1974
|
-
*
|
|
1975
|
-
* Field projections (`PR_VIEW_JSON_FIELDS`) and telemetry-wrapped GraphQL
|
|
1976
|
-
* queries deliberately stay with their consumers — they are per-consumer
|
|
1977
|
-
* shapes, not shared plumbing.
|
|
1978
|
-
*/
|
|
1979
|
-
const runGhCapture = (args, cwd = process.cwd(), acceptableExitCodes = [0]) => runCapture("gh", args, cwd, { acceptableExitCodes });
|
|
1980
|
-
const runGhJsonAt = (args, cwd) => JSON.parse(runGhCapture(args, cwd).stdout);
|
|
1981
|
-
const runGhJson = (args) => runGhJsonAt(args, process.cwd());
|
|
1982
|
-
const gitReferenceSchema = z.object({ object: z.object({ sha: z.string().regex(/^[0-9a-f]{40}$/u) }) });
|
|
1983
|
-
/** Parse GitHub's git-ref JSON (`GET .../git/ref/heads/<name>`). */
|
|
1984
|
-
const parseGitHubBranchTipResponse = (value) => gitReferenceSchema.parse(value).object.sha;
|
|
1985
|
-
const githubBranchTipApiPath = (repository, branch) => `/repos/${repository.owner}/${repository.name}/git/ref/heads/${encodeURIComponent(branch)}`;
|
|
1986
|
-
/** Read a branch tip from GitHub rather than a checkout's cached remote ref. */
|
|
1987
|
-
const fetchGitHubBranchTipSha = (repository, branch, cwd) => parseGitHubBranchTipResponse(runGhJsonAt(["api", githubBranchTipApiPath(repository, branch)], cwd));
|
|
1988
|
-
const repositoryFileSchema = z.object({
|
|
1989
|
-
content: z.string(),
|
|
1990
|
-
encoding: z.literal("base64"),
|
|
1991
|
-
type: z.literal("file")
|
|
1992
|
-
});
|
|
1993
|
-
/**
|
|
1994
|
-
* Read one file's content at an exact commit from GitHub.
|
|
1995
|
-
*
|
|
1996
|
-
* #922: the authoritative base policy is the profile GitHub serves at the base
|
|
1997
|
-
* tip. A checkout is candidate-controlled, so it is not an authority for it.
|
|
1998
|
-
* There is exactly one implementation on purpose — no local-first read with an
|
|
1999
|
-
* API fallback, which would let a checkout answer the authority's question.
|
|
2000
|
-
*
|
|
2001
|
-
* Throws for every failure — an absent path, a directory, a file too large for
|
|
2002
|
-
* the contents endpoint to inline, an unreachable or unauthorized API. The
|
|
2003
|
-
* caller decides what an unreadable authority means; it is never an absent one.
|
|
2004
|
-
*/
|
|
2005
|
-
const fetchGitHubFileAtRef = (repository, filePath, ref, cwd) => {
|
|
2006
|
-
const encodedPath = filePath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
2007
|
-
const response = repositoryFileSchema.parse(runGhJsonAt(["api", `/repos/${repository.owner}/${repository.name}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`], cwd));
|
|
2008
|
-
return Buffer$1.from(response.content, "base64").toString("utf-8");
|
|
2009
|
-
};
|
|
2010
|
-
/**
|
|
2011
|
-
* Fetch and normalize the issue/PR comment thread for `<owner>/<repo>`.
|
|
2012
|
-
*
|
|
2013
|
-
* `--slurp` postdates gh 2.46; with `--paginate` alone gh returns a single
|
|
2014
|
-
* merged array. `normalizeIssueComments` accepts both the merged and slurped
|
|
2015
|
-
* (array-of-arrays) shapes, so behavior is identical on either gh version
|
|
2016
|
-
* (patronage/internal#380).
|
|
2017
|
-
*/
|
|
2018
|
-
const fetchIssueComments = (fullName, issueNumber) => normalizeIssueComments(runGhJson([
|
|
2019
|
-
"api",
|
|
2020
|
-
"--paginate",
|
|
2021
|
-
`/repos/${fullName}/issues/${issueNumber}/comments`
|
|
2022
|
-
]));
|
|
3005
|
+
if (previous === void 0) return rawValue;
|
|
3006
|
+
const previousResolved = resolveCwdOption(previous);
|
|
3007
|
+
const currentResolved = resolveCwdOption(rawValue);
|
|
3008
|
+
if (previousResolved !== currentResolved) throw new Error(`--cwd was passed multiple times with conflicting values: "${previous}" (resolves to ${previousResolved}) and "${rawValue}" (resolves to ${currentResolved})`);
|
|
3009
|
+
return rawValue;
|
|
3010
|
+
}
|
|
3011
|
+
function markCwdOptionDefault(command) {
|
|
3012
|
+
const option = command.options.find((candidate) => candidate.attributeName() === "cwd");
|
|
3013
|
+
if (option) {
|
|
3014
|
+
option.defaultValue = ".";
|
|
3015
|
+
option.defaultValueDescription = ".";
|
|
3016
|
+
}
|
|
3017
|
+
return command;
|
|
3018
|
+
}
|
|
3019
|
+
function resolveCheckoutPath(cwd, target) {
|
|
3020
|
+
return path.resolve(cwd, target);
|
|
3021
|
+
}
|
|
3022
|
+
function resolveProofOutputPaths(cwd, defaultPath, requested) {
|
|
3023
|
+
return {
|
|
3024
|
+
additional: requested ? resolveCheckoutPath(cwd, requested) : void 0,
|
|
3025
|
+
canonical: resolveCheckoutPath(cwd, defaultPath)
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
2023
3028
|
//#endregion
|
|
2024
3029
|
//#region src/glob-match.ts
|
|
2025
3030
|
function isValidGlob(pattern) {
|
|
@@ -2378,254 +3383,38 @@ function tryParseProfileContent(content) {
|
|
|
2378
3383
|
} catch {
|
|
2379
3384
|
return;
|
|
2380
3385
|
}
|
|
2381
|
-
}
|
|
2382
|
-
/**
|
|
2383
|
-
* Derive the HQ lane-permalink base (`<origin>/lanes/by-ref`) from an HQ ingest
|
|
2384
|
-
* endpoint. Reuses the same host the non-blocking gate sink already targets, so
|
|
2385
|
-
* branded factory check runs deep-link to HQ without a second config source.
|
|
2386
|
-
* Returns `undefined` for a malformed endpoint (callers keep the PR fallback).
|
|
2387
|
-
*/
|
|
2388
|
-
function hqLaneRefBaseUrl(endpoint) {
|
|
2389
|
-
try {
|
|
2390
|
-
return `${new URL(endpoint).origin}/lanes/by-ref`;
|
|
2391
|
-
} catch {
|
|
2392
|
-
return;
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
/**
|
|
2396
|
-
* The HQ lane-permalink base for a repository profile, or `undefined` when HQ is
|
|
2397
|
-
* not enabled/configured. Gated identically to the HQ gate sink
|
|
2398
|
-
* (`profile.hq.enabled`) so the check-run deep link and the ingest sink stay in
|
|
2399
|
-
* lockstep.
|
|
2400
|
-
*/
|
|
2401
|
-
function hqLaneRefBaseUrlFromProfile(profile) {
|
|
2402
|
-
if (!profile?.hq?.enabled) return;
|
|
2403
|
-
return hqLaneRefBaseUrl(profile.hq.endpoint);
|
|
2404
|
-
}
|
|
2405
|
-
function formatLegacyProfileError(profilePath, foundVersion, migration) {
|
|
2406
|
-
return [`Profile is invalid: ${profilePath}: schemaVersion ${foundVersion} is not supported; migrate to schemaVersion 6 (ADR 0021).`, ...migration.map((line) => ` ${line}`)].join("\n");
|
|
2407
|
-
}
|
|
2408
|
-
function formatProfileError(profilePath, error) {
|
|
2409
|
-
return `Profile is invalid: ${profilePath}: ${error.issues.map((issue) => {
|
|
2410
|
-
return `${issue.path.length > 0 ? issue.path.join(".") : "root"}: ${issue.message}`;
|
|
2411
|
-
}).join("; ")}`;
|
|
2412
|
-
}
|
|
2413
|
-
//#endregion
|
|
2414
|
-
//#region src/review-ladder-policy.ts
|
|
2415
|
-
const REVIEW_SEVERITIES = [
|
|
2416
|
-
"critical",
|
|
2417
|
-
"high",
|
|
2418
|
-
"medium",
|
|
2419
|
-
"low",
|
|
2420
|
-
"unknown"
|
|
2421
|
-
];
|
|
2422
|
-
const REVIEW_CATEGORIES = [
|
|
2423
|
-
"correctness",
|
|
2424
|
-
"safety",
|
|
2425
|
-
"coordination",
|
|
2426
|
-
"maintainability",
|
|
2427
|
-
"unknown"
|
|
2428
|
-
];
|
|
2429
|
-
const resolveReviewFindingSeverity = (priority) => {
|
|
2430
|
-
const normalized = priority?.trim().toLowerCase();
|
|
2431
|
-
if (!normalized) return "unknown";
|
|
2432
|
-
if ([
|
|
2433
|
-
"p0",
|
|
2434
|
-
"critical",
|
|
2435
|
-
"crit",
|
|
2436
|
-
"blocker",
|
|
2437
|
-
"blocking"
|
|
2438
|
-
].includes(normalized)) return "critical";
|
|
2439
|
-
if ([
|
|
2440
|
-
"p1",
|
|
2441
|
-
"high",
|
|
2442
|
-
"severe",
|
|
2443
|
-
"major"
|
|
2444
|
-
].includes(normalized)) return "high";
|
|
2445
|
-
if ([
|
|
2446
|
-
"p2",
|
|
2447
|
-
"medium",
|
|
2448
|
-
"moderate",
|
|
2449
|
-
"normal",
|
|
2450
|
-
"refinement"
|
|
2451
|
-
].includes(normalized)) return "medium";
|
|
2452
|
-
if ([
|
|
2453
|
-
"p3",
|
|
2454
|
-
"p4",
|
|
2455
|
-
"low",
|
|
2456
|
-
"minor",
|
|
2457
|
-
"info",
|
|
2458
|
-
"informational"
|
|
2459
|
-
].includes(normalized)) return "low";
|
|
2460
|
-
return "unknown";
|
|
2461
|
-
};
|
|
2462
|
-
const countFindingsBySeverity = (findings) => {
|
|
2463
|
-
const counts = {
|
|
2464
|
-
critical: 0,
|
|
2465
|
-
high: 0,
|
|
2466
|
-
low: 0,
|
|
2467
|
-
medium: 0,
|
|
2468
|
-
unknown: 0
|
|
2469
|
-
};
|
|
2470
|
-
for (const finding of findings) counts[resolveReviewFindingSeverity(finding.priority)] += 1;
|
|
2471
|
-
return counts;
|
|
2472
|
-
};
|
|
2473
|
-
const resolveReviewFindingCategory = (category) => REVIEW_CATEGORIES.find((value) => value === category?.trim().toLowerCase()) ?? "unknown";
|
|
2474
|
-
const resolveFindingBlocking = (finding, disposition = "open") => {
|
|
2475
|
-
if (disposition !== "open") return false;
|
|
2476
|
-
if (resolveReviewFindingCategory(finding.category) !== "maintainability") return true;
|
|
2477
|
-
if (finding.blockingAfterCap !== void 0) return finding.blockingAfterCap;
|
|
2478
|
-
const severity = resolveReviewFindingSeverity(finding.priority);
|
|
2479
|
-
return severity === "critical" || severity === "high" || severity === "unknown";
|
|
2480
|
-
};
|
|
2481
|
-
const FACTORY_REVIEW_CYCLE_CAP = 5;
|
|
2482
|
-
const resolveReviewLadderPolicy = () => ({ gate: { cap: FACTORY_REVIEW_CYCLE_CAP } });
|
|
2483
|
-
//#endregion
|
|
2484
|
-
//#region src/sha256-hex.ts
|
|
2485
|
-
const ROUND_CONSTANTS = new Uint32Array([
|
|
2486
|
-
1116352408,
|
|
2487
|
-
1899447441,
|
|
2488
|
-
3049323471,
|
|
2489
|
-
3921009573,
|
|
2490
|
-
961987163,
|
|
2491
|
-
1508970993,
|
|
2492
|
-
2453635748,
|
|
2493
|
-
2870763221,
|
|
2494
|
-
3624381080,
|
|
2495
|
-
310598401,
|
|
2496
|
-
607225278,
|
|
2497
|
-
1426881987,
|
|
2498
|
-
1925078388,
|
|
2499
|
-
2162078206,
|
|
2500
|
-
2614888103,
|
|
2501
|
-
3248222580,
|
|
2502
|
-
3835390401,
|
|
2503
|
-
4022224774,
|
|
2504
|
-
264347078,
|
|
2505
|
-
604807628,
|
|
2506
|
-
770255983,
|
|
2507
|
-
1249150122,
|
|
2508
|
-
1555081692,
|
|
2509
|
-
1996064986,
|
|
2510
|
-
2554220882,
|
|
2511
|
-
2821834349,
|
|
2512
|
-
2952996808,
|
|
2513
|
-
3210313671,
|
|
2514
|
-
3336571891,
|
|
2515
|
-
3584528711,
|
|
2516
|
-
113926993,
|
|
2517
|
-
338241895,
|
|
2518
|
-
666307205,
|
|
2519
|
-
773529912,
|
|
2520
|
-
1294757372,
|
|
2521
|
-
1396182291,
|
|
2522
|
-
1695183700,
|
|
2523
|
-
1986661051,
|
|
2524
|
-
2177026350,
|
|
2525
|
-
2456956037,
|
|
2526
|
-
2730485921,
|
|
2527
|
-
2820302411,
|
|
2528
|
-
3259730800,
|
|
2529
|
-
3345764771,
|
|
2530
|
-
3516065817,
|
|
2531
|
-
3600352804,
|
|
2532
|
-
4094571909,
|
|
2533
|
-
275423344,
|
|
2534
|
-
430227734,
|
|
2535
|
-
506948616,
|
|
2536
|
-
659060556,
|
|
2537
|
-
883997877,
|
|
2538
|
-
958139571,
|
|
2539
|
-
1322822218,
|
|
2540
|
-
1537002063,
|
|
2541
|
-
1747873779,
|
|
2542
|
-
1955562222,
|
|
2543
|
-
2024104815,
|
|
2544
|
-
2227730452,
|
|
2545
|
-
2361852424,
|
|
2546
|
-
2428436474,
|
|
2547
|
-
2756734187,
|
|
2548
|
-
3204031479,
|
|
2549
|
-
3329325298
|
|
2550
|
-
]);
|
|
2551
|
-
const INITIAL_STATE = new Uint32Array([
|
|
2552
|
-
1779033703,
|
|
2553
|
-
3144134277,
|
|
2554
|
-
1013904242,
|
|
2555
|
-
2773480762,
|
|
2556
|
-
1359893119,
|
|
2557
|
-
2600822924,
|
|
2558
|
-
528734635,
|
|
2559
|
-
1541459225
|
|
2560
|
-
]);
|
|
2561
|
-
const BLOCK_BYTES = 64;
|
|
2562
|
-
const LENGTH_FIELD_BYTES = 8;
|
|
2563
|
-
const WORD_BITS = 32;
|
|
2564
|
-
const rotateRight = (value, bits) => (value >>> bits | value << WORD_BITS - bits) >>> 0;
|
|
2565
|
-
/** Message bytes plus the FIPS 180-4 padding and 64-bit length suffix. */
|
|
2566
|
-
const padMessage = (message) => {
|
|
2567
|
-
const paddedLength = Math.ceil((message.length + 1 + LENGTH_FIELD_BYTES) / BLOCK_BYTES) * BLOCK_BYTES;
|
|
2568
|
-
const padded = new Uint8Array(paddedLength);
|
|
2569
|
-
padded.set(message);
|
|
2570
|
-
padded[message.length] = 128;
|
|
2571
|
-
const bitLength = BigInt(message.length) * 8n;
|
|
2572
|
-
new DataView(padded.buffer).setBigUint64(paddedLength - LENGTH_FIELD_BYTES, bitLength);
|
|
2573
|
-
return padded;
|
|
2574
|
-
};
|
|
2575
|
-
const ROUNDS = 64;
|
|
2576
|
-
const SCHEDULE_SEED_WORDS = 16;
|
|
2577
|
-
const STATE_WORDS = 8;
|
|
2578
|
-
const wordAt = (words, index) => words[index];
|
|
2579
|
-
/** One 64-byte block folded into the running state, in place. */
|
|
2580
|
-
const compress = (state, view, offset) => {
|
|
2581
|
-
const schedule = new Uint32Array(ROUNDS);
|
|
2582
|
-
for (let index = 0; index < SCHEDULE_SEED_WORDS; index += 1) schedule[index] = view.getUint32(offset + index * 4);
|
|
2583
|
-
for (let index = SCHEDULE_SEED_WORDS; index < ROUNDS; index += 1) {
|
|
2584
|
-
const previous = wordAt(schedule, index - 15);
|
|
2585
|
-
const recent = wordAt(schedule, index - 2);
|
|
2586
|
-
const sigma0 = rotateRight(previous, 7) ^ rotateRight(previous, 18) ^ previous >>> 3;
|
|
2587
|
-
const sigma1 = rotateRight(recent, 17) ^ rotateRight(recent, 19) ^ recent >>> 10;
|
|
2588
|
-
schedule[index] = wordAt(schedule, index - 16) + sigma0 + wordAt(schedule, index - 7) + sigma1 >>> 0;
|
|
2589
|
-
}
|
|
2590
|
-
const working = new Uint32Array(state);
|
|
2591
|
-
for (let index = 0; index < ROUNDS; index += 1) {
|
|
2592
|
-
const a = wordAt(working, 0);
|
|
2593
|
-
const b = wordAt(working, 1);
|
|
2594
|
-
const c = wordAt(working, 2);
|
|
2595
|
-
const d = wordAt(working, 3);
|
|
2596
|
-
const e = wordAt(working, 4);
|
|
2597
|
-
const f = wordAt(working, 5);
|
|
2598
|
-
const g = wordAt(working, 6);
|
|
2599
|
-
const h = wordAt(working, 7);
|
|
2600
|
-
const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
2601
|
-
const choose = e & f ^ ~e & g;
|
|
2602
|
-
const temp1 = h + sum1 + choose + wordAt(ROUND_CONSTANTS, index) + wordAt(schedule, index) >>> 0;
|
|
2603
|
-
const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
|
2604
|
-
const majority = a & b ^ a & c ^ b & c;
|
|
2605
|
-
working[7] = g;
|
|
2606
|
-
working[6] = f;
|
|
2607
|
-
working[5] = e;
|
|
2608
|
-
working[4] = d + temp1 >>> 0;
|
|
2609
|
-
working[3] = c;
|
|
2610
|
-
working[2] = b;
|
|
2611
|
-
working[1] = a;
|
|
2612
|
-
working[0] = temp1 + (sum0 + majority >>> 0) >>> 0;
|
|
2613
|
-
}
|
|
2614
|
-
for (let index = 0; index < STATE_WORDS; index += 1) state[index] = wordAt(state, index) + wordAt(working, index) >>> 0;
|
|
2615
|
-
};
|
|
2616
|
-
const toHex = (state) => {
|
|
2617
|
-
let hex = "";
|
|
2618
|
-
for (const word of state) hex += word.toString(16).padStart(8, "0");
|
|
2619
|
-
return hex;
|
|
2620
|
-
};
|
|
2621
|
-
/** The lowercase hex SHA-256 digest of a UTF-8 string. */
|
|
2622
|
-
const sha256Hex = (input) => {
|
|
2623
|
-
const padded = padMessage(new TextEncoder().encode(input));
|
|
2624
|
-
const view = new DataView(padded.buffer);
|
|
2625
|
-
const state = new Uint32Array(INITIAL_STATE);
|
|
2626
|
-
for (let offset = 0; offset < padded.length; offset += BLOCK_BYTES) compress(state, view, offset);
|
|
2627
|
-
return toHex(state);
|
|
2628
|
-
};
|
|
3386
|
+
}
|
|
3387
|
+
/**
|
|
3388
|
+
* Derive the HQ lane-permalink base (`<origin>/lanes/by-ref`) from an HQ ingest
|
|
3389
|
+
* endpoint. Reuses the same host the non-blocking gate sink already targets, so
|
|
3390
|
+
* branded factory check runs deep-link to HQ without a second config source.
|
|
3391
|
+
* Returns `undefined` for a malformed endpoint (callers keep the PR fallback).
|
|
3392
|
+
*/
|
|
3393
|
+
function hqLaneRefBaseUrl(endpoint) {
|
|
3394
|
+
try {
|
|
3395
|
+
return `${new URL(endpoint).origin}/lanes/by-ref`;
|
|
3396
|
+
} catch {
|
|
3397
|
+
return;
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
/**
|
|
3401
|
+
* The HQ lane-permalink base for a repository profile, or `undefined` when HQ is
|
|
3402
|
+
* not enabled/configured. Gated identically to the HQ gate sink
|
|
3403
|
+
* (`profile.hq.enabled`) so the check-run deep link and the ingest sink stay in
|
|
3404
|
+
* lockstep.
|
|
3405
|
+
*/
|
|
3406
|
+
function hqLaneRefBaseUrlFromProfile(profile) {
|
|
3407
|
+
if (!profile?.hq?.enabled) return;
|
|
3408
|
+
return hqLaneRefBaseUrl(profile.hq.endpoint);
|
|
3409
|
+
}
|
|
3410
|
+
function formatLegacyProfileError(profilePath, foundVersion, migration) {
|
|
3411
|
+
return [`Profile is invalid: ${profilePath}: schemaVersion ${foundVersion} is not supported; migrate to schemaVersion 6 (ADR 0021).`, ...migration.map((line) => ` ${line}`)].join("\n");
|
|
3412
|
+
}
|
|
3413
|
+
function formatProfileError(profilePath, error) {
|
|
3414
|
+
return `Profile is invalid: ${profilePath}: ${error.issues.map((issue) => {
|
|
3415
|
+
return `${issue.path.length > 0 ? issue.path.join(".") : "root"}: ${issue.message}`;
|
|
3416
|
+
}).join("; ")}`;
|
|
3417
|
+
}
|
|
2629
3418
|
//#endregion
|
|
2630
3419
|
//#region src/demand-resolution.ts
|
|
2631
3420
|
/**
|
|
@@ -2835,46 +3624,6 @@ const authoritativePolicyResolution = ({ base, demand }) => ({
|
|
|
2835
3624
|
reviewPolicyChanged: demand.reviewPolicyChanged,
|
|
2836
3625
|
status: "authoritative"
|
|
2837
3626
|
});
|
|
2838
|
-
const candidatePullRequestViewSchema = z.object({
|
|
2839
|
-
baseRefName: z.string().min(1),
|
|
2840
|
-
isCrossRepository: z.unknown()
|
|
2841
|
-
});
|
|
2842
|
-
/**
|
|
2843
|
-
* The pull request GitHub associates with the checkout's current branch.
|
|
2844
|
-
*
|
|
2845
|
-
* `pr:verify` and `pr:review` are handed a git ref, never a PR number, so this
|
|
2846
|
-
* is how they reach the authority at all. A checkout with no pull request open
|
|
2847
|
-
* is normal — verification runs before publication — so a failed lookup is
|
|
2848
|
-
* `unavailable` with the reason, never a throw.
|
|
2849
|
-
*
|
|
2850
|
-
* The lookup names no PR number, so GitHub resolves it from the branch. A
|
|
2851
|
-
* branch with more than one open pull request could therefore resolve a base
|
|
2852
|
-
* ref that is not the one readiness will use. That does not create a downgrade
|
|
2853
|
-
* path: readiness resolves the authority from the PR it was given and refuses
|
|
2854
|
-
* evidence whose recorded policy identity disagrees (`policyIdentityRefusal`).
|
|
2855
|
-
*/
|
|
2856
|
-
const lookupCandidatePullRequest = (cwd) => {
|
|
2857
|
-
try {
|
|
2858
|
-
const parsed = candidatePullRequestViewSchema.safeParse(runGhJsonAt([
|
|
2859
|
-
"pr",
|
|
2860
|
-
"view",
|
|
2861
|
-
"--json",
|
|
2862
|
-
"baseRefName,isCrossRepository"
|
|
2863
|
-
], cwd));
|
|
2864
|
-
return parsed.success ? {
|
|
2865
|
-
pullRequest: parsed.data,
|
|
2866
|
-
status: "found"
|
|
2867
|
-
} : {
|
|
2868
|
-
reason: `GitHub did not report a usable base ref for this branch's pull request (${parsed.error.issues.map((issue) => issue.message).join("; ")}).`,
|
|
2869
|
-
status: "unavailable"
|
|
2870
|
-
};
|
|
2871
|
-
} catch (error) {
|
|
2872
|
-
return {
|
|
2873
|
-
reason: `no pull request could be read for this branch: ${failureText$2(error)}`,
|
|
2874
|
-
status: "unavailable"
|
|
2875
|
-
};
|
|
2876
|
-
}
|
|
2877
|
-
};
|
|
2878
3627
|
/**
|
|
2879
3628
|
* The ONE way a factory stage resolves review demand (#922 Slice B).
|
|
2880
3629
|
*
|
|
@@ -6153,6 +6902,33 @@ const exchangeFactoryAppInstallationToken = async (input) => {
|
|
|
6153
6902
|
//#endregion
|
|
6154
6903
|
//#region src/github-app-access.ts
|
|
6155
6904
|
const NO_APP_SKIP_MESSAGE = "no factory GitHub App configured; skipping durable check-run republish.\n";
|
|
6905
|
+
/**
|
|
6906
|
+
* Fail-closed App credential mint/exchange that never completed. Distinct from
|
|
6907
|
+
* an evaluated candidate refusal: the branded check must stay unpublished
|
|
6908
|
+
* (#1125), while the command still exits nonzero (#949).
|
|
6909
|
+
*/
|
|
6910
|
+
var FactoryAppUnevaluableError = class extends Error {
|
|
6911
|
+
constructor(message) {
|
|
6912
|
+
super(message);
|
|
6913
|
+
this.name = "FactoryAppUnevaluableError";
|
|
6914
|
+
}
|
|
6915
|
+
};
|
|
6916
|
+
const FACTORY_APP_UNEVALUABLE_MARKERS = [
|
|
6917
|
+
"Cursor OIDC mint",
|
|
6918
|
+
"Factory App broker exchange",
|
|
6919
|
+
"unable to mint Factory App token"
|
|
6920
|
+
];
|
|
6921
|
+
/**
|
|
6922
|
+
* True when the error is an App-credential miss — mint timeout, broker
|
|
6923
|
+
* exchange, or PEM mint — not a proof-chain refusal. `pr:ready` uses this to
|
|
6924
|
+
* skip a red `patronage-factory/pr-ready` that would read as evaluated-blocked
|
|
6925
|
+
* (#1125). Message markers cover injected doubles that throw a plain `Error`.
|
|
6926
|
+
*/
|
|
6927
|
+
const isFactoryAppUnevaluableError = (error) => {
|
|
6928
|
+
if (error instanceof FactoryAppUnevaluableError || error instanceof CursorOidcMintError) return true;
|
|
6929
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6930
|
+
return FACTORY_APP_UNEVALUABLE_MARKERS.some((marker) => message.includes(marker));
|
|
6931
|
+
};
|
|
6156
6932
|
const exchangeFromCursorVm = async (repository, dependencies, env, request, timeoutMs) => {
|
|
6157
6933
|
const mint = dependencies.mintCursorOidc ?? mintCursorOidcToken;
|
|
6158
6934
|
const exchange = dependencies.exchangeBroker ?? exchangeFactoryAppInstallationToken;
|
|
@@ -6339,7 +7115,7 @@ const isOutcome = (value) => OUTCOMES.includes(value);
|
|
|
6339
7115
|
* comes from `verifyProofPassed`: a proof with no recorded outcome is
|
|
6340
7116
|
* unfinished (fail closed).
|
|
6341
7117
|
*/
|
|
6342
|
-
const outcomeFor = (value) => isOutcome(value.outcome) ? value.outcome : "aborted";
|
|
7118
|
+
const outcomeFor$1 = (value) => isOutcome(value.outcome) ? value.outcome : "aborted";
|
|
6343
7119
|
const asClassification = (value) => DIFF_CLASSIFICATIONS.includes(value) ? value : "unknown";
|
|
6344
7120
|
/**
|
|
6345
7121
|
* Project a local pr:verify proof onto the durable check-run payload. Accepts
|
|
@@ -6359,7 +7135,7 @@ const prVerifyCheckPayloadFor = (proof) => {
|
|
|
6359
7135
|
kind: PR_VERIFY_CHECK_PAYLOAD_KIND,
|
|
6360
7136
|
mode: value.mode,
|
|
6361
7137
|
...carriesReleases ? { notRequiredCommands: value.notRequiredCommands } : {},
|
|
6362
|
-
outcome: outcomeFor(value),
|
|
7138
|
+
outcome: outcomeFor$1(value),
|
|
6363
7139
|
proofSchemaVersion: typeof value.schemaVersion === "number" ? value.schemaVersion : 0,
|
|
6364
7140
|
schemaVersion: PR_VERIFY_CHECK_PAYLOAD_SCHEMA_VERSION,
|
|
6365
7141
|
...typeof value.patchId === "string" ? { patchId: value.patchId } : {},
|
|
@@ -6835,9 +7611,11 @@ async function confirmPublishedCheckRun({ config, createdId, dependencies, input
|
|
|
6835
7611
|
* Never posts a commit-status fallback: the commit status is a human-readable
|
|
6836
7612
|
* mirror, not a proof surface, so a caller that needs an App-verified check
|
|
6837
7613
|
* run must be told plainly whether it got one. Returns `true` only when the
|
|
6838
|
-
* App-owned check run landed. Throws
|
|
6839
|
-
* (missing mint or Cursor OIDC exchange,
|
|
6840
|
-
* that miss and exit 0
|
|
7614
|
+
* App-owned check run landed. Throws {@link FactoryAppUnevaluableError} when
|
|
7615
|
+
* Factory App access is fail-closed (missing mint or Cursor OIDC exchange,
|
|
7616
|
+
* #949): `pr:ready` must not notice that miss and exit 0, and must not publish
|
|
7617
|
+
* a red check that reads as candidate refusal (#1125). An optional-App skip
|
|
7618
|
+
* still returns `false`.
|
|
6841
7619
|
*
|
|
6842
7620
|
* "Landed" means GitHub serves it, not that the POST was accepted (#520). On
|
|
6843
7621
|
* PR #519 the POST was accepted, `pr:ready` reported ready and armed, and the
|
|
@@ -6867,7 +7645,7 @@ async function ensureFactoryCheckRunPublished(input, dependencies = {}) {
|
|
|
6867
7645
|
const access = await resolveFactoryAppAccess(input, dependencies);
|
|
6868
7646
|
if (access.kind !== "ok") {
|
|
6869
7647
|
onDiagnostic?.(`${checkName}: ${access.message}`);
|
|
6870
|
-
if (access.failClosed) throw new
|
|
7648
|
+
if (access.failClosed) throw new FactoryAppUnevaluableError(`${checkName}: ${access.message.trim()}`);
|
|
6871
7649
|
return false;
|
|
6872
7650
|
}
|
|
6873
7651
|
try {
|
|
@@ -7498,6 +8276,7 @@ const prVerifySchemaVersionSchema = z.literal(4);
|
|
|
7498
8276
|
const prVerifyProofSchema = z.object({
|
|
7499
8277
|
authoringSession: z.string().trim().min(1),
|
|
7500
8278
|
base: z.string().min(1),
|
|
8279
|
+
baseSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
7501
8280
|
baselineFullProofs: z.array(z.object({
|
|
7502
8281
|
base: z.string().min(1),
|
|
7503
8282
|
changedFiles: z.array(z.string()),
|
|
@@ -8115,8 +8894,10 @@ function defaultPrVerifyGit() {
|
|
|
8115
8894
|
changedFiles,
|
|
8116
8895
|
currentHeadSha,
|
|
8117
8896
|
emptyTreeHash,
|
|
8897
|
+
fetchOriginRef,
|
|
8118
8898
|
mergeBaseSha,
|
|
8119
8899
|
recognizeCatchUp: recognizeCatchUpCandidate,
|
|
8900
|
+
resolveCommitSha,
|
|
8120
8901
|
runVerificationCommand(command, cwd, env) {
|
|
8121
8902
|
return verificationOutcomeOf(runInheritedTee("bash", ["-lc", withPinnedNodeOnPath(command, path.dirname(process.execPath))], cwd, env));
|
|
8122
8903
|
},
|
|
@@ -8150,10 +8931,11 @@ function baselineFullProofsForDocsOnly({ previousProof, profilePath, projectKey,
|
|
|
8150
8931
|
seedFilter: matchesProfile
|
|
8151
8932
|
});
|
|
8152
8933
|
}
|
|
8153
|
-
function buildPrVerifyProof({ authoringSession, base, baselineFullProofs, catchUpRecognition, classification, commands, endedAt, executedCommands, files, headSha, impactStamp, mode, mergeBaseSha: proofMergeBaseSha, notRequiredCommands, outcome, patchId, policyResolution, profilePath, projectKey, repository, startedAt }) {
|
|
8934
|
+
function buildPrVerifyProof({ authoringSession, base, baseSha, baselineFullProofs, catchUpRecognition, classification, commands, endedAt, executedCommands, files, headSha, impactStamp, mode, mergeBaseSha: proofMergeBaseSha, notRequiredCommands, outcome, patchId, policyResolution, profilePath, projectKey, repository, startedAt }) {
|
|
8154
8935
|
return {
|
|
8155
8936
|
authoringSession,
|
|
8156
8937
|
base,
|
|
8938
|
+
baseSha,
|
|
8157
8939
|
...baselineFullProofs ? { baselineFullProofs } : {},
|
|
8158
8940
|
...catchUpRecognition ? { catchUpRecognition } : {},
|
|
8159
8941
|
changedFiles: files,
|
|
@@ -8245,18 +9027,30 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
8245
9027
|
cwd,
|
|
8246
9028
|
profilePath: args.profilePath
|
|
8247
9029
|
});
|
|
8248
|
-
const
|
|
9030
|
+
const pullRequestLookup = (dependencies.lookupPullRequestBase ?? lookupLivePullRequestBase)(cwd);
|
|
9031
|
+
const base = resolveCandidateBase({
|
|
9032
|
+
cwd,
|
|
9033
|
+
dependencies: {
|
|
9034
|
+
fetchOriginRef: git.fetchOriginRef ?? fetchOriginRef,
|
|
9035
|
+
liveBranchTipSha: dependencies.liveBranchTipSha ?? liveBranchTipSha,
|
|
9036
|
+
lookupPullRequestBase: () => pullRequestLookup,
|
|
9037
|
+
resolveCommitSha: git.resolveCommitSha ?? resolveCommitSha
|
|
9038
|
+
},
|
|
9039
|
+
requestedBase: args.base
|
|
9040
|
+
});
|
|
9041
|
+
console.log(`pr:verify ${describeCandidateBase(base)}`);
|
|
9042
|
+
const files = git.changedFiles(cwd, base.sha);
|
|
8249
9043
|
const relativeProfile = repositoryRelativeProfilePath({
|
|
8250
9044
|
cwd,
|
|
8251
9045
|
profilePath
|
|
8252
9046
|
});
|
|
8253
9047
|
const { headSha, mergeBaseSha: proofMergeBaseSha, patchId } = resolveCandidateIdentity({
|
|
8254
|
-
base:
|
|
9048
|
+
base: base.sha,
|
|
8255
9049
|
cwd,
|
|
8256
9050
|
git
|
|
8257
9051
|
});
|
|
8258
9052
|
const catchUpRecognitionResult = (git.recognizeCatchUp ?? recognizeCatchUpCandidate)({
|
|
8259
|
-
base:
|
|
9053
|
+
base: base.sha,
|
|
8260
9054
|
cwd
|
|
8261
9055
|
});
|
|
8262
9056
|
const catchUpRecognition = catchUpRecognitionResult.recognized ? catchUpRecognitionResult.recognition : void 0;
|
|
@@ -8279,17 +9073,17 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
8279
9073
|
});
|
|
8280
9074
|
const { policyResolution } = resolveStageReviewDemand({
|
|
8281
9075
|
authority: dependencies.basePolicy ?? defaultBaseReviewPolicyAuthority(),
|
|
8282
|
-
callerBaseRef:
|
|
9076
|
+
callerBaseRef: base.ref,
|
|
8283
9077
|
candidatePolicy: profile.review,
|
|
8284
9078
|
changedFiles: files,
|
|
8285
9079
|
cwd,
|
|
8286
|
-
lookupPullRequest: dependencies.lookupPullRequest ??
|
|
9080
|
+
lookupPullRequest: dependencies.lookupPullRequest ?? (() => policyLookupFromBaseLookup(pullRequestLookup)),
|
|
8287
9081
|
profilePath: relativeProfile,
|
|
8288
9082
|
readCheckoutRepository: git.checkoutRepository ?? checkoutRepository
|
|
8289
9083
|
});
|
|
8290
9084
|
const rootSharedGlobs = [...DEFAULT_ROOT_SHARED_GLOBS, relativeProfile];
|
|
8291
9085
|
const context = createVerificationRunContext({
|
|
8292
|
-
baseRef:
|
|
9086
|
+
baseRef: base.sha,
|
|
8293
9087
|
changedFiles: files,
|
|
8294
9088
|
fullSuiteBaseRef: git.emptyTreeHash(cwd),
|
|
8295
9089
|
rootSharedGlobs
|
|
@@ -8351,7 +9145,8 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
8351
9145
|
};
|
|
8352
9146
|
const proofContext = {
|
|
8353
9147
|
authoringSession,
|
|
8354
|
-
base:
|
|
9148
|
+
base: base.ref,
|
|
9149
|
+
baseSha: base.sha,
|
|
8355
9150
|
policyResolution,
|
|
8356
9151
|
...catchUpRecognition ? { catchUpRecognition } : {},
|
|
8357
9152
|
classification,
|
|
@@ -8464,7 +9259,7 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
8464
9259
|
if (!command.requiredCheck) continue;
|
|
8465
9260
|
if (scopedOutNames.has(command.name)) continue;
|
|
8466
9261
|
runEvidenceEmit({
|
|
8467
|
-
base:
|
|
9262
|
+
base: base.sha,
|
|
8468
9263
|
check: command.requiredCheck,
|
|
8469
9264
|
cwd,
|
|
8470
9265
|
outcome: "pass",
|
|
@@ -8662,246 +9457,24 @@ const blockedReasonIssues = (proof) => {
|
|
|
8662
9457
|
if (named.length !== reasons.length) return blockedReasonIssue(`blockedReasons must name every blocking reason: ${reasons.length} reason(s), ${named.length} named`);
|
|
8663
9458
|
const drifted = named.findIndex((reason, index) => reason.detail !== blockedReasonDetail(reasons[index]));
|
|
8664
9459
|
return drifted === -1 ? [] : blockedReasonIssue(`blockedReasons[${drifted}] does not carry blocking reason ${drifted}; the two projections must tell one story`);
|
|
8665
|
-
};
|
|
8666
|
-
//#endregion
|
|
8667
|
-
//#region src/packages/review-prompt-sections/schema.ts
|
|
8668
|
-
const REVIEW_PROMPT_SECTION_PROVENANCES = [
|
|
8669
|
-
"coding-standards",
|
|
8670
|
-
"prior-review-ledger",
|
|
8671
|
-
"issue-review-focus"
|
|
8672
|
-
];
|
|
8673
|
-
const nonBlankString$2 = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
|
|
8674
|
-
const reviewPromptSectionSchema = z.object({
|
|
8675
|
-
provenance: z.enum(REVIEW_PROMPT_SECTION_PROVENANCES),
|
|
8676
|
-
source: nonBlankString$2,
|
|
8677
|
-
text: nonBlankString$2
|
|
8678
|
-
}).strict();
|
|
8679
|
-
const promptSectionRank = (section) => REVIEW_PROMPT_SECTION_PROVENANCES.indexOf(section.provenance);
|
|
8680
|
-
const hasDuplicateProvenance = (sections) => new Set(sections.map((section) => section.provenance)).size !== sections.length;
|
|
8681
|
-
const isOutOfAssemblyOrder = (sections) => sections.some((section, index) => index > 0 && promptSectionRank(section) < promptSectionRank(sections[index - 1]));
|
|
8682
|
-
const reviewPromptSectionsSchema = z.array(reviewPromptSectionSchema).min(1).refine((sections) => !hasDuplicateProvenance(sections) && !isOutOfAssemblyOrder(sections), { message: "prompt sections must be in assembly order without duplicates" });
|
|
8683
|
-
//#endregion
|
|
8684
|
-
//#region src/review-ladder-ledger.ts
|
|
8685
|
-
const CYCLE_2_SEVERITY_FLOOR_REFERENCE = "cycle-2-severity-floor";
|
|
8686
|
-
const normalizeCitedSpan = (text) => text.split(/\r?\n/u).map((line) => line.trim().replaceAll(/\s+/gu, " ")).filter((line) => line.length > 0).join("\n");
|
|
8687
|
-
const PRESCRIBED_ACTION_STOPWORDS = new Set([
|
|
8688
|
-
"a",
|
|
8689
|
-
"an",
|
|
8690
|
-
"the",
|
|
8691
|
-
"to",
|
|
8692
|
-
"of",
|
|
8693
|
-
"in",
|
|
8694
|
-
"on",
|
|
8695
|
-
"and",
|
|
8696
|
-
"or",
|
|
8697
|
-
"is",
|
|
8698
|
-
"are",
|
|
8699
|
-
"be",
|
|
8700
|
-
"this",
|
|
8701
|
-
"that",
|
|
8702
|
-
"it",
|
|
8703
|
-
"for",
|
|
8704
|
-
"with",
|
|
8705
|
-
"as",
|
|
8706
|
-
"by",
|
|
8707
|
-
"from",
|
|
8708
|
-
"at",
|
|
8709
|
-
"into",
|
|
8710
|
-
"should",
|
|
8711
|
-
"must",
|
|
8712
|
-
"will",
|
|
8713
|
-
"would",
|
|
8714
|
-
"can",
|
|
8715
|
-
"could",
|
|
8716
|
-
"not",
|
|
8717
|
-
"but",
|
|
8718
|
-
"so",
|
|
8719
|
-
"if",
|
|
8720
|
-
"then",
|
|
8721
|
-
"than",
|
|
8722
|
-
"when",
|
|
8723
|
-
"which",
|
|
8724
|
-
"who",
|
|
8725
|
-
"whom",
|
|
8726
|
-
"its",
|
|
8727
|
-
"their",
|
|
8728
|
-
"there",
|
|
8729
|
-
"here",
|
|
8730
|
-
"also",
|
|
8731
|
-
"use",
|
|
8732
|
-
"using",
|
|
8733
|
-
"instead",
|
|
8734
|
-
"rather",
|
|
8735
|
-
"than",
|
|
8736
|
-
"over",
|
|
8737
|
-
"up",
|
|
8738
|
-
"out",
|
|
8739
|
-
"have",
|
|
8740
|
-
"has",
|
|
8741
|
-
"had",
|
|
8742
|
-
"been"
|
|
8743
|
-
]);
|
|
8744
|
-
const tokenizePrescribedAction = (text) => {
|
|
8745
|
-
const words = text.toLowerCase().replaceAll(/[^a-z0-9\s]/gu, " ").split(/\s+/u).filter((word) => word.length > 0 && !PRESCRIBED_ACTION_STOPWORDS.has(word));
|
|
8746
|
-
return [...new Set(words)].toSorted();
|
|
8747
|
-
};
|
|
8748
|
-
const normalizeTitle = (title) => title.trim().toLowerCase().replaceAll(/\s+/gu, " ");
|
|
8749
|
-
const normalizeFile = (file) => file.trim();
|
|
8750
|
-
const findingKey = (finding) => {
|
|
8751
|
-
const path = finding.file ?? "";
|
|
8752
|
-
const normalizedSpan = normalizeCitedSpan(finding.citedSpan ?? "");
|
|
8753
|
-
const actionTokens = tokenizePrescribedAction(finding.prescribedAction ?? "");
|
|
8754
|
-
const legacyDiscriminator = normalizedSpan.length > 0 || actionTokens.length > 0 ? "" : normalizeTitle(finding.title ?? "");
|
|
8755
|
-
return sha256Hex(`${path}\0${sha256Hex(normalizedSpan)}\0${actionTokens.join(" ")}\0${legacyDiscriminator}`);
|
|
8756
|
-
};
|
|
8757
|
-
const declarationMatchesEntry = (entry, target) => normalizeTitle(entry.title) === normalizeTitle(target.title) && (target.file === void 0 || entry.file !== void 0 && normalizeFile(entry.file) === normalizeFile(target.file));
|
|
8758
|
-
const applyDisposition = (ledger, declaration) => {
|
|
8759
|
-
const entry = declaration.findingReport ? resolveEntryForFinding([...ledger.values()], declaration.findingReport) : [...ledger.values()].find((candidate) => declarationMatchesEntry(candidate, declaration.finding));
|
|
8760
|
-
if (!entry) return false;
|
|
8761
|
-
entry.disposition = declaration.disposition;
|
|
8762
|
-
entry.priorDisposition = void 0;
|
|
8763
|
-
if (declaration.reference !== void 0) entry.reference = declaration.reference;
|
|
8764
|
-
return true;
|
|
8765
|
-
};
|
|
8766
|
-
const definedAttributes = (finding) => Object.fromEntries([
|
|
8767
|
-
"blockingAfterCap",
|
|
8768
|
-
"category",
|
|
8769
|
-
"citedSpan",
|
|
8770
|
-
"file",
|
|
8771
|
-
"prescribedAction",
|
|
8772
|
-
"priority"
|
|
8773
|
-
].flatMap((attribute) => finding[attribute] === void 0 ? [] : [[attribute, finding[attribute]]]));
|
|
8774
|
-
const toLadderFindingReport = (finding) => ({
|
|
8775
|
-
...definedAttributes(finding),
|
|
8776
|
-
...finding.supersedes ? { supersedes: finding.supersedes } : {},
|
|
8777
|
-
title: finding.title
|
|
8778
|
-
});
|
|
8779
|
-
const MIN_MEANINGFUL_OVERLAP_LINE_LENGTH = 20;
|
|
8780
|
-
const spansOverlap = (a, b) => {
|
|
8781
|
-
if (a.length === 0 || b.length === 0) return false;
|
|
8782
|
-
if (a === b || a.includes(b) || b.includes(a)) return true;
|
|
8783
|
-
const linesA = new Set(a.split("\n").filter((line) => line.length >= MIN_MEANINGFUL_OVERLAP_LINE_LENGTH));
|
|
8784
|
-
return b.split("\n").some((line) => line.length >= MIN_MEANINGFUL_OVERLAP_LINE_LENGTH && linesA.has(line));
|
|
8785
|
-
};
|
|
8786
|
-
const resolveEntryForFinding = (entries, finding) => {
|
|
8787
|
-
const primaryKey = findingKey(finding);
|
|
8788
|
-
const exact = entries.find((entry) => entry.key === primaryKey);
|
|
8789
|
-
if (exact) return exact;
|
|
8790
|
-
const { supersedes } = finding;
|
|
8791
|
-
if (supersedes) {
|
|
8792
|
-
const superseded = entries.find((entry) => declarationMatchesEntry(entry, supersedes));
|
|
8793
|
-
if (superseded) return superseded;
|
|
8794
|
-
}
|
|
8795
|
-
if (finding.file !== void 0 && finding.citedSpan !== void 0) {
|
|
8796
|
-
const candidateSpan = normalizeCitedSpan(finding.citedSpan);
|
|
8797
|
-
if (candidateSpan.length > 0) {
|
|
8798
|
-
const overlapping = entries.find((entry) => entry.disposition === "open" && entry.file === finding.file && entry.citedSpan !== void 0 && spansOverlap(normalizeCitedSpan(entry.citedSpan), candidateSpan));
|
|
8799
|
-
if (overlapping) return overlapping;
|
|
8800
|
-
}
|
|
8801
|
-
}
|
|
8802
|
-
};
|
|
8803
|
-
const REFLAG_TRANSITIONS = {
|
|
8804
|
-
"fixed-in-thread": (entry) => {
|
|
8805
|
-
entry.disposition = "open";
|
|
8806
|
-
entry.reopenedCount += 1;
|
|
8807
|
-
},
|
|
8808
|
-
"follow-up-filed": (entry) => {
|
|
8809
|
-
entry.priorDisposition = entry.disposition;
|
|
8810
|
-
entry.disposition = "stale-repeat";
|
|
8811
|
-
entry.staleRepeatCount += 1;
|
|
8812
|
-
},
|
|
8813
|
-
open: () => {},
|
|
8814
|
-
"rerun-noise": (entry) => {
|
|
8815
|
-
entry.staleRepeatCount += 1;
|
|
8816
|
-
},
|
|
8817
|
-
"stale-repeat": (entry) => {
|
|
8818
|
-
entry.staleRepeatCount += 1;
|
|
8819
|
-
},
|
|
8820
|
-
waived: (entry) => {
|
|
8821
|
-
entry.priorDisposition = entry.disposition;
|
|
8822
|
-
entry.disposition = "stale-repeat";
|
|
8823
|
-
entry.staleRepeatCount += 1;
|
|
8824
|
-
}
|
|
8825
|
-
};
|
|
8826
|
-
const applyFlag = (ledger, finding, ref, demoteNew) => {
|
|
8827
|
-
const matched = resolveEntryForFinding([...ledger.values()], finding);
|
|
8828
|
-
if (!matched) {
|
|
8829
|
-
const key = findingKey(finding);
|
|
8830
|
-
ledger.set(key, {
|
|
8831
|
-
...definedAttributes(finding),
|
|
8832
|
-
disposition: demoteNew ? "rerun-noise" : "open",
|
|
8833
|
-
firstFlagged: ref,
|
|
8834
|
-
flagCount: 1,
|
|
8835
|
-
key,
|
|
8836
|
-
lastFlagged: ref,
|
|
8837
|
-
reopenedCount: 0,
|
|
8838
|
-
staleRepeatCount: 0,
|
|
8839
|
-
title: finding.title.trim()
|
|
8840
|
-
});
|
|
8841
|
-
return;
|
|
8842
|
-
}
|
|
8843
|
-
matched.flagCount += 1;
|
|
8844
|
-
matched.lastFlagged = ref;
|
|
8845
|
-
Object.assign(matched, definedAttributes(finding));
|
|
8846
|
-
REFLAG_TRANSITIONS[matched.disposition](matched);
|
|
8847
|
-
};
|
|
8848
|
-
const findingKeySet = (findings) => new Set(findings.map((finding) => findingKey(finding)));
|
|
8849
|
-
const isSameHeadRerun = (previous, current) => previous?.identity !== void 0 && current.identity !== void 0 && evidenceFresh({
|
|
8850
|
-
candidate: current.identity,
|
|
8851
|
-
recorded: previous.identity,
|
|
8852
|
-
rule: "patch-id"
|
|
8853
|
-
});
|
|
8854
|
-
const isDisjointSameHeadRerun = (previous, current) => {
|
|
8855
|
-
if (current.correlatedRerun) return false;
|
|
8856
|
-
if (!isSameHeadRerun(previous, current) || previous?.findings.length === 0 || current.findings.length === 0) return false;
|
|
8857
|
-
const previousKeys = findingKeySet(previous?.findings ?? []);
|
|
8858
|
-
return current.findings.every((finding) => !previousKeys.has(findingKey(finding)));
|
|
8859
|
-
};
|
|
8860
|
-
const foldLedger = (cycles, refs) => {
|
|
8861
|
-
const ledger = /* @__PURE__ */ new Map();
|
|
8862
|
-
const unmatchedDispositions = [];
|
|
8863
|
-
for (const [index, entry] of cycles.entries()) {
|
|
8864
|
-
const dispositions = entry.dispositions ?? [];
|
|
8865
|
-
const severityFloorDispositions = dispositions.filter((declaration) => declaration.findingReport !== void 0);
|
|
8866
|
-
for (const declaration of dispositions.filter((candidate) => candidate.findingReport === void 0)) if (!applyDisposition(ledger, declaration)) unmatchedDispositions.push(declaration);
|
|
8867
|
-
const demoteNew = isDisjointSameHeadRerun(cycles[index - 1], entry);
|
|
8868
|
-
for (const finding of entry.findings) applyFlag(ledger, finding, refs[index], demoteNew);
|
|
8869
|
-
for (const declaration of severityFloorDispositions) if (!applyDisposition(ledger, declaration)) unmatchedDispositions.push(declaration);
|
|
8870
|
-
}
|
|
8871
|
-
return {
|
|
8872
|
-
ledger,
|
|
8873
|
-
unmatchedDispositions
|
|
8874
|
-
};
|
|
8875
|
-
};
|
|
8876
|
-
const openLadderFindings = (ledger) => ledger.filter((entry) => entry.disposition === "open");
|
|
8877
|
-
const blockingLadderFindings = (ledger) => openLadderFindings(ledger).filter((entry) => resolveFindingBlocking(entry, entry.disposition));
|
|
8878
|
-
const staleRepeatLadderFindings = (ledger) => ledger.filter((entry) => entry.disposition === "stale-repeat");
|
|
8879
|
-
const inferFixedInThreadDispositions = (ledger, findings, options = {}) => {
|
|
8880
|
-
if (options.sameHeadAsPriorCycle) return [];
|
|
8881
|
-
const flaggedKeys = new Set(findings.map((finding) => {
|
|
8882
|
-
const match = resolveEntryForFinding(ledger, finding);
|
|
8883
|
-
return match ? match.key : findingKey(finding);
|
|
8884
|
-
}));
|
|
8885
|
-
return openLadderFindings(ledger).filter((entry) => !flaggedKeys.has(entry.key)).map((entry) => ({
|
|
8886
|
-
disposition: "fixed-in-thread",
|
|
8887
|
-
finding: {
|
|
8888
|
-
title: entry.title,
|
|
8889
|
-
...entry.file === void 0 ? {} : { file: entry.file }
|
|
8890
|
-
}
|
|
8891
|
-
}));
|
|
8892
|
-
};
|
|
8893
|
-
const ladderDispositionDeclarationsSchema = z.array(z.object({
|
|
8894
|
-
disposition: z.enum([
|
|
8895
|
-
"fixed-in-thread",
|
|
8896
|
-
"follow-up-filed",
|
|
8897
|
-
"waived"
|
|
8898
|
-
]),
|
|
8899
|
-
finding: z.object({
|
|
8900
|
-
file: z.string().min(1).optional(),
|
|
8901
|
-
title: z.string().min(1)
|
|
8902
|
-
}).strict(),
|
|
8903
|
-
reference: z.string().min(1).optional()
|
|
8904
|
-
}).strict());
|
|
9460
|
+
};
|
|
9461
|
+
//#endregion
|
|
9462
|
+
//#region src/packages/review-prompt-sections/schema.ts
|
|
9463
|
+
const REVIEW_PROMPT_SECTION_PROVENANCES = [
|
|
9464
|
+
"coding-standards",
|
|
9465
|
+
"prior-review-ledger",
|
|
9466
|
+
"issue-review-focus"
|
|
9467
|
+
];
|
|
9468
|
+
const nonBlankString$2 = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
|
|
9469
|
+
const reviewPromptSectionSchema = z.object({
|
|
9470
|
+
provenance: z.enum(REVIEW_PROMPT_SECTION_PROVENANCES),
|
|
9471
|
+
source: nonBlankString$2,
|
|
9472
|
+
text: nonBlankString$2
|
|
9473
|
+
}).strict();
|
|
9474
|
+
const promptSectionRank = (section) => REVIEW_PROMPT_SECTION_PROVENANCES.indexOf(section.provenance);
|
|
9475
|
+
const hasDuplicateProvenance = (sections) => new Set(sections.map((section) => section.provenance)).size !== sections.length;
|
|
9476
|
+
const isOutOfAssemblyOrder = (sections) => sections.some((section, index) => index > 0 && promptSectionRank(section) < promptSectionRank(sections[index - 1]));
|
|
9477
|
+
const reviewPromptSectionsSchema = z.array(reviewPromptSectionSchema).min(1).refine((sections) => !hasDuplicateProvenance(sections) && !isOutOfAssemblyOrder(sections), { message: "prompt sections must be in assembly order without duplicates" });
|
|
8905
9478
|
//#endregion
|
|
8906
9479
|
//#region src/review-ladder.ts
|
|
8907
9480
|
const LADDER_NEXT_ACTION_KINDS = [
|
|
@@ -8963,7 +9536,7 @@ const resolveLadderPosition = ({ blockingFindings, gateCount, openFindings, poli
|
|
|
8963
9536
|
};
|
|
8964
9537
|
return {
|
|
8965
9538
|
forcedTransition: {
|
|
8966
|
-
blockingFindings: blockingFindings.map((entry) => entry.
|
|
9539
|
+
blockingFindings: blockingFindings.map((entry) => entry.address),
|
|
8967
9540
|
cap,
|
|
8968
9541
|
kind: "gate-cap-exhausted"
|
|
8969
9542
|
},
|
|
@@ -9001,7 +9574,7 @@ const eventFor = ({ cycles, openFindings, blockingFindings, position, staleRepea
|
|
|
9001
9574
|
};
|
|
9002
9575
|
const evaluateReviewLadder = ({ cycles, policy }) => {
|
|
9003
9576
|
const refs = ladderCycleRefsFor(cycles);
|
|
9004
|
-
const { ledger,
|
|
9577
|
+
const { ledger, occurrences } = foldLedger(cycles, refs);
|
|
9005
9578
|
const ledgerEntries = [...ledger.values()];
|
|
9006
9579
|
const openFindings = openLadderFindings(ledgerEntries);
|
|
9007
9580
|
const blockingFindings = blockingLadderFindings(ledgerEntries);
|
|
@@ -9029,18 +9602,349 @@ const evaluateReviewLadder = ({ cycles, policy }) => {
|
|
|
9029
9602
|
staleRepeats
|
|
9030
9603
|
}),
|
|
9031
9604
|
ledger: ledgerEntries,
|
|
9032
|
-
|
|
9605
|
+
occurrences
|
|
9606
|
+
};
|
|
9607
|
+
};
|
|
9608
|
+
//#endregion
|
|
9609
|
+
//#region src/review-policy-declarations.ts
|
|
9610
|
+
const identityRepeatDeclarations = (occurrences, cycleIndex) => occurrences.flatMap((occurrence) => {
|
|
9611
|
+
if (cycleOfAddress(occurrence.address) !== cycleIndex || occurrence.root === occurrence.address) return [];
|
|
9612
|
+
const disposition = IDENTITY_REPEAT_DISPOSITION[projectedGroupDisposition(occurrences.filter((member) => member.root === occurrence.root && cycleOfAddress(member.address) < cycleIndex))];
|
|
9613
|
+
return disposition === void 0 ? [] : [{
|
|
9614
|
+
authority: "policy",
|
|
9615
|
+
disposition,
|
|
9616
|
+
rule: "identity-repeat",
|
|
9617
|
+
target: occurrence.address
|
|
9618
|
+
}];
|
|
9619
|
+
});
|
|
9620
|
+
const severityFloorDeclarations = (occurrences, liveCycleCount) => {
|
|
9621
|
+
if (liveCycleCount < 2) return [];
|
|
9622
|
+
return occurrences.flatMap((occurrence) => {
|
|
9623
|
+
const severity = resolveReviewFindingSeverity(occurrence.finding.priority);
|
|
9624
|
+
return occurrence.disposition === "open" && (severity === "medium" || severity === "low") ? [{
|
|
9625
|
+
authority: "policy",
|
|
9626
|
+
disposition: "policy-accepted",
|
|
9627
|
+
rule: "cycle-2-severity-floor",
|
|
9628
|
+
target: occurrence.address
|
|
9629
|
+
}] : [];
|
|
9630
|
+
});
|
|
9631
|
+
};
|
|
9632
|
+
//#endregion
|
|
9633
|
+
//#region src/review-proof-import.ts
|
|
9634
|
+
const REVIEW_PROOF_IMPORT_REFERENCE = "schema-2 import: declaration recorded without a reference";
|
|
9635
|
+
const CYCLE_2_SEVERITY_FLOOR_REFERENCE = "cycle-2-severity-floor";
|
|
9636
|
+
const legacyFindingSchema = z.object({
|
|
9637
|
+
blockingAfterCap: z.boolean().optional(),
|
|
9638
|
+
category: z.string().optional(),
|
|
9639
|
+
citedSpan: z.string().optional(),
|
|
9640
|
+
file: z.string().optional(),
|
|
9641
|
+
prescribedAction: z.string().optional(),
|
|
9642
|
+
priority: z.string().optional(),
|
|
9643
|
+
supersedes: z.object({
|
|
9644
|
+
file: z.string().optional(),
|
|
9645
|
+
title: z.string()
|
|
9646
|
+
}).passthrough().optional(),
|
|
9647
|
+
title: z.string()
|
|
9648
|
+
}).passthrough();
|
|
9649
|
+
const legacyDeclarationSchema = z.object({
|
|
9650
|
+
disposition: z.enum([
|
|
9651
|
+
"fixed-in-thread",
|
|
9652
|
+
"follow-up-filed",
|
|
9653
|
+
"waived"
|
|
9654
|
+
]),
|
|
9655
|
+
finding: z.object({
|
|
9656
|
+
file: z.string().optional(),
|
|
9657
|
+
title: z.string()
|
|
9658
|
+
}).passthrough(),
|
|
9659
|
+
findingReport: legacyFindingSchema.optional(),
|
|
9660
|
+
reference: z.string().optional()
|
|
9661
|
+
}).passthrough();
|
|
9662
|
+
const legacyCycleSchema = z.object({
|
|
9663
|
+
correlatedRerun: z.boolean().optional(),
|
|
9664
|
+
dispositions: z.array(legacyDeclarationSchema).optional(),
|
|
9665
|
+
engine: z.string().optional(),
|
|
9666
|
+
findings: z.array(legacyFindingSchema),
|
|
9667
|
+
historical: z.literal(true).optional(),
|
|
9668
|
+
identity: z.object({
|
|
9669
|
+
headSha: z.string(),
|
|
9670
|
+
patchId: z.string()
|
|
9671
|
+
}).passthrough().optional(),
|
|
9672
|
+
model: z.string().optional(),
|
|
9673
|
+
session: z.record(z.string(), z.unknown()).optional(),
|
|
9674
|
+
stage: z.enum(["interior", "gate"]),
|
|
9675
|
+
usage: z.record(z.string(), z.unknown()).optional()
|
|
9676
|
+
}).passthrough();
|
|
9677
|
+
const legacyLadderSchema = z.object({ cycles: z.array(legacyCycleSchema) }).passthrough();
|
|
9678
|
+
const MIN_MEANINGFUL_OVERLAP_LINE_LENGTH = 20;
|
|
9679
|
+
const spansOverlap = (a, b) => {
|
|
9680
|
+
if (a.length === 0 || b.length === 0) return false;
|
|
9681
|
+
if (a === b || a.includes(b) || b.includes(a)) return true;
|
|
9682
|
+
const linesA = new Set(a.split("\n").filter((line) => line.length >= MIN_MEANINGFUL_OVERLAP_LINE_LENGTH));
|
|
9683
|
+
return b.split("\n").some((line) => line.length >= MIN_MEANINGFUL_OVERLAP_LINE_LENGTH && linesA.has(line));
|
|
9684
|
+
};
|
|
9685
|
+
const resolveLegacyEntry = (entries, finding) => {
|
|
9686
|
+
const primaryKey = findingKey(finding);
|
|
9687
|
+
const exact = entries.find((entry) => entry.key === primaryKey);
|
|
9688
|
+
if (exact) return exact;
|
|
9689
|
+
const { supersedes } = finding;
|
|
9690
|
+
if (supersedes) {
|
|
9691
|
+
const superseded = entries.find((entry) => declarationMatchesEntry(entry, supersedes));
|
|
9692
|
+
if (superseded) return superseded;
|
|
9693
|
+
}
|
|
9694
|
+
if (finding.file !== void 0 && finding.citedSpan !== void 0) {
|
|
9695
|
+
const candidateSpan = normalizeCitedSpan(finding.citedSpan);
|
|
9696
|
+
if (candidateSpan.length > 0) return entries.find((entry) => entry.disposition === "open" && entry.file === finding.file && entry.citedSpan !== void 0 && spansOverlap(normalizeCitedSpan(entry.citedSpan), candidateSpan));
|
|
9697
|
+
}
|
|
9698
|
+
};
|
|
9699
|
+
const LEGACY_REFLAG = {
|
|
9700
|
+
"fixed-in-thread": "open",
|
|
9701
|
+
"follow-up-filed": "stale-repeat",
|
|
9702
|
+
open: "open",
|
|
9703
|
+
"rerun-noise": "rerun-noise",
|
|
9704
|
+
"stale-repeat": "stale-repeat",
|
|
9705
|
+
waived: "stale-repeat"
|
|
9706
|
+
};
|
|
9707
|
+
const isDisjointSameHeadRerun = (previous, current) => {
|
|
9708
|
+
if (current.correlatedRerun || previous?.identity === void 0 || current.identity === void 0 || previous.identity.patchId !== current.identity.patchId || previous.findings.length === 0 || current.findings.length === 0) return false;
|
|
9709
|
+
const previousKeys = new Set(previous.findings.map((finding) => findingKey(finding)));
|
|
9710
|
+
return current.findings.every((finding) => !previousKeys.has(findingKey(finding)));
|
|
9711
|
+
};
|
|
9712
|
+
const applyLegacyDeclaration = (entries, declaration) => {
|
|
9713
|
+
const entry = declaration.findingReport ? resolveLegacyEntry(entries, declaration.findingReport) : entries.find((candidate) => declarationMatchesEntry(candidate, declaration.finding));
|
|
9714
|
+
if (entry) entry.disposition = declaration.disposition;
|
|
9715
|
+
};
|
|
9716
|
+
const isFloorDeclaration = (declaration) => declaration.findingReport !== void 0 && declaration.reference === CYCLE_2_SEVERITY_FLOOR_REFERENCE;
|
|
9717
|
+
const outcomeFor = (entries, declaration) => {
|
|
9718
|
+
const matches = entries.filter((entry) => declarationMatchesEntry(entry, declaration.finding));
|
|
9719
|
+
if (matches.length !== 1) return {
|
|
9720
|
+
candidates: matches.flatMap((entry) => entry.members),
|
|
9721
|
+
kind: "refused",
|
|
9722
|
+
reason: matches.length === 0 ? "title+file matches no entry" : `title+file matches ${matches.length} entries`
|
|
9723
|
+
};
|
|
9724
|
+
const [entry] = matches;
|
|
9725
|
+
if (!entry) throw new Error("unreachable: one match");
|
|
9726
|
+
if (entry.keys.size !== 1) return {
|
|
9727
|
+
candidates: [...entry.members],
|
|
9728
|
+
kind: "refused",
|
|
9729
|
+
reason: `the matched entry absorbed ${entry.keys.size} distinct finding keys (${entry.members.join(", ")}); the schema-2 outcome was similarity-grounded`
|
|
9730
|
+
};
|
|
9731
|
+
return {
|
|
9732
|
+
kind: "converted",
|
|
9733
|
+
targets: [...entry.members]
|
|
9734
|
+
};
|
|
9735
|
+
};
|
|
9736
|
+
const replayLegacyFold = (cycles) => {
|
|
9737
|
+
const entries = [];
|
|
9738
|
+
return cycles.map((cycle, cycleIndex) => {
|
|
9739
|
+
const decided = [];
|
|
9740
|
+
const stored = cycle.dispositions ?? [];
|
|
9741
|
+
const beforeFlags = stored.filter((declaration) => declaration.findingReport === void 0);
|
|
9742
|
+
const afterFlags = stored.filter((declaration) => declaration.findingReport !== void 0);
|
|
9743
|
+
for (const declaration of beforeFlags) {
|
|
9744
|
+
decided.push({
|
|
9745
|
+
declaration,
|
|
9746
|
+
outcome: outcomeFor(entries, declaration)
|
|
9747
|
+
});
|
|
9748
|
+
applyLegacyDeclaration(entries, declaration);
|
|
9749
|
+
}
|
|
9750
|
+
const demoteNew = isDisjointSameHeadRerun(cycles[cycleIndex - 1], cycle);
|
|
9751
|
+
for (const [findingIndex, raw] of cycle.findings.entries()) {
|
|
9752
|
+
const finding = toLadderFindingReport(raw);
|
|
9753
|
+
const address = occurrenceAddress(cycleIndex + 1, findingIndex + 1);
|
|
9754
|
+
const key = findingKey(finding);
|
|
9755
|
+
const matched = resolveLegacyEntry(entries, finding);
|
|
9756
|
+
if (matched) {
|
|
9757
|
+
matched.members.push(address);
|
|
9758
|
+
matched.keys.add(key);
|
|
9759
|
+
if (finding.citedSpan !== void 0) matched.citedSpan = finding.citedSpan;
|
|
9760
|
+
if (finding.file !== void 0) matched.file = finding.file;
|
|
9761
|
+
matched.disposition = LEGACY_REFLAG[matched.disposition];
|
|
9762
|
+
continue;
|
|
9763
|
+
}
|
|
9764
|
+
entries.push({
|
|
9765
|
+
...finding.citedSpan === void 0 ? {} : { citedSpan: finding.citedSpan },
|
|
9766
|
+
disposition: demoteNew ? "rerun-noise" : "open",
|
|
9767
|
+
...finding.file === void 0 ? {} : { file: finding.file },
|
|
9768
|
+
key,
|
|
9769
|
+
keys: new Set([key]),
|
|
9770
|
+
members: [address],
|
|
9771
|
+
title: finding.title.trim()
|
|
9772
|
+
});
|
|
9773
|
+
}
|
|
9774
|
+
for (const declaration of afterFlags) {
|
|
9775
|
+
if (!isFloorDeclaration(declaration)) decided.push({
|
|
9776
|
+
declaration,
|
|
9777
|
+
outcome: outcomeFor(entries, declaration)
|
|
9778
|
+
});
|
|
9779
|
+
applyLegacyDeclaration(entries, declaration);
|
|
9780
|
+
}
|
|
9781
|
+
return { declarations: decided };
|
|
9782
|
+
});
|
|
9783
|
+
};
|
|
9784
|
+
const convertedDeclaration = (declaration, target) => ({
|
|
9785
|
+
authority: "operator",
|
|
9786
|
+
disposition: declaration.disposition,
|
|
9787
|
+
reference: declaration.reference ?? REVIEW_PROOF_IMPORT_REFERENCE,
|
|
9788
|
+
target
|
|
9789
|
+
});
|
|
9790
|
+
const isUnattestedFix = (declaration) => declaration.disposition === "fixed-in-thread" && declaration.reference === void 0 && declaration.findingReport === void 0;
|
|
9791
|
+
const INFERRED_FIX_REASON = "a fixed-in-thread the schema-2 writer inferred from absence on a blocking-severity occurrence is not a verified fix";
|
|
9792
|
+
const unattestedFixOutcome = (target, finding) => {
|
|
9793
|
+
const severity = resolveReviewFindingSeverity(finding?.priority);
|
|
9794
|
+
return severity === "medium" || severity === "low" ? {
|
|
9795
|
+
authority: "policy",
|
|
9796
|
+
disposition: "policy-accepted",
|
|
9797
|
+
rule: "schema-2-import",
|
|
9798
|
+
target
|
|
9799
|
+
} : void 0;
|
|
9800
|
+
};
|
|
9801
|
+
const recoveryItem = (declaration, cycle, outcome) => ({
|
|
9802
|
+
candidates: outcome.candidates,
|
|
9803
|
+
cycle,
|
|
9804
|
+
declaration: {
|
|
9805
|
+
disposition: declaration.disposition,
|
|
9806
|
+
...declaration.finding.file === void 0 ? {} : { file: declaration.finding.file },
|
|
9807
|
+
...declaration.reference === void 0 ? {} : { reference: declaration.reference },
|
|
9808
|
+
title: declaration.finding.title
|
|
9809
|
+
},
|
|
9810
|
+
reason: outcome.reason
|
|
9811
|
+
});
|
|
9812
|
+
const convertCycles = (legacy, proofDigest) => {
|
|
9813
|
+
const replay = replayLegacyFold(legacy);
|
|
9814
|
+
const cycles = [];
|
|
9815
|
+
const recovery = [];
|
|
9816
|
+
let liveCycleCount = 0;
|
|
9817
|
+
for (const [index, cycle] of legacy.entries()) {
|
|
9818
|
+
const cycleIndex = index + 1;
|
|
9819
|
+
if (!(cycle.historical === true)) liveCycleCount += 1;
|
|
9820
|
+
const findings = cycle.findings.map((finding) => toLadderFindingReport(finding));
|
|
9821
|
+
const occurrences = occurrencesForCycle({
|
|
9822
|
+
cycleIndex,
|
|
9823
|
+
declaredCycle: ladderCycleRefsFor([...cycles, { stage: cycle.stage }]).at(-1)?.cycle ?? cycleIndex,
|
|
9824
|
+
findings,
|
|
9825
|
+
priorCycles: cycles
|
|
9826
|
+
});
|
|
9827
|
+
const findingAt = (address) => {
|
|
9828
|
+
const groups = /^c(?<cycle>\d+)f(?<finding>\d+)$/u.exec(address)?.groups;
|
|
9829
|
+
const at = Number(groups?.cycle) - 1;
|
|
9830
|
+
return (at === index ? findings : cycles[at]?.findings)?.[Number(groups?.finding) - 1];
|
|
9831
|
+
};
|
|
9832
|
+
const explicit = [];
|
|
9833
|
+
for (const { declaration, outcome } of replay[index]?.declarations ?? []) {
|
|
9834
|
+
if (outcome.kind === "refused") {
|
|
9835
|
+
recovery.push(recoveryItem(declaration, cycleIndex, outcome));
|
|
9836
|
+
continue;
|
|
9837
|
+
}
|
|
9838
|
+
for (const target of outcome.targets) {
|
|
9839
|
+
if (!isUnattestedFix(declaration)) {
|
|
9840
|
+
explicit.push(convertedDeclaration(declaration, target));
|
|
9841
|
+
continue;
|
|
9842
|
+
}
|
|
9843
|
+
const accepted = unattestedFixOutcome(target, findingAt(target));
|
|
9844
|
+
if (accepted) explicit.push(accepted);
|
|
9845
|
+
else recovery.push(recoveryItem(declaration, cycleIndex, {
|
|
9846
|
+
candidates: [target],
|
|
9847
|
+
kind: "refused",
|
|
9848
|
+
reason: INFERRED_FIX_REASON
|
|
9849
|
+
}));
|
|
9850
|
+
}
|
|
9851
|
+
}
|
|
9852
|
+
const cycleWith = (declarations) => {
|
|
9853
|
+
const recording = {
|
|
9854
|
+
declarations,
|
|
9855
|
+
imported: {
|
|
9856
|
+
proofDigest,
|
|
9857
|
+
schemaVersion: 2
|
|
9858
|
+
},
|
|
9859
|
+
occurrences
|
|
9860
|
+
};
|
|
9861
|
+
const { correlatedRerun: _correlatedRerun, dispositions: _dispositions, findings: _findings, ...rest } = cycle;
|
|
9862
|
+
return {
|
|
9863
|
+
...rest,
|
|
9864
|
+
findings,
|
|
9865
|
+
recording
|
|
9866
|
+
};
|
|
9867
|
+
};
|
|
9868
|
+
const foldWith = (candidate) => {
|
|
9869
|
+
const all = [...cycles, candidate];
|
|
9870
|
+
return foldLedger(all, ladderCycleRefsFor(all)).occurrences;
|
|
9871
|
+
};
|
|
9872
|
+
const repeats = identityRepeatDeclarations(foldWith(cycleWith(explicit)), cycleIndex);
|
|
9873
|
+
const floor = severityFloorDeclarations(foldWith(cycleWith([...explicit, ...repeats])), liveCycleCount);
|
|
9874
|
+
cycles.push(cycleWith([
|
|
9875
|
+
...explicit,
|
|
9876
|
+
...repeats,
|
|
9877
|
+
...floor
|
|
9878
|
+
]));
|
|
9879
|
+
}
|
|
9880
|
+
return {
|
|
9881
|
+
cycles,
|
|
9882
|
+
recovery
|
|
9883
|
+
};
|
|
9884
|
+
};
|
|
9885
|
+
/**
|
|
9886
|
+
* The operator-facing readout of a proof's unconverted declarations: one line
|
|
9887
|
+
* per item naming the declaration, why it was refused and the candidate
|
|
9888
|
+
* addresses `pr:review --dispositions` can name. Printed by pr:review,
|
|
9889
|
+
* pr:ready and the admission preflight (doctor) alike.
|
|
9890
|
+
*/
|
|
9891
|
+
const describeRecoveryRequired = (items) => items.map((item) => `Recovery required: schema-2 declaration ${item.declaration.disposition} "${item.declaration.title}"${item.declaration.file === void 0 ? "" : ` in ${item.declaration.file}`} (cycle ${item.cycle}) needs re-declaring by address; candidates: ${item.candidates.length === 0 ? "none" : item.candidates.join(", ")}. It could not be converted: ${item.reason}. Use pr:review --dispositions on the next recording; the proof admits nothing until then.`);
|
|
9892
|
+
/** The digest of a proof as the factory serializes it. */
|
|
9893
|
+
const proofSerializationDigest = (raw) => sha256Hex(`${JSON.stringify(raw, null, 2)}\n`);
|
|
9894
|
+
/**
|
|
9895
|
+
* Convert a schema-2 proof value into the schema-3 shape. `proofDigest` names
|
|
9896
|
+
* the original bytes (retained by the recording path for audit); when the
|
|
9897
|
+
* caller holds no bytes, the digest of the factory serialization stands in.
|
|
9898
|
+
* The result still goes through the schema-3 validator.
|
|
9899
|
+
*/
|
|
9900
|
+
const importSchema2Proof = (raw, proofDigest = proofSerializationDigest(raw)) => {
|
|
9901
|
+
const parsed = z.object({
|
|
9902
|
+
ladder: legacyLadderSchema.optional(),
|
|
9903
|
+
schemaVersion: z.literal(2)
|
|
9904
|
+
}).passthrough().safeParse(raw);
|
|
9905
|
+
if (!parsed.success) throw new TypeError(`Invalid pr:review proof JSON: schema-2 proof cannot be imported: ${parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`);
|
|
9906
|
+
const { ladder, ...rest } = parsed.data;
|
|
9907
|
+
if (!ladder) {
|
|
9908
|
+
const { schemaVersion: _legacyVersion, ...fields } = rest;
|
|
9909
|
+
return {
|
|
9910
|
+
...fields,
|
|
9911
|
+
schemaVersion: 3
|
|
9912
|
+
};
|
|
9913
|
+
}
|
|
9914
|
+
const { cycles, recovery } = convertCycles(ladder.cycles, proofDigest);
|
|
9915
|
+
evaluateReviewLadder({
|
|
9916
|
+
cycles,
|
|
9917
|
+
policy: { gate: { cap: 1 } }
|
|
9918
|
+
});
|
|
9919
|
+
const { schemaVersion: _legacyVersion, ...fields } = rest;
|
|
9920
|
+
return {
|
|
9921
|
+
...fields,
|
|
9922
|
+
importedFrom: {
|
|
9923
|
+
proofDigest,
|
|
9924
|
+
schemaVersion: 2
|
|
9925
|
+
},
|
|
9926
|
+
ladder: { cycles },
|
|
9927
|
+
recoveryRequired: recovery,
|
|
9928
|
+
schemaVersion: 3
|
|
9033
9929
|
};
|
|
9034
9930
|
};
|
|
9035
9931
|
//#endregion
|
|
9036
9932
|
//#region src/pr-readiness/ladder-proof-state.ts
|
|
9037
|
-
const
|
|
9933
|
+
const persistedCycleSchema = z.custom((cycle) => typeof cycle === "object" && cycle !== null, "ladder cycle must be an object").superRefine((cycle, context) => {
|
|
9934
|
+
const parsed = reviewCycleRecordingSchema.safeParse(cycle.recording);
|
|
9935
|
+
if (!parsed.success) context.addIssue({
|
|
9936
|
+
code: "custom",
|
|
9937
|
+
message: `ladder cycle carries no valid recording (schema 3): ${parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`,
|
|
9938
|
+
path: ["recording"]
|
|
9939
|
+
});
|
|
9940
|
+
});
|
|
9941
|
+
const prReviewLadderStateSchema = z.object({ cycles: z.array(persistedCycleSchema) }).superRefine((ladder, context) => {
|
|
9038
9942
|
try {
|
|
9039
9943
|
ladderLedgerFor(ladder);
|
|
9040
|
-
} catch {
|
|
9944
|
+
} catch (error) {
|
|
9041
9945
|
context.addIssue({
|
|
9042
9946
|
code: "custom",
|
|
9043
|
-
message:
|
|
9947
|
+
message: `ladder cycles do not fold into a valid disposition ledger: ${error instanceof Error ? error.message : String(error)}`
|
|
9044
9948
|
});
|
|
9045
9949
|
}
|
|
9046
9950
|
});
|
|
@@ -9102,13 +10006,12 @@ const openStateFromReviews = (reviews) => {
|
|
|
9102
10006
|
openEntries
|
|
9103
10007
|
};
|
|
9104
10008
|
};
|
|
9105
|
-
const openStateForCycle = (proof, epoch
|
|
9106
|
-
if (adjudicatedLedger) return openStateFromLedger(adjudicatedLedger);
|
|
10009
|
+
const openStateForCycle = (proof, epoch) => {
|
|
9107
10010
|
if (epoch.kind === "ladder") return epoch.snapshot ? epoch.snapshot.cycleState : openStateFromLadder(epoch.ladder);
|
|
9108
10011
|
return openStateFromReviews(proof.reviews);
|
|
9109
10012
|
};
|
|
9110
|
-
const reviewCycleStateForEpoch = (proof, epoch
|
|
9111
|
-
const openState = openStateForCycle(proof, epoch
|
|
10013
|
+
const reviewCycleStateForEpoch = (proof, epoch) => {
|
|
10014
|
+
const openState = openStateForCycle(proof, epoch);
|
|
9112
10015
|
const countsBySeverity = countFindingsBySeverity(openState.openEntries);
|
|
9113
10016
|
const highestOpenSeverity = REVIEW_SEVERITIES.find((severity) => countsBySeverity[severity] > 0);
|
|
9114
10017
|
const countsByBlockingSeverity = countFindingsBySeverity(openState.blockingEntries);
|
|
@@ -9128,7 +10031,7 @@ const reviewCycleStateForEpoch = (proof, epoch, adjudicatedLedger) => {
|
|
|
9128
10031
|
windowExhausted
|
|
9129
10032
|
};
|
|
9130
10033
|
};
|
|
9131
|
-
const reviewCycleStateFor = (proof, policy
|
|
10034
|
+
const reviewCycleStateFor = (proof, policy) => reviewCycleStateForEpoch(proof, reviewEpochModeFor(proof, policy));
|
|
9132
10035
|
const reviewAcceptedAfterCycleCap = (proof, review) => {
|
|
9133
10036
|
if (review.outcome !== "failed") return false;
|
|
9134
10037
|
const state = reviewCycleStateFor({
|
|
@@ -9137,51 +10040,40 @@ const reviewAcceptedAfterCycleCap = (proof, review) => {
|
|
|
9137
10040
|
});
|
|
9138
10041
|
return state.windowExhausted && state.autoBlockingFindings === 0;
|
|
9139
10042
|
};
|
|
9140
|
-
const
|
|
9141
|
-
if (epoch.kind !== "ladder") return
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
};
|
|
9145
|
-
if (epoch.snapshot) return {
|
|
9146
|
-
autoBlockingFindings: epoch.snapshot.cycleState.autoBlockingFindings,
|
|
9147
|
-
unmatchedDispositions: epoch.snapshot.evaluation.unmatchedDispositions.length
|
|
9148
|
-
};
|
|
9149
|
-
const { ledger, unmatchedDispositions } = foldLedger(epoch.ladder.cycles, ladderCycleRefsFor(epoch.ladder.cycles));
|
|
9150
|
-
return {
|
|
9151
|
-
autoBlockingFindings: blockingLadderFindings([...ledger.values()]).length,
|
|
9152
|
-
unmatchedDispositions: unmatchedDispositions.length
|
|
9153
|
-
};
|
|
10043
|
+
const concealedBlockingFindings = (epoch) => {
|
|
10044
|
+
if (epoch.kind !== "ladder") return 0;
|
|
10045
|
+
if (epoch.snapshot) return epoch.snapshot.cycleState.autoBlockingFindings;
|
|
10046
|
+
return blockingLadderFindings(ladderLedgerFor(epoch.ladder)).length;
|
|
9154
10047
|
};
|
|
9155
10048
|
const resolveReviewAcceptanceForEpoch = (proof, review, epoch) => {
|
|
9156
|
-
if (
|
|
9157
|
-
|
|
9158
|
-
if (disagreement.autoBlockingFindings > 0 || disagreement.unmatchedDispositions > 0) return "blocked";
|
|
9159
|
-
return "accepted-clean";
|
|
9160
|
-
}
|
|
10049
|
+
if (proof.recoveryRequired !== void 0 && proof.recoveryRequired.length > 0) return "recovery-required";
|
|
10050
|
+
if (review.outcome === "passed") return concealedBlockingFindings(epoch) > 0 ? "blocked" : "accepted-clean";
|
|
9161
10051
|
if (review.outcome !== "failed") return "blocked";
|
|
9162
10052
|
if (epoch.kind === "ladder" && epoch.snapshot && reviewAcceptedThroughSnapshot(epoch.snapshot)) return "accepted-through-ladder";
|
|
9163
10053
|
if (epoch.kind === "declared-window" && reviewAcceptedAfterCycleCap(proof, review)) return "accepted-after-cap";
|
|
9164
10054
|
return "blocked";
|
|
9165
10055
|
};
|
|
9166
10056
|
const resolveReviewAcceptance = (proof, review, policy) => resolveReviewAcceptanceForEpoch(proof, review, reviewEpochModeFor(proof, policy));
|
|
9167
|
-
const resolveReviewTerminalStateForEpoch = (proof, review, epoch
|
|
10057
|
+
const resolveReviewTerminalStateForEpoch = (proof, review, epoch) => {
|
|
9168
10058
|
const acceptance = resolveReviewAcceptanceForEpoch(proof, review, epoch);
|
|
9169
10059
|
if (acceptance === "accepted-clean") return "clean";
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
return openFindings === 0 && autoBlockingFindings === 0 ? "clean" : "blocked";
|
|
9174
|
-
}
|
|
10060
|
+
if (acceptance === "recovery-required") return "blocked";
|
|
10061
|
+
const { autoBlockingFindings, openFindings } = reviewCycleStateForEpoch(proof, epoch);
|
|
10062
|
+
if (acceptance === "blocked") return openFindings === 0 && autoBlockingFindings === 0 ? "clean" : "blocked";
|
|
9175
10063
|
return openFindings === 0 ? "clean" : "accepted-with-findings";
|
|
9176
10064
|
};
|
|
9177
10065
|
const resolveReviewTerminalState = (proof, review, policy) => resolveReviewTerminalStateForEpoch(proof, review, reviewEpochModeFor(proof, policy));
|
|
9178
|
-
const resolveReviewTerminalStateAcrossReviews = (proof, epoch
|
|
9179
|
-
const states = proof.reviews.map((review) => resolveReviewTerminalStateForEpoch(proof, review, epoch
|
|
10066
|
+
const resolveReviewTerminalStateAcrossReviews = (proof, epoch) => {
|
|
10067
|
+
const states = proof.reviews.map((review) => resolveReviewTerminalStateForEpoch(proof, review, epoch));
|
|
9180
10068
|
if (states.includes("blocked")) return "blocked";
|
|
9181
10069
|
if (states.includes("accepted-with-findings")) return "accepted-with-findings";
|
|
9182
10070
|
return states.length > 0 ? "clean" : void 0;
|
|
9183
10071
|
};
|
|
9184
10072
|
const resolveReviewTerminalStateAcrossReviewsForPolicy = (proof, policy) => resolveReviewTerminalStateAcrossReviews(proof, reviewEpochModeFor(proof, policy));
|
|
10073
|
+
const accepted = (proof, review, epoch) => {
|
|
10074
|
+
const acceptance = resolveReviewAcceptanceForEpoch(proof, review, epoch);
|
|
10075
|
+
return acceptance !== "blocked" && acceptance !== "recovery-required";
|
|
10076
|
+
};
|
|
9185
10077
|
const reviewProofForEpoch = (proof, kind, epoch, current) => {
|
|
9186
10078
|
const review = proof?.reviews.find((entry) => entry.kind === kind);
|
|
9187
10079
|
if (proof && review && review.typedVerdict !== true) return {
|
|
@@ -9189,7 +10081,7 @@ const reviewProofForEpoch = (proof, kind, epoch, current) => {
|
|
|
9189
10081
|
reviewedPatchId: proof.patchId,
|
|
9190
10082
|
untypedVerdict: true
|
|
9191
10083
|
};
|
|
9192
|
-
if (proof && review &&
|
|
10084
|
+
if (proof && review && accepted(proof, review, epoch)) {
|
|
9193
10085
|
if (current && !evidenceFresh({
|
|
9194
10086
|
candidate: current,
|
|
9195
10087
|
recorded: { patchId: proof.patchId },
|
|
@@ -9216,8 +10108,9 @@ var review_proof_exports = /* @__PURE__ */ __exportAll({
|
|
|
9216
10108
|
PR_REVIEW_FINAL_READINESS_NOTICE: () => PR_REVIEW_FINAL_READINESS_NOTICE,
|
|
9217
10109
|
PR_REVIEW_FINDING_PROVENANCE_VERSION: () => 1,
|
|
9218
10110
|
PR_REVIEW_PROOF_DESCRIPTOR: () => PR_REVIEW_PROOF_DESCRIPTOR,
|
|
9219
|
-
PR_REVIEW_SCHEMA_VERSION: () =>
|
|
10111
|
+
PR_REVIEW_SCHEMA_VERSION: () => 3,
|
|
9220
10112
|
REVIEW_SEVERITIES: () => REVIEW_SEVERITIES,
|
|
10113
|
+
parseReviewProofBytes: () => parseReviewProofBytes,
|
|
9221
10114
|
prReviewFindingSchema: () => prReviewFindingSchema,
|
|
9222
10115
|
prReviewProofSchema: () => prReviewProofSchema,
|
|
9223
10116
|
prReviewResultSchema: () => prReviewResultSchema,
|
|
@@ -9309,18 +10202,42 @@ const prReviewProofSchema = z.object({
|
|
|
9309
10202
|
cleanedPaths: z.array(z.string()),
|
|
9310
10203
|
findingProvenanceVersion: z.literal(1),
|
|
9311
10204
|
headSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
10205
|
+
importedFrom: z.object({
|
|
10206
|
+
proofDigest: z.string().regex(/^[0-9a-f]{64}$/u),
|
|
10207
|
+
schemaVersion: z.literal(2)
|
|
10208
|
+
}).strict().optional(),
|
|
9312
10209
|
ladder: prReviewLadderStateSchema.optional(),
|
|
9313
10210
|
maxReviewCycles: z.number().int().positive().optional(),
|
|
9314
10211
|
patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
|
|
9315
10212
|
policyResolution: policyResolutionSchema.optional(),
|
|
10213
|
+
recoveryRequired: z.array(z.object({
|
|
10214
|
+
candidates: z.array(z.string().regex(OCCURRENCE_ADDRESS_PATTERN)),
|
|
10215
|
+
cycle: z.number().int().positive(),
|
|
10216
|
+
declaration: z.object({
|
|
10217
|
+
disposition: z.enum([
|
|
10218
|
+
"fixed-in-thread",
|
|
10219
|
+
"follow-up-filed",
|
|
10220
|
+
"waived"
|
|
10221
|
+
]),
|
|
10222
|
+
file: z.string().optional(),
|
|
10223
|
+
reference: z.string().optional(),
|
|
10224
|
+
title: z.string()
|
|
10225
|
+
}).strict(),
|
|
10226
|
+
reason: z.string().min(1)
|
|
10227
|
+
}).strict()).optional(),
|
|
9316
10228
|
reviewCycle: z.number().int().positive().optional(),
|
|
9317
10229
|
reviewRequirement: z.object({
|
|
9318
10230
|
reason: z.enum(["no-applicable-mode", "faithful-merge"]),
|
|
9319
10231
|
status: z.literal("not-required")
|
|
9320
10232
|
}).strict().optional(),
|
|
9321
10233
|
reviews: z.array(prReviewResultSchema),
|
|
9322
|
-
schemaVersion: z.literal(
|
|
10234
|
+
schemaVersion: z.literal(3)
|
|
9323
10235
|
}).passthrough().superRefine((proof, context) => {
|
|
10236
|
+
if (proof.importedFrom !== void 0 && proof.recoveryRequired === void 0) context.addIssue({
|
|
10237
|
+
code: "custom",
|
|
10238
|
+
message: "a proof converted from schema 2 (importedFrom) must carry recoveryRequired, an array that may be empty",
|
|
10239
|
+
path: ["recoveryRequired"]
|
|
10240
|
+
});
|
|
9324
10241
|
if (proof.reviewRequirement && proof.reviews.length > 0) context.addIssue({
|
|
9325
10242
|
code: "custom",
|
|
9326
10243
|
message: "not-required review proofs must not contain review runs",
|
|
@@ -9337,20 +10254,23 @@ const validatePrReviewResult = (value) => {
|
|
|
9337
10254
|
if (!parsed.success) throw new TypeError("Invalid pr:review result JSON");
|
|
9338
10255
|
return parsed.data;
|
|
9339
10256
|
};
|
|
9340
|
-
const validatePrReviewProof = (value) => {
|
|
9341
|
-
|
|
9342
|
-
|
|
10257
|
+
const validatePrReviewProof = (value, proofDigest) => {
|
|
10258
|
+
const imported = value && typeof value === "object" && value.schemaVersion === 2 ? importSchema2Proof(value, proofDigest) : value;
|
|
10259
|
+
if (!imported || typeof imported !== "object" || imported.schemaVersion !== 3) throw new Error("Unsupported pr:review proof schemaVersion");
|
|
10260
|
+
const parsed = prReviewProofSchema.safeParse(imported);
|
|
9343
10261
|
if (!parsed.success) {
|
|
9344
10262
|
const inReviews = parsed.error.issues.some((issue) => issue.path[0] === "reviews");
|
|
9345
|
-
throw new TypeError(inReviews ? "Invalid pr:review review entry" :
|
|
10263
|
+
throw new TypeError(inReviews ? "Invalid pr:review review entry" : `Invalid pr:review proof JSON: ${parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`);
|
|
9346
10264
|
}
|
|
9347
10265
|
return parsed.data;
|
|
9348
10266
|
};
|
|
10267
|
+
const parseReviewProofBytes = (bytes) => validatePrReviewProof(JSON.parse(bytes), sha256Hex(bytes));
|
|
9349
10268
|
const PR_REVIEW_PROOF_DESCRIPTOR = {
|
|
9350
10269
|
defaultPath: DEFAULT_PR_REVIEW_PROOF_PATH,
|
|
9351
10270
|
label: "pr:review proof",
|
|
9352
10271
|
parse: validatePrReviewProof,
|
|
9353
|
-
|
|
10272
|
+
parseBytes: parseReviewProofBytes,
|
|
10273
|
+
schemaVersion: 3
|
|
9354
10274
|
};
|
|
9355
10275
|
//#endregion
|
|
9356
10276
|
//#region src/review-proof-applicability.ts
|
|
@@ -13380,6 +14300,34 @@ function hqIngestCredentialCheck({ env, resolve = defaultSecretReferenceProbe, r
|
|
|
13380
14300
|
//#endregion
|
|
13381
14301
|
//#region src/admission-preflight.ts
|
|
13382
14302
|
const errorMessage$1 = (error) => error instanceof Error ? error.message : String(error);
|
|
14303
|
+
/**
|
|
14304
|
+
* The base every row below measures against (#1131, #1121). Bound the same
|
|
14305
|
+
* way `pr:review` binds it, so doctor and publish read one candidate: the
|
|
14306
|
+
* live pull request base when a pull request exists, otherwise the requested
|
|
14307
|
+
* or default ref fetched fresh. A binding failure is reported here and leaves
|
|
14308
|
+
* the candidate unresolved, so every other row says so rather than measuring
|
|
14309
|
+
* against a guessed base.
|
|
14310
|
+
*/
|
|
14311
|
+
const candidateBaseCheck = (bound) => "binding" in bound ? {
|
|
14312
|
+
message: `Candidate ${describeCandidateBase(bound.binding)}; pr:review and pr:publish measure against this same base.`,
|
|
14313
|
+
name: "admission:candidate-base",
|
|
14314
|
+
status: "ok"
|
|
14315
|
+
} : {
|
|
14316
|
+
message: `Candidate base could not be bound: ${bound.error}`,
|
|
14317
|
+
name: "admission:candidate-base",
|
|
14318
|
+
status: "warning"
|
|
14319
|
+
};
|
|
14320
|
+
const bindCandidateBase$1 = ({ base, candidateBase, cwd }) => {
|
|
14321
|
+
try {
|
|
14322
|
+
return { binding: resolveCandidateBase({
|
|
14323
|
+
cwd,
|
|
14324
|
+
...candidateBase === void 0 ? {} : { dependencies: candidateBase },
|
|
14325
|
+
...base === void 0 ? {} : { requestedBase: base }
|
|
14326
|
+
}) };
|
|
14327
|
+
} catch (error) {
|
|
14328
|
+
return { error: errorMessage$1(error) };
|
|
14329
|
+
}
|
|
14330
|
+
};
|
|
13383
14331
|
const resolveCandidate = ({ base, cwd, verifyProof }) => {
|
|
13384
14332
|
try {
|
|
13385
14333
|
const files = changedFiles(cwd, base);
|
|
@@ -13456,11 +14404,38 @@ const verifyProofCheck = ({ candidate, candidateError, preread, verifyProofPath
|
|
|
13456
14404
|
};
|
|
13457
14405
|
};
|
|
13458
14406
|
/**
|
|
14407
|
+
* Why a review proof file yielded no proof: the file is absent, or it is
|
|
14408
|
+
* present and unreadable (#1121, #1131). The two are different diagnoses with
|
|
14409
|
+
* different repairs, so preflight never reports one as the other.
|
|
14410
|
+
*/
|
|
14411
|
+
const reviewProofReadFailure = (error, proofPath) => error?.code === "ENOENT" ? { kind: "absent" } : {
|
|
14412
|
+
kind: "invalid",
|
|
14413
|
+
message: `pr:review proof at ${proofPath} is present but invalid, not absent: ${errorMessage$1(error)}. A proof that fails to parse is treated by readiness as missing evidence; re-record it with pr:review --findings <path>.`
|
|
14414
|
+
};
|
|
14415
|
+
/**
|
|
14416
|
+
* A review-proof lock older than the write wait window (#1132). No writer
|
|
14417
|
+
* ever removes a lock it did not create, so a crashed writer's lock refuses
|
|
14418
|
+
* every later `pr:review` until an operator removes it; this row names the
|
|
14419
|
+
* step. Absent (no row) when there is no lock or it is young enough to be a
|
|
14420
|
+
* live writer's.
|
|
14421
|
+
*/
|
|
14422
|
+
const reviewProofLockCheck = (reviewProofPath) => {
|
|
14423
|
+
const held = describeHeldProofLock(reviewProofPath);
|
|
14424
|
+
if (held === void 0 || !proofLockOutlivesWait(held)) return [];
|
|
14425
|
+
return [{
|
|
14426
|
+
message: `pr:review proof lock ${held.lockPath} has been held for ${Math.round(held.ageMs / 1e3)}s since ${held.since} (token ${held.token}, pid ${held.pid}); every recording will refuse with concurrent-write until it is released: ${LOCK_REMOVAL_INSTRUCTION}.`,
|
|
14427
|
+
name: "admission:review-proof-lock",
|
|
14428
|
+
status: "warning"
|
|
14429
|
+
}];
|
|
14430
|
+
};
|
|
14431
|
+
/**
|
|
13459
14432
|
* Independent review proof. Requiredness comes from the profile-resolved review
|
|
13460
14433
|
* modes; currency and acceptance come from the same identity and
|
|
13461
|
-
* terminal-state helpers `pr:publish` uses.
|
|
14434
|
+
* terminal-state helpers `pr:publish` uses. The proof is read through
|
|
14435
|
+
* `tryReadProof`, which keeps the read error: an unparseable file is reported
|
|
14436
|
+
* as invalid, never as absent (#1121).
|
|
13462
14437
|
*/
|
|
13463
|
-
const reviewProofCheck = ({ candidate, candidateError,
|
|
14438
|
+
const reviewProofCheck = ({ candidate, candidateError, read, reviewModes, reviewProofPath }) => {
|
|
13464
14439
|
const name = "admission:review-proof";
|
|
13465
14440
|
if (!candidate) return {
|
|
13466
14441
|
message: `Review proof state is unknown: no git candidate is resolvable here (${candidateError ?? "unknown error"}).`,
|
|
@@ -13472,21 +14447,44 @@ const reviewProofCheck = ({ candidate, candidateError, proof, reviewModes }) =>
|
|
|
13472
14447
|
name,
|
|
13473
14448
|
status: "ok"
|
|
13474
14449
|
};
|
|
14450
|
+
const required = `${reviewModes.join(", ")} review(s) are required. Have an independent clean session write findings and pass them to pr:review --findings <path>.`;
|
|
14451
|
+
if (read.proof === void 0) {
|
|
14452
|
+
const failure = reviewProofReadFailure(read.error, reviewProofPath);
|
|
14453
|
+
return {
|
|
14454
|
+
message: failure.kind === "absent" ? `pr:review proof is absent: no file at ${reviewProofPath}. ${required}` : `${failure.message} ${required}`,
|
|
14455
|
+
name,
|
|
14456
|
+
status: "warning"
|
|
14457
|
+
};
|
|
14458
|
+
}
|
|
14459
|
+
const { proof } = read;
|
|
14460
|
+
const recorded = latestRecordingSentence(proof);
|
|
14461
|
+
const recovery = describeRecoveryRequired(proof.recoveryRequired ?? []).map((line) => ` ${line}`).join("");
|
|
13475
14462
|
const status = reviewStatus({
|
|
13476
14463
|
headSha: candidate.headSha,
|
|
13477
14464
|
patchId: candidate.patchId,
|
|
13478
14465
|
required: true,
|
|
13479
|
-
reviewedHeadSha: proof
|
|
13480
|
-
reviewedPatchId: proof
|
|
14466
|
+
reviewedHeadSha: proof.headSha,
|
|
14467
|
+
reviewedPatchId: proof.patchId
|
|
13481
14468
|
});
|
|
13482
|
-
const terminal =
|
|
13483
|
-
|
|
14469
|
+
const terminal = resolveReviewTerminalStateAcrossReviewsForPolicy(proof, resolveReviewLadderPolicy());
|
|
14470
|
+
if (status === "current" && (terminal === "clean" || terminal === "accepted-with-findings")) return {
|
|
14471
|
+
message: `pr:review proof is current for this candidate (${terminal}) across ${reviewModes.join(", ")}.${recorded}`,
|
|
14472
|
+
name,
|
|
14473
|
+
status: "ok"
|
|
14474
|
+
};
|
|
13484
14475
|
return {
|
|
13485
|
-
message:
|
|
14476
|
+
message: status === "stale" ? `pr:review proof is stale: it reviewed patch ${proof.patchId} at head ${proof.headSha}, and the candidate is patch ${candidate.patchId} at head ${candidate.headSha} (${terminal}).${recorded}${recovery} ${required}` : `pr:review proof is ${status} (${terminal}).${recorded}${recovery} ${required}`,
|
|
13486
14477
|
name,
|
|
13487
|
-
status:
|
|
14478
|
+
status: "warning"
|
|
13488
14479
|
};
|
|
13489
14480
|
};
|
|
14481
|
+
const latestRecordingSentence = (proof) => {
|
|
14482
|
+
const latest = proof.ladder?.cycles.at(-1)?.recording;
|
|
14483
|
+
const cycle = proof.ladder?.cycles.length ?? 0;
|
|
14484
|
+
if (latest === void 0) return "";
|
|
14485
|
+
if ("recordingId" in latest) return ` Latest recording ${latest.recordingId} (cycle ${cycle}, base ${latest.base.ref} at ${latest.base.sha}).`;
|
|
14486
|
+
return ` Latest cycle ${cycle} was imported from a schema-2 proof (${latest.imported.proofDigest}).`;
|
|
14487
|
+
};
|
|
13490
14488
|
/**
|
|
13491
14489
|
* Resolve the demand this candidate actually owes.
|
|
13492
14490
|
*
|
|
@@ -13599,19 +14597,26 @@ const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, profileP
|
|
|
13599
14597
|
* requirement is reported by the doctor `github-app` check, which states the
|
|
13600
14598
|
* consequence of its absence.
|
|
13601
14599
|
*/
|
|
13602
|
-
const admissionPreflightChecks = ({ base
|
|
14600
|
+
const admissionPreflightChecks = ({ base, candidateBase, cwd, env = process.env, hqIngestCredentials, profile, profilePath, reviewProofPath = DEFAULT_PR_REVIEW_PROOF_PATH, verifyProofPath = DEFAULT_PR_VERIFY_PROOF_PATH }) => {
|
|
13603
14601
|
const resolvedVerifyProofPath = path.resolve(cwd, verifyProofPath);
|
|
13604
14602
|
const preread = tryReadProof(readPrVerifyProof, resolvedVerifyProofPath);
|
|
13605
|
-
const
|
|
14603
|
+
const bound = bindCandidateBase$1({
|
|
13606
14604
|
base,
|
|
14605
|
+
candidateBase,
|
|
14606
|
+
cwd
|
|
14607
|
+
});
|
|
14608
|
+
const resolved = "binding" in bound ? resolveCandidate({
|
|
14609
|
+
base: bound.binding.sha,
|
|
13607
14610
|
cwd,
|
|
13608
14611
|
verifyProof: preread.proof
|
|
13609
|
-
});
|
|
14612
|
+
}) : bound;
|
|
13610
14613
|
const candidate = "candidate" in resolved ? resolved.candidate : void 0;
|
|
13611
14614
|
const candidateError = "error" in resolved ? resolved.error : void 0;
|
|
13612
|
-
const
|
|
14615
|
+
const resolvedReviewProofPath = path.resolve(cwd, reviewProofPath);
|
|
14616
|
+
const reviewRead = tryReadProof((proofPath) => readProofFor(PR_REVIEW_PROOF_DESCRIPTOR, cwd, proofPath), resolvedReviewProofPath);
|
|
13613
14617
|
return [
|
|
13614
14618
|
authoringSessionCheck(env),
|
|
14619
|
+
candidateBaseCheck(bound),
|
|
13615
14620
|
verifyProofCheck({
|
|
13616
14621
|
candidate,
|
|
13617
14622
|
candidateError,
|
|
@@ -13621,9 +14626,11 @@ const admissionPreflightChecks = ({ base = "origin/main", cwd, env = process.env
|
|
|
13621
14626
|
reviewProofCheck({
|
|
13622
14627
|
candidate,
|
|
13623
14628
|
candidateError,
|
|
13624
|
-
|
|
13625
|
-
reviewModes: candidate ? resolveApplicableReviewModes(profile, candidate.files) : ["correctness"]
|
|
14629
|
+
read: reviewRead,
|
|
14630
|
+
reviewModes: candidate ? resolveApplicableReviewModes(profile, candidate.files) : ["correctness"],
|
|
14631
|
+
reviewProofPath: resolvedReviewProofPath
|
|
13626
14632
|
}),
|
|
14633
|
+
...reviewProofLockCheck(resolvedReviewProofPath),
|
|
13627
14634
|
...requiredCheckChecks({
|
|
13628
14635
|
candidate,
|
|
13629
14636
|
candidateError,
|
|
@@ -13875,7 +14882,7 @@ function repositoryUrlMatches(origin, profile) {
|
|
|
13875
14882
|
//#endregion
|
|
13876
14883
|
//#region src/commands/doctor.ts
|
|
13877
14884
|
function createDoctorCommand(output) {
|
|
13878
|
-
return markCwdOptionDefault(new Command("doctor").description("Validate a project profile without mutating GitHub or local files").option("--base <ref>", "base
|
|
14885
|
+
return markCwdOptionDefault(new Command("doctor").description("Validate a project profile without mutating GitHub or local files").option("--base <ref>", "base ref to assert for --preflight. With a pull request open for this branch its live base is the base and a disagreeing value is reported; without one, this ref (default origin/main) is fetched fresh").option("--cwd <path>", "working directory to validate", collectCwdOption).option("--json", "print the doctor report as JSON").option("--preflight", "also list the pre-checkable admission requirements for the current candidate (read-only; never blocks); each required check states whether this candidate is demanded to satisfy it, resolved by candidate:impact; findings-file shape and wave membership have no read-only pre-check").option("--profile <path>", "path to the project profile JSON file").action(async (options) => {
|
|
13879
14886
|
const report = await doctorProjectProfile({
|
|
13880
14887
|
base: options.base,
|
|
13881
14888
|
cwd: resolveCwdOption(options.cwd),
|
|
@@ -15637,7 +16644,7 @@ const prVerifyEvaluation = ({ acceptedBaselineHeadShas, currentHeadSha, docsOnly
|
|
|
15637
16644
|
prVerifyStatus: "missing"
|
|
15638
16645
|
};
|
|
15639
16646
|
};
|
|
15640
|
-
const addReviewBlockingReasons = ({ blockers, correctnessRequired, correctnessStatus, correctnessUntyped }) => {
|
|
16647
|
+
const addReviewBlockingReasons = ({ blockers, correctnessRequired, correctnessStatus, correctnessUntyped, recovering }) => {
|
|
15641
16648
|
const demand = reviewModeDemand("correctness");
|
|
15642
16649
|
if (correctnessRequired && correctnessUntyped) blockers.push({
|
|
15643
16650
|
demand,
|
|
@@ -15651,12 +16658,19 @@ const addReviewBlockingReasons = ({ blockers, correctnessRequired, correctnessSt
|
|
|
15651
16658
|
demand,
|
|
15652
16659
|
reason: "Correctness review proof is stale because the PR-owned patch-id changed."
|
|
15653
16660
|
});
|
|
15654
|
-
else if (correctnessStatus === "blocked") blockers.push({
|
|
16661
|
+
else if (correctnessStatus === "blocked" && !recovering) blockers.push({
|
|
15655
16662
|
demand,
|
|
15656
16663
|
reason: "Correctness review proof ran and explicitly failed (open blocking findings or errored run); resolve the findings and re-run pr:review."
|
|
15657
16664
|
});
|
|
15658
16665
|
};
|
|
15659
|
-
const
|
|
16666
|
+
const recoveryPending = (reviewProof) => (reviewProof?.recoveryRequired?.length ?? 0) > 0;
|
|
16667
|
+
const addRecoveryBlockingReasons = ({ blockers, reviewProof }) => {
|
|
16668
|
+
for (const reason of describeRecoveryRequired(reviewProof?.recoveryRequired ?? [])) blockers.push({
|
|
16669
|
+
demand: reviewModeDemand("correctness"),
|
|
16670
|
+
reason
|
|
16671
|
+
});
|
|
16672
|
+
};
|
|
16673
|
+
const addSecurityBlockingReasons = ({ blockers, recovering, securityRequired, securityStatus, securityUntyped }) => {
|
|
15660
16674
|
const demand = reviewModeDemand("security");
|
|
15661
16675
|
if (securityRequired && securityUntyped) blockers.push({
|
|
15662
16676
|
demand,
|
|
@@ -15670,7 +16684,7 @@ const addSecurityBlockingReasons = ({ blockers, securityRequired, securityStatus
|
|
|
15670
16684
|
demand,
|
|
15671
16685
|
reason: "Security review proof is stale because the PR-owned patch-id changed."
|
|
15672
16686
|
});
|
|
15673
|
-
else if (securityStatus === "blocked") blockers.push({
|
|
16687
|
+
else if (securityStatus === "blocked" && !recovering) blockers.push({
|
|
15674
16688
|
demand,
|
|
15675
16689
|
reason: "Security review proof ran and explicitly failed (open blocking findings or errored run); resolve or disposition the findings and re-run pr:review."
|
|
15676
16690
|
});
|
|
@@ -15770,6 +16784,7 @@ const awaitPostUndraftChecksRepair = (input) => {
|
|
|
15770
16784
|
};
|
|
15771
16785
|
const collectBlockers = ({ correctnessRequired, correctnessStatus, correctnessUntyped, input, prVerifyStatus, verificationProof, securityRequired, securityStatus, securityUntyped }) => {
|
|
15772
16786
|
const blockers = [];
|
|
16787
|
+
const recovering = recoveryPending(input.reviewProof);
|
|
15773
16788
|
if (prVerifyStatus === "missing") blockers.push({
|
|
15774
16789
|
demand: DEMAND_KEYS.prVerify,
|
|
15775
16790
|
reason: "Missing passing typed pr:verify proof for the current PR head."
|
|
@@ -15787,10 +16802,16 @@ const collectBlockers = ({ correctnessRequired, correctnessStatus, correctnessUn
|
|
|
15787
16802
|
blockers,
|
|
15788
16803
|
correctnessRequired,
|
|
15789
16804
|
correctnessStatus,
|
|
15790
|
-
correctnessUntyped
|
|
16805
|
+
correctnessUntyped,
|
|
16806
|
+
recovering
|
|
16807
|
+
});
|
|
16808
|
+
addRecoveryBlockingReasons({
|
|
16809
|
+
blockers,
|
|
16810
|
+
reviewProof: input.reviewProof
|
|
15791
16811
|
});
|
|
15792
16812
|
addSecurityBlockingReasons({
|
|
15793
16813
|
blockers,
|
|
16814
|
+
recovering,
|
|
15794
16815
|
securityRequired,
|
|
15795
16816
|
securityStatus,
|
|
15796
16817
|
securityUntyped
|
|
@@ -17027,6 +18048,40 @@ const armingNotice = (arming) => {
|
|
|
17027
18048
|
if (arming.outcome === "merged") return `This candidate merged during the arming call: ${arming.detail ?? "GitHub found it already mergeable."}`;
|
|
17028
18049
|
return `Native auto-merge was NOT armed for ${arming.headSha}: ${arming.detail ?? "unknown reason"} Nothing will merge this candidate until the recorded follow-up runs.`;
|
|
17029
18050
|
};
|
|
18051
|
+
/**
|
|
18052
|
+
* #1125: a throw before the branded check is published used to POST a red
|
|
18053
|
+
* stub (`ledger head unknown`) even when the miss was Cursor OIDC mint or
|
|
18054
|
+
* broker exchange. That reads as evaluated-refused. Absence is the #1037
|
|
18055
|
+
* not-yet shape: the required check does not pass, and a retry is not a
|
|
18056
|
+
* candidate repair. Proof-chain refusals still publish red.
|
|
18057
|
+
*/
|
|
18058
|
+
const publishThrownFailureCheck = async (input) => {
|
|
18059
|
+
const message = input.error instanceof Error ? input.error.message : String(input.error);
|
|
18060
|
+
if (isFactoryAppUnevaluableError(input.error)) {
|
|
18061
|
+
console.warn(`pr:ready could not evaluate; no ${FACTORY_CHECK_NAMES["pr-ready"]} check run was published for ${input.sha}. ${message.trim()} Retry. This is not a candidate refusal.`);
|
|
18062
|
+
return;
|
|
18063
|
+
}
|
|
18064
|
+
const failureCheckInput = {
|
|
18065
|
+
conclusion: "failure",
|
|
18066
|
+
cwd: input.cwd,
|
|
18067
|
+
gate: "pr-ready",
|
|
18068
|
+
owner: input.owner,
|
|
18069
|
+
pr: input.pr,
|
|
18070
|
+
proof: {
|
|
18071
|
+
blockingReasons: [message],
|
|
18072
|
+
schemaVersion: 3,
|
|
18073
|
+
status: "blocked"
|
|
18074
|
+
},
|
|
18075
|
+
repo: input.repo,
|
|
18076
|
+
sha: input.sha
|
|
18077
|
+
};
|
|
18078
|
+
try {
|
|
18079
|
+
if (!await (input.publishFinalCheckRun ? input.publishFinalCheckRun(failureCheckInput) : ensureFactoryCheckRunPublished(failureCheckInput, { onDiagnostic: (diagnostic) => process.stderr.write(diagnostic) }))) console.warn(`pr:ready could not publish the failing ${FACTORY_CHECK_NAMES["pr-ready"]} check run for ${input.sha}; the required check is absent rather than red.`);
|
|
18080
|
+
} catch (publishError) {
|
|
18081
|
+
const publishMessage = publishError instanceof Error ? publishError.message : String(publishError);
|
|
18082
|
+
console.warn(`pr:ready could not publish the failing ${FACTORY_CHECK_NAMES["pr-ready"]} check run for ${input.sha}; the required check is absent rather than red. ${publishMessage}`);
|
|
18083
|
+
}
|
|
18084
|
+
};
|
|
17030
18085
|
async function runPrReady(args, dependencies = {}) {
|
|
17031
18086
|
const cwd = path.resolve(args.cwd ?? process.cwd());
|
|
17032
18087
|
const profilePath = readinessProfilePath(cwd, args.profilePath);
|
|
@@ -17309,27 +18364,15 @@ async function runPrReady(args, dependencies = {}) {
|
|
|
17309
18364
|
} catch (error) {
|
|
17310
18365
|
if (error instanceof UnreadableLiveBaseTipError && presentedHeadSha === void 0) presentedHeadSha = error.headRefOid;
|
|
17311
18366
|
if (!finalCheckPublished && presentedHeadSha) {
|
|
17312
|
-
|
|
17313
|
-
const failureCheckInput = {
|
|
17314
|
-
conclusion: "failure",
|
|
18367
|
+
await publishThrownFailureCheck({
|
|
17315
18368
|
cwd,
|
|
17316
|
-
|
|
18369
|
+
error,
|
|
17317
18370
|
owner: repository.owner,
|
|
17318
18371
|
pr: args.pr,
|
|
17319
|
-
|
|
17320
|
-
blockingReasons: [message],
|
|
17321
|
-
schemaVersion: 3,
|
|
17322
|
-
status: "blocked"
|
|
17323
|
-
},
|
|
18372
|
+
publishFinalCheckRun: dependencies.publishFinalCheckRun,
|
|
17324
18373
|
repo: repository.name,
|
|
17325
18374
|
sha: presentedHeadSha
|
|
17326
|
-
};
|
|
17327
|
-
try {
|
|
17328
|
-
if (!await (dependencies.publishFinalCheckRun ? dependencies.publishFinalCheckRun(failureCheckInput) : ensureFactoryCheckRunPublished(failureCheckInput, { onDiagnostic: (diagnostic) => process.stderr.write(diagnostic) }))) console.warn(`pr:ready could not publish the failing ${FACTORY_CHECK_NAMES["pr-ready"]} check run for ${presentedHeadSha}; the required check is absent rather than red.`);
|
|
17329
|
-
} catch (publishError) {
|
|
17330
|
-
const publishMessage = publishError instanceof Error ? publishError.message : String(publishError);
|
|
17331
|
-
console.warn(`pr:ready could not publish the failing ${FACTORY_CHECK_NAMES["pr-ready"]} check run for ${presentedHeadSha}; the required check is absent rather than red. ${publishMessage}`);
|
|
17332
|
-
}
|
|
18375
|
+
});
|
|
17333
18376
|
await presentHud();
|
|
17334
18377
|
}
|
|
17335
18378
|
throw error;
|
|
@@ -17373,7 +18416,37 @@ const tryAppendReviewGateNotRequiredTraceEvent = ({ identity, proof, repoRoot })
|
|
|
17373
18416
|
}
|
|
17374
18417
|
};
|
|
17375
18418
|
//#endregion
|
|
17376
|
-
//#region src/review-
|
|
18419
|
+
//#region src/review-fault-injection.ts
|
|
18420
|
+
const REVIEW_FAULT_ENV = "FACTORY_REVIEW_FAULT";
|
|
18421
|
+
const REVIEW_FAULT_POINTS = [
|
|
18422
|
+
"after-retain",
|
|
18423
|
+
"proof-rename",
|
|
18424
|
+
"after-proof-write"
|
|
18425
|
+
];
|
|
18426
|
+
var InjectedReviewFaultError = class extends Error {
|
|
18427
|
+
point;
|
|
18428
|
+
constructor(point) {
|
|
18429
|
+
super(`injected fault at ${point} (${REVIEW_FAULT_ENV})`);
|
|
18430
|
+
this.name = "InjectedReviewFaultError";
|
|
18431
|
+
this.point = point;
|
|
18432
|
+
}
|
|
18433
|
+
};
|
|
18434
|
+
const isFaultPoint = (value) => REVIEW_FAULT_POINTS.includes(value);
|
|
18435
|
+
/**
|
|
18436
|
+
* The fault point this run injects, or undefined. Throws when the variable
|
|
18437
|
+
* is set but injection is not allowed, or names an unknown point.
|
|
18438
|
+
*/
|
|
18439
|
+
const resolveReviewFault = ({ allowFaultInjection = false, env = process.env } = {}) => {
|
|
18440
|
+
const requested = env[REVIEW_FAULT_ENV];
|
|
18441
|
+
if (requested === void 0 || requested === "") return;
|
|
18442
|
+
if (env.NODE_ENV !== "test" && !allowFaultInjection) throw new Error(`${REVIEW_FAULT_ENV} is set (${requested}) but fault injection is test-only: unset it, or pass --allow-fault-injection to run the recoverable-recording demonstration deliberately.`);
|
|
18443
|
+
if (!isFaultPoint(requested)) throw new Error(`${REVIEW_FAULT_ENV} names an unknown fault point "${requested}"; known points: ${REVIEW_FAULT_POINTS.join(", ")}.`);
|
|
18444
|
+
return requested;
|
|
18445
|
+
};
|
|
18446
|
+
/** Throw the injected fault when this is its point. */
|
|
18447
|
+
const faultAt = (injected, point) => {
|
|
18448
|
+
if (injected === point) throw new InjectedReviewFaultError(point);
|
|
18449
|
+
};
|
|
17377
18450
|
const nonBlankString = z.string().trim().min(1);
|
|
17378
18451
|
const reviewFindingSchema = z.object({
|
|
17379
18452
|
blockingAfterCap: z.boolean().optional(),
|
|
@@ -17413,13 +18486,22 @@ const findingsReviewSchema = z.object({
|
|
|
17413
18486
|
path: ["findings"]
|
|
17414
18487
|
});
|
|
17415
18488
|
});
|
|
18489
|
+
const reviewResolutionSchema = z.object({
|
|
18490
|
+
reference: nonBlankString,
|
|
18491
|
+
target: z.string().regex(OCCURRENCE_ADDRESS_PATTERN)
|
|
18492
|
+
}).strict();
|
|
17416
18493
|
const reviewFindingsFileSchema = z.object({
|
|
17417
18494
|
model: nonBlankString,
|
|
17418
18495
|
patchId: nonBlankString,
|
|
17419
18496
|
producer: nonBlankString.optional(),
|
|
18497
|
+
resolutions: z.array(reviewResolutionSchema).optional(),
|
|
17420
18498
|
reviews: z.array(findingsReviewSchema),
|
|
17421
18499
|
rung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
|
|
17422
|
-
schemaVersion: z.union([
|
|
18500
|
+
schemaVersion: z.union([
|
|
18501
|
+
z.literal(1),
|
|
18502
|
+
z.literal(2),
|
|
18503
|
+
z.literal(3)
|
|
18504
|
+
]),
|
|
17423
18505
|
sessionId: nonBlankString
|
|
17424
18506
|
}).strict().superRefine((file, context) => {
|
|
17425
18507
|
if (file.schemaVersion === 1) {
|
|
@@ -17436,12 +18518,12 @@ const reviewFindingsFileSchema = z.object({
|
|
|
17436
18518
|
} else {
|
|
17437
18519
|
if (file.producer === void 0) context.addIssue({
|
|
17438
18520
|
code: "custom",
|
|
17439
|
-
message:
|
|
18521
|
+
message: `schemaVersion ${file.schemaVersion} findings require producer`,
|
|
17440
18522
|
path: ["producer"]
|
|
17441
18523
|
});
|
|
17442
18524
|
if (file.rung === void 0) context.addIssue({
|
|
17443
18525
|
code: "custom",
|
|
17444
|
-
message:
|
|
18526
|
+
message: `schemaVersion ${file.schemaVersion} findings require rung`,
|
|
17445
18527
|
path: ["rung"]
|
|
17446
18528
|
});
|
|
17447
18529
|
}
|
|
@@ -17460,13 +18542,92 @@ const reviewFindingsFileSchema = z.object({
|
|
|
17460
18542
|
}
|
|
17461
18543
|
});
|
|
17462
18544
|
const validateReviewFindingsFile = (value) => reviewFindingsFileSchema.parse(value);
|
|
17463
|
-
const
|
|
17464
|
-
|
|
18545
|
+
const parseReviewFindingsBytes = (bytes, resolvedPath) => {
|
|
18546
|
+
try {
|
|
18547
|
+
return validateReviewFindingsFile(JSON.parse(bytes));
|
|
18548
|
+
} catch (error) {
|
|
18549
|
+
throw new Error(`--findings file is invalid: ${resolvedPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
18550
|
+
}
|
|
18551
|
+
};
|
|
18552
|
+
const exampleLeafValues = {
|
|
18553
|
+
blockingAfterCap: true,
|
|
18554
|
+
body: "<what is wrong, where, and how to reproduce it>",
|
|
18555
|
+
category: "correctness",
|
|
18556
|
+
citedSpan: "<the exact code text the finding is about, quoted verbatim from the diff>",
|
|
18557
|
+
file: "<repository-relative path of the file the finding is about>",
|
|
18558
|
+
issuesFlagged: 1,
|
|
18559
|
+
kind: "correctness",
|
|
18560
|
+
line: 12,
|
|
18561
|
+
model: "<your model identifier>",
|
|
18562
|
+
outcome: "failed",
|
|
18563
|
+
patchId: "<the stable patch-id from Review scope above>",
|
|
18564
|
+
prescribedAction: "<a concise, standalone statement of the required fix>",
|
|
18565
|
+
priority: "P2",
|
|
18566
|
+
producer: "<the engine and model that ran this review, for example codex-cli/gpt-5.6-sol>",
|
|
18567
|
+
reference: "<why the named prior occurrence is resolved: the commit or the verified fix>",
|
|
18568
|
+
sessionId: "<this session's id>",
|
|
18569
|
+
summary: "<one concise sentence; use \"No actionable findings.\" only when issuesFlagged is 0>",
|
|
18570
|
+
target: "c1f1",
|
|
18571
|
+
title: "<short finding title>"
|
|
18572
|
+
};
|
|
18573
|
+
const exampleForSchema = (schema, key, overrides, omit) => {
|
|
18574
|
+
if (schema instanceof z.ZodOptional) return exampleForSchema(schema.unwrap(), key, overrides, omit);
|
|
18575
|
+
if (schema instanceof z.ZodObject) return Object.fromEntries(Object.entries(schema.shape).filter(([field]) => !omit.has(field)).map(([field, fieldSchema]) => [field, exampleForSchema(fieldSchema, field, overrides, omit)]));
|
|
18576
|
+
if (schema instanceof z.ZodArray) return [exampleForSchema(schema.element, key, overrides, omit)];
|
|
18577
|
+
const value = overrides[key] ?? exampleLeafValues[key];
|
|
18578
|
+
if (value === void 0) throw new Error(`review-findings-file.ts: no example value for schema field "${key}"; add one beside the schema so the assembled prompt can print it.`);
|
|
18579
|
+
return value;
|
|
18580
|
+
};
|
|
18581
|
+
/**
|
|
18582
|
+
* The findings file the assembled prompt asks a reviewer to write, rendered as
|
|
18583
|
+
* pretty-printed JSON derived from the schema above (#1131). It is a valid
|
|
18584
|
+
* instance, not a description: one failed review with one finding carrying
|
|
18585
|
+
* every optional attribute. `waveRung` selects schemaVersion 3 with that rung,
|
|
18586
|
+
* a producer and one resolution; absent, the schemaVersion 1 shape omits all
|
|
18587
|
+
* three (resolutions are still accepted at version 1; the version-3 example
|
|
18588
|
+
* is the one that shows the field).
|
|
18589
|
+
*/
|
|
18590
|
+
const renderReviewFindingsFileExample = (waveRung) => JSON.stringify(exampleForSchema(reviewFindingsFileSchema, "file", waveRung === void 0 ? { schemaVersion: 1 } : {
|
|
18591
|
+
rung: waveRung,
|
|
18592
|
+
schemaVersion: 3
|
|
18593
|
+
}, new Set(waveRung === void 0 ? [
|
|
18594
|
+
"producer",
|
|
18595
|
+
"resolutions",
|
|
18596
|
+
"rung"
|
|
18597
|
+
] : [])), null, 2);
|
|
18598
|
+
//#endregion
|
|
18599
|
+
//#region src/review-findings-retention.ts
|
|
18600
|
+
const REVIEW_FINDINGS_RETENTION_DIR = ".factory-memory/review-findings";
|
|
18601
|
+
const REVIEW_PROOF_IMPORT_RETENTION_DIR = ".factory-memory/review-proof-imports";
|
|
18602
|
+
const retainReviewBytes = ({ bytes, cwd, directory: relativeDirectory = REVIEW_FINDINGS_RETENTION_DIR }) => {
|
|
18603
|
+
const digest = sha256Hex(bytes);
|
|
18604
|
+
const directory = path.resolve(cwd, relativeDirectory);
|
|
18605
|
+
const target = path.join(directory, `${digest}.json`);
|
|
18606
|
+
mkdirSync(directory, { recursive: true });
|
|
18607
|
+
let existing;
|
|
18608
|
+
try {
|
|
18609
|
+
existing = readFileSync(target, "utf-8");
|
|
18610
|
+
} catch {
|
|
18611
|
+
existing = void 0;
|
|
18612
|
+
}
|
|
18613
|
+
if (existing === bytes) return {
|
|
18614
|
+
digest,
|
|
18615
|
+
path: target
|
|
18616
|
+
};
|
|
18617
|
+
const temp = path.join(directory, `.${digest}.${process.pid}.tmp`);
|
|
17465
18618
|
try {
|
|
17466
|
-
|
|
18619
|
+
writeFileSync(temp, bytes);
|
|
18620
|
+
renameSync(temp, target);
|
|
17467
18621
|
} catch (error) {
|
|
17468
|
-
|
|
18622
|
+
try {
|
|
18623
|
+
unlinkSync(temp);
|
|
18624
|
+
} catch {}
|
|
18625
|
+
throw error;
|
|
17469
18626
|
}
|
|
18627
|
+
return {
|
|
18628
|
+
digest,
|
|
18629
|
+
path: target
|
|
18630
|
+
};
|
|
17470
18631
|
};
|
|
17471
18632
|
//#endregion
|
|
17472
18633
|
//#region src/issue-body-sections.ts
|
|
@@ -17578,8 +18739,7 @@ const tryAppendReviewLadderStageTraceEvent = (input) => {
|
|
|
17578
18739
|
const isReviewLineageContinuous = (prior, current, isAncestor) => prior.base === current.base && (prior.headSha === current.headSha || isAncestor(prior.headSha, current.headSha));
|
|
17579
18740
|
//#endregion
|
|
17580
18741
|
//#region src/review-ladder-proof.ts
|
|
17581
|
-
const priorLadderStateFor = ({ base,
|
|
17582
|
-
const prior = readOptionalProofFor(PR_REVIEW_PROOF_DESCRIPTOR, cwd, output);
|
|
18742
|
+
const priorLadderStateFor = ({ base, headSha, isAncestor, prior }) => {
|
|
17583
18743
|
if (!prior?.ladder) return;
|
|
17584
18744
|
return isReviewLineageContinuous({
|
|
17585
18745
|
base: prior.base,
|
|
@@ -17589,76 +18749,76 @@ const priorLadderStateFor = ({ base, cwd, headSha, isAncestor, output }) => {
|
|
|
17589
18749
|
headSha
|
|
17590
18750
|
}, isAncestor) ? prior.ladder : void 0;
|
|
17591
18751
|
};
|
|
17592
|
-
const
|
|
17593
|
-
|
|
17594
|
-
const unmatched = declarations.filter((declaration) => !priorLedger.some((entry) => declarationMatchesEntry(entry, declaration.finding)) && !currentFindings.some((finding) => declarationMatchesEntry(finding, declaration.finding)));
|
|
17595
|
-
if (unmatched.length > 0) {
|
|
17596
|
-
const unmatchedKeys = unmatched.map((declaration) => normalizedDispositionKey(declaration.finding)).join("; ");
|
|
17597
|
-
const availableTargets = [...priorLedger.map((entry) => normalizedDispositionKey(entry)), ...currentFindings.map((finding) => normalizedDispositionKey(finding))];
|
|
17598
|
-
const availableKeys = availableTargets.length === 0 ? "prior ledger is empty" : availableTargets.join("; ");
|
|
17599
|
-
throw new Error(`--dispositions entries match no ledger finding. Unmatched normalized declaration keys: ${unmatchedKeys}. Available normalized ledger keys: ${availableKeys}. Declarations are keyed by normalized title plus file; check both against the persisted ladder ledger.`);
|
|
17600
|
-
}
|
|
17601
|
-
};
|
|
17602
|
-
const declaredDispositionsFor = (dispositionsPath, cwd) => {
|
|
17603
|
-
if (!dispositionsPath) return [];
|
|
18752
|
+
const readDeclaredDispositions = (dispositionsPath, cwd) => {
|
|
18753
|
+
if (!dispositionsPath) return;
|
|
17604
18754
|
const resolved = path.resolve(cwd, dispositionsPath);
|
|
17605
|
-
const
|
|
18755
|
+
const bytes = readFileSync(resolved, "utf-8");
|
|
18756
|
+
let parsed;
|
|
18757
|
+
try {
|
|
18758
|
+
parsed = dispositionDeclarationsFileSchema.safeParse(JSON.parse(bytes));
|
|
18759
|
+
} catch (error) {
|
|
18760
|
+
throw new Error(`--dispositions file is invalid: ${resolved}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
18761
|
+
}
|
|
17606
18762
|
if (!parsed.success) throw new Error(`--dispositions file is invalid: ${resolved}: ${parsed.error.issues.map((issue) => issue.message).join("; ")}`);
|
|
17607
|
-
return
|
|
18763
|
+
return {
|
|
18764
|
+
bytes,
|
|
18765
|
+
declarations: parsed.data
|
|
18766
|
+
};
|
|
17608
18767
|
};
|
|
17609
18768
|
const priorLedgerFor = (priorLadder) => priorLadder ? ladderLedgerFor(priorLadder) : [];
|
|
17610
|
-
|
|
18769
|
+
const foldWith = (priorCycles, cycle) => {
|
|
18770
|
+
const cycles = [...priorCycles, cycle];
|
|
18771
|
+
return foldLedger(cycles, ladderCycleRefsFor(cycles)).occurrences;
|
|
18772
|
+
};
|
|
18773
|
+
function ladderStateForRun({ historical, identity, model, policy, priorLadder, recording, runs, sessionAttribution }) {
|
|
17611
18774
|
const findings = runs.flatMap((run) => run.review.findings.map((finding) => toLadderFindingReport(finding)));
|
|
17612
18775
|
const priorCycles = priorLadder?.cycles ?? [];
|
|
17613
|
-
const
|
|
17614
|
-
const
|
|
18776
|
+
const cycleIndex = priorCycles.length + 1;
|
|
18777
|
+
const declaredCycle = ladderCycleRefsFor([...priorCycles, { stage: "gate" }]).at(-1)?.cycle ?? cycleIndex;
|
|
18778
|
+
const liveCycleCount = priorCycles.filter((cycle) => cycle.historical !== true).length + (historical === true ? 0 : 1);
|
|
17615
18779
|
const session = {
|
|
17616
|
-
...resumedSession === void 0 ? {} : { resumedSession },
|
|
17617
18780
|
...sessionAttribution?.sessionId ? { sessionId: sessionAttribution.sessionId } : {},
|
|
17618
18781
|
...sessionAttribution?.runtime ? { runtime: sessionAttribution.runtime } : {}
|
|
17619
18782
|
};
|
|
17620
|
-
const
|
|
17621
|
-
const
|
|
17622
|
-
|
|
17623
|
-
|
|
17624
|
-
|
|
17625
|
-
|
|
17626
|
-
|
|
17627
|
-
|
|
17628
|
-
|
|
17629
|
-
|
|
17630
|
-
|
|
17631
|
-
|
|
17632
|
-
|
|
17633
|
-
|
|
17634
|
-
|
|
17635
|
-
|
|
17636
|
-
|
|
17637
|
-
|
|
17638
|
-
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
|
|
17642
|
-
|
|
17643
|
-
|
|
17644
|
-
})) : []
|
|
17645
|
-
],
|
|
17646
|
-
findings,
|
|
17647
|
-
...historical === true ? { historical: true } : {},
|
|
17648
|
-
identity,
|
|
17649
|
-
model,
|
|
17650
|
-
stage: "gate",
|
|
17651
|
-
...Object.keys(session).length > 0 ? { session } : {},
|
|
17652
|
-
...resumedSession ? { correlatedRerun: true } : {}
|
|
18783
|
+
const cycleWith = (declarations) => {
|
|
18784
|
+
const record = {
|
|
18785
|
+
appliedTo: recording.appliedTo,
|
|
18786
|
+
base: recording.base,
|
|
18787
|
+
declarations,
|
|
18788
|
+
...recording.dispositionsDigest === void 0 ? {} : { dispositionsDigest: recording.dispositionsDigest },
|
|
18789
|
+
findingsDigest: recording.findingsDigest,
|
|
18790
|
+
occurrences: occurrencesForCycle({
|
|
18791
|
+
cycleIndex,
|
|
18792
|
+
declaredCycle,
|
|
18793
|
+
findings,
|
|
18794
|
+
priorCycles
|
|
18795
|
+
}),
|
|
18796
|
+
recordingId: recording.recordingId
|
|
18797
|
+
};
|
|
18798
|
+
return {
|
|
18799
|
+
findings,
|
|
18800
|
+
...historical === true ? { historical: true } : {},
|
|
18801
|
+
identity,
|
|
18802
|
+
model,
|
|
18803
|
+
recording: record,
|
|
18804
|
+
stage: "gate",
|
|
18805
|
+
...Object.keys(session).length > 0 ? { session } : {}
|
|
18806
|
+
};
|
|
17653
18807
|
};
|
|
18808
|
+
const explicit = recording.declarations;
|
|
18809
|
+
const repeats = identityRepeatDeclarations(foldWith(priorCycles, cycleWith(explicit)), cycleIndex);
|
|
18810
|
+
const floor = severityFloorDeclarations(foldWith(priorCycles, cycleWith([...explicit, ...repeats])), liveCycleCount);
|
|
18811
|
+
const currentCycle = cycleWith([
|
|
18812
|
+
...explicit,
|
|
18813
|
+
...repeats,
|
|
18814
|
+
...floor
|
|
18815
|
+
]);
|
|
17654
18816
|
const cycles = [...priorCycles, currentCycle];
|
|
17655
|
-
const evaluation = evaluateReviewLadder({
|
|
17656
|
-
cycles,
|
|
17657
|
-
policy
|
|
17658
|
-
});
|
|
17659
|
-
if (evaluation.unmatchedDispositions.length > 0) throw new Error(`Dispositions were dropped at fold time — they matched no flagged finding in the ladder history: ${evaluation.unmatchedDispositions.map((declaration) => declaration.finding.title).join("; ")}. The fold and the pre-run --dispositions validation disagree; nothing was persisted.`);
|
|
17660
18817
|
return {
|
|
17661
|
-
evaluation
|
|
18818
|
+
evaluation: evaluateReviewLadder({
|
|
18819
|
+
cycles,
|
|
18820
|
+
policy
|
|
18821
|
+
}),
|
|
17662
18822
|
ladder: { cycles }
|
|
17663
18823
|
};
|
|
17664
18824
|
}
|
|
@@ -17680,7 +18840,7 @@ function emitLadderStageTrace({ cwd, evaluation, headSha, issue, patchId, profil
|
|
|
17680
18840
|
//#endregion
|
|
17681
18841
|
//#region src/review-contract.ts
|
|
17682
18842
|
const SESSION_ID_RULE = `"sessionId" is this review session's own id. It must differ from the authoring session id the candidate was verified under (FACTORY_SESSION_ID): pr:review --findings refuses a findings file whose sessionId equals the authoring session.`;
|
|
17683
|
-
const authorityRule = (waveRung) => waveRung === void 0 ? `Emit schemaVersion 1 and omit "producer" and "rung": this candidate resolves no boundary-wave review rung. If your dispatch instructions do name a rung, emit schemaVersion
|
|
18843
|
+
const authorityRule = (waveRung) => waveRung === void 0 ? `Emit schemaVersion 1 and omit "producer" and "rung": this candidate resolves no boundary-wave review rung. If your dispatch instructions do name a rung, emit schemaVersion 3 with that rung and with "producer" instead. "resolutions" is accepted at every schemaVersion: when the prior-review ledger below shows an open occurrence you verified fixed, add "resolutions": [{"target": "<its address, for example c1f1>", "reference": "<the commit or the verified fix>"}]; silence is not resolution, and an occurrence you do not name stays open.` : `This candidate belongs to a boundary wave that demands the ${waveRung} review rung, so schemaVersion 3 is required. Record "rung": "${waveRung}" exactly as shown, and set "producer" to the engine and model that ran this review (for example "codex-cli/gpt-5.6-sol"). Findings without both fields are recorded without authority, and pr:publish then refuses with "no passing review at that authority". "resolutions" is optional: when the prior-review ledger below shows an open occurrence you verified fixed, name it by its address (for example "c1f1") with a reference; silence is not resolution, and an occurrence you do not name stays open.`;
|
|
17684
18844
|
/**
|
|
17685
18845
|
* The findings-file instructions for one candidate.
|
|
17686
18846
|
*
|
|
@@ -17688,23 +18848,9 @@ const authorityRule = (waveRung) => waveRung === void 0 ? `Emit schemaVersion 1
|
|
|
17688
18848
|
* resolves one. It selects the findings schema version the gate will accept.
|
|
17689
18849
|
*/
|
|
17690
18850
|
const reviewFooterInstructions = (waveRung) => {
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
"schemaVersion": ${schemaVersion},
|
|
17695
|
-
${waveRung === void 0 ? "" : ` "producer": "<the engine and model that ran this review, for example codex-cli/gpt-5.6-sol>",\n "rung": "${waveRung}",\n`} "model": "<your model identifier>",
|
|
17696
|
-
"sessionId": "<this session's id>",
|
|
17697
|
-
"patchId": "<the stable patch-id from Review scope above>",
|
|
17698
|
-
"reviews": [
|
|
17699
|
-
{
|
|
17700
|
-
"kind": "correctness" or "security" (this review's mode, matching the mode-floor instructions below),
|
|
17701
|
-
"outcome": "passed" or "failed",
|
|
17702
|
-
"issuesFlagged": an integer count of actionable findings (must equal findings.length),
|
|
17703
|
-
"summary": one concise sentence; use "No actionable findings." only when issuesFlagged is 0,
|
|
17704
|
-
"findings": [] when issuesFlagged is 0, otherwise an array of objects with title, body, and optional file, line, priority, category, blockingAfterCap, citedSpan, prescribedAction, and supersedes fields
|
|
17705
|
-
}
|
|
17706
|
-
]
|
|
17707
|
-
}
|
|
18851
|
+
return `Do not print a machine-readable footer in your chat response. Write your findings to the typed findings file at the path given in your dispatch/task instructions (the file pr:review reads via --findings <path>); that file is the verdict. The file must be valid JSON matching review-findings-file.ts's schema (schemaVersion ${waveRung === void 0 ? 1 : 3}). This example is printed from that schema and validates as written:
|
|
18852
|
+
${renderReviewFindingsFileExample(waveRung)}
|
|
18853
|
+
Field rules the validator enforces: "kind" is this review's mode ("correctness" or "security", matching the mode-floor instructions below); "outcome" is "passed" or "failed"; "issuesFlagged" must equal findings.length, so a passed review has "issuesFlagged": 0, "findings": [] and "summary": "No actionable findings."; a failed review has at least one finding. Each finding requires title and body; file, line, priority, category, blockingAfterCap, citedSpan, prescribedAction, and supersedes are optional. Do not add fields the example does not show.
|
|
17708
18854
|
${authorityRule(waveRung)}
|
|
17709
18855
|
${SESSION_ID_RULE}
|
|
17710
18856
|
Use category "correctness", "safety", "coordination", or "maintainability"; set blockingAfterCap true for correctness, safety, or coordination findings. citedSpan must be the exact code text the finding is about, quoted verbatim from the diff (identity is derived from this, not from the title, so quote precisely). prescribedAction must be a concise, standalone statement of the required fix (identity is derived from this text too).`;
|
|
@@ -17785,21 +18931,26 @@ const issueFocusSegment = (focus) => ({
|
|
|
17785
18931
|
text: `Per-issue review focus (source: issue #${focus.issueNumber} "${REVIEW_FOCUS_SECTION}" section, appended verbatim):
|
|
17786
18932
|
${focus.text}`
|
|
17787
18933
|
});
|
|
17788
|
-
const
|
|
17789
|
-
const location =
|
|
17790
|
-
const
|
|
17791
|
-
|
|
17792
|
-
}
|
|
18934
|
+
const priorOccurrenceLine = (entry, occurrence) => {
|
|
18935
|
+
const location = occurrence.finding.file ? ` (${occurrence.finding.file})` : "";
|
|
18936
|
+
const repeat = occurrence.root === occurrence.address ? "" : ` — repeats ${occurrence.root}`;
|
|
18937
|
+
const why = occurrence.reference ?? occurrence.rule;
|
|
18938
|
+
const reference = why === void 0 ? "" : ` — ${why}`;
|
|
18939
|
+
const disposition = occurrence.disposition === "open" ? "open, unadjudicated" : `${occurrence.disposition}, ${occurrence.authority ?? "recorded"}`;
|
|
18940
|
+
const title = occurrence.finding.title.trim() || entry.title;
|
|
18941
|
+
return `- ${occurrence.address} [${disposition}] "${title}"${location}${repeat}${reference}`;
|
|
18942
|
+
};
|
|
18943
|
+
const priorLedgerLines = (entries) => entries.flatMap((entry) => entry.occurrences.map((occurrence) => priorOccurrenceLine(entry, occurrence))).join("\n");
|
|
17793
18944
|
const priorLedgerSegment = (entries) => ({
|
|
17794
18945
|
section: {
|
|
17795
18946
|
provenance: "prior-review-ledger",
|
|
17796
18947
|
source: "persisted pr:review disposition ledger (prior cycles, #233)",
|
|
17797
|
-
text: entries
|
|
18948
|
+
text: priorLedgerLines(entries)
|
|
17798
18949
|
},
|
|
17799
|
-
text: `Prior review ledger (source: persisted pr:review disposition ledger from earlier cycles on this PR):
|
|
17800
|
-
${entries
|
|
18950
|
+
text: `Prior review ledger (source: persisted pr:review disposition ledger from earlier cycles on this PR; one line per recorded occurrence, by address):
|
|
18951
|
+
${priorLedgerLines(entries)}
|
|
17801
18952
|
|
|
17802
|
-
Open
|
|
18953
|
+
Open occurrences above are unadjudicated: each still needs a disposition. If your review verifies that an open occurrence is resolved at this patch, name it in the findings file's resolutions by address with the reason (resolutions are accepted at every findings schemaVersion); an occurrence you do not name stays open, because silence is never resolution. Do not re-raise any already-adjudicated occurrence (waived, filed as a follow-up, policy-accepted, or fixed in this thread) under a new title. If a finding restates a prior occurrence shown here, set supersedes to {"title": "<exact prior title>", "file": "<its file>"} instead of minting a new title for the same theme.`
|
|
17803
18954
|
});
|
|
17804
18955
|
const assembleReviewPrompt = ({ base, changedFiles, cwd = process.cwd(), headSha, issueFocus, kind, patchId, priorLedgerEntries, profile, waveRung }, dependencies = {}) => {
|
|
17805
18956
|
const priorLedger = priorLedgerEntries ?? [];
|
|
@@ -17836,6 +18987,20 @@ const assembleReviewPrompt = ({ base, changedFiles, cwd = process.cwd(), headSha
|
|
|
17836
18987
|
};
|
|
17837
18988
|
};
|
|
17838
18989
|
//#endregion
|
|
18990
|
+
//#region src/review-recording-id.ts
|
|
18991
|
+
const lengthPrefixed = (field) => `${Buffer.byteLength(field, "utf-8")}:${field}`;
|
|
18992
|
+
const reviewRecordingId = ({ baseSha, dispositionsBytes, findingsBytes, historical, model, patchId, producer, rung, sessionId }) => sha256Hex([
|
|
18993
|
+
patchId,
|
|
18994
|
+
baseSha,
|
|
18995
|
+
sessionId,
|
|
18996
|
+
producer ?? "",
|
|
18997
|
+
rung ?? "",
|
|
18998
|
+
model,
|
|
18999
|
+
historical ? "1" : "0",
|
|
19000
|
+
sha256Hex(findingsBytes),
|
|
19001
|
+
sha256Hex(dispositionsBytes ?? "")
|
|
19002
|
+
].map((field) => lengthPrefixed(field)).join(""));
|
|
19003
|
+
//#endregion
|
|
17839
19004
|
//#region src/pr-review.ts
|
|
17840
19005
|
const DEFAULT_ASSEMBLED_REVIEW_PROMPT_PATH = ".factory-memory/pr-review-prompt.json";
|
|
17841
19006
|
const MISSING_ASSEMBLED_PROMPT_NOTICE = "pr:review notice: assembled review prompt not found at .factory-memory/pr-review-prompt.json; accepting findings without promptSections.";
|
|
@@ -17966,7 +19131,7 @@ const emptyProof = (input) => ({
|
|
|
17966
19131
|
reviewCycle: input.cycle,
|
|
17967
19132
|
...input.reviewRequirement ? { reviewRequirement: input.reviewRequirement } : {},
|
|
17968
19133
|
reviews: [],
|
|
17969
|
-
schemaVersion:
|
|
19134
|
+
schemaVersion: 3
|
|
17970
19135
|
});
|
|
17971
19136
|
const assertIndependentSession = ({ headSha, patchId, reviewSessionId, verifyProof }) => {
|
|
17972
19137
|
if (reviewSessionId === "unknown") throw new Error("Review independence cannot be confirmed: findings sessionId is unknown.");
|
|
@@ -18021,7 +19186,12 @@ const reviewResults = (findingsFile, kinds, startedAt, endedAt) => {
|
|
|
18021
19186
|
});
|
|
18022
19187
|
};
|
|
18023
19188
|
const assertReviewOutcomes = (proof, policy, cycle, maxCycles) => {
|
|
18024
|
-
|
|
19189
|
+
const acceptances = new Set(proof.reviews.map((review) => resolveReviewAcceptance(proof, review, policy)));
|
|
19190
|
+
if (acceptances.has("recovery-required")) {
|
|
19191
|
+
for (const line of describeRecoveryRequired(proof.recoveryRequired ?? [])) console.error(line);
|
|
19192
|
+
throw new Error("pr:review recorded the cycle, but the proof requires recovery of a schema-2 declaration before it can admit.");
|
|
19193
|
+
}
|
|
19194
|
+
if (!acceptances.has("blocked")) return;
|
|
18025
19195
|
const state = reviewCycleStateFor(proof, policy);
|
|
18026
19196
|
if (proof.reviews.some((review) => review.outcome === "passed")) console.error(`Stored review outcome is passed, but a re-fold of persisted cycles yields ${state.autoBlockingFindings} auto-blocking finding(s) and ${state.openFindings} open finding(s). The stored pass and the ladder disagree.`);
|
|
18027
19197
|
else if (cycle >= maxCycles) console.error(`Review cycle limit reached (${cycle}/${maxCycles}). Stop and request human direction before rerunning.`);
|
|
@@ -18043,19 +19213,18 @@ const assembleGitDependencies = (git) => git.repositoryRoot !== void 0 && git.sh
|
|
|
18043
19213
|
repositoryRoot: git.repositoryRoot,
|
|
18044
19214
|
showFileAtRef: git.showFileAtRef
|
|
18045
19215
|
} : void 0;
|
|
18046
|
-
const writeAssembledReviewPrompt = ({ args, cwd, fetchIssueBody, files, git, headSha, kinds, output, patchId, profile, waveRung }) => {
|
|
19216
|
+
const writeAssembledReviewPrompt = ({ args, baseRef, cwd, fetchIssueBody, files, git, headSha, kinds, output, patchId, profile, waveRung }) => {
|
|
18047
19217
|
const priorLedger = priorLedgerFor(priorLadderStateFor({
|
|
18048
|
-
base:
|
|
18049
|
-
cwd,
|
|
19218
|
+
base: baseRef,
|
|
18050
19219
|
headSha,
|
|
18051
19220
|
isAncestor: (ancestor, descendant) => git.isAncestor(cwd, ancestor, descendant),
|
|
18052
|
-
output
|
|
19221
|
+
prior: readOptionalProofFor(PR_REVIEW_PROOF_DESCRIPTOR, cwd, output)
|
|
18053
19222
|
}));
|
|
18054
19223
|
const issueFocus = args.issue === void 0 ? void 0 : loadIssueFocus(args.issue, profile, fetchIssueBody);
|
|
18055
19224
|
const assembleGit = assembleGitDependencies(git);
|
|
18056
19225
|
const assembledReviews = kinds.map((kind) => {
|
|
18057
19226
|
const assembled = assembleReviewPrompt({
|
|
18058
|
-
base:
|
|
19227
|
+
base: baseRef,
|
|
18059
19228
|
changedFiles: files,
|
|
18060
19229
|
cwd,
|
|
18061
19230
|
headSha,
|
|
@@ -18129,13 +19298,13 @@ const withAssembledPromptSections = (reviews, sectionsByKind) => {
|
|
|
18129
19298
|
* this, a candidate that removed `review` from its own profile briefed the
|
|
18130
19299
|
* reviewer on correctness alone and then presented that evidence as complete.
|
|
18131
19300
|
*/
|
|
18132
|
-
const reviewPolicyIdentity = ({
|
|
19301
|
+
const reviewPolicyIdentity = ({ baseRef, changedFiles: files, cwd, dependencies, git, profile, profilePath, pullRequestLookup }) => resolveStageReviewDemand({
|
|
18133
19302
|
authority: dependencies.basePolicy ?? defaultBaseReviewPolicyAuthority(),
|
|
18134
|
-
callerBaseRef:
|
|
19303
|
+
callerBaseRef: baseRef,
|
|
18135
19304
|
candidatePolicy: profile.review,
|
|
18136
19305
|
changedFiles: files,
|
|
18137
19306
|
cwd,
|
|
18138
|
-
lookupPullRequest: dependencies.lookupPullRequest ??
|
|
19307
|
+
lookupPullRequest: dependencies.lookupPullRequest ?? (() => policyLookupFromBaseLookup(pullRequestLookup)),
|
|
18139
19308
|
profilePath: repositoryRelativeProfilePath({
|
|
18140
19309
|
cwd,
|
|
18141
19310
|
profilePath
|
|
@@ -18147,7 +19316,7 @@ const reviewPolicyIdentity = ({ args, changedFiles: files, cwd, dependencies, gi
|
|
|
18147
19316
|
* nothing extra here; `pr:ready` is the stage that refuses on it (#922).
|
|
18148
19317
|
*/
|
|
18149
19318
|
const demandedReviewKinds = (policy) => policy.status === "authoritative" ? policy.reviewModes : [];
|
|
18150
|
-
const resolveFindingsBinding = ({ args, cwd, findingsPatchId, git, headSha, patchId }) => {
|
|
19319
|
+
const resolveFindingsBinding = ({ args, base, cwd, findingsPatchId, git, headSha, patchId }) => {
|
|
18151
19320
|
const atCandidate = evidenceFresh({
|
|
18152
19321
|
candidate: {
|
|
18153
19322
|
headSha,
|
|
@@ -18162,20 +19331,19 @@ const resolveFindingsBinding = ({ args, cwd, findingsPatchId, git, headSha, patc
|
|
|
18162
19331
|
}
|
|
18163
19332
|
if (atCandidate) throw new Error(`--historical findings already match the candidate stable patch-id ${patchId}. Record them as the current cycle, without --historical.`);
|
|
18164
19333
|
const ancestors = resolveAncestorCandidatePatchIds({
|
|
18165
|
-
base:
|
|
19334
|
+
base: base.sha,
|
|
18166
19335
|
cwd,
|
|
18167
19336
|
firstParentCommits: git.firstParentCommits ?? firstParentCommits,
|
|
18168
19337
|
headSha,
|
|
18169
19338
|
stablePatchIdAtRef: git.stablePatchIdAtRef ?? defaultStablePatchIdAtRef
|
|
18170
19339
|
});
|
|
18171
19340
|
const matched = ancestors.find((ancestor) => ancestor.patchId === findingsPatchId);
|
|
18172
|
-
if (!matched) throw new Error(`Review findings patchId ${findingsPatchId} matches no commit on this branch. The candidate stable patch-id is ${patchId}, and ${ancestors.length} earlier head(s) of this branch above ${
|
|
19341
|
+
if (!matched) throw new Error(`Review findings patchId ${findingsPatchId} matches no commit on this branch. The candidate stable patch-id is ${patchId}, and ${ancestors.length} earlier head(s) of this branch above ${base.ref} (${base.sha}) were checked. A historical cycle must name a patch this branch really had; a commit a merge brought in from the base is not one of them. A cycle recorded against a different base cannot be matched here, because every patch-id is computed against ${base.ref} at ${base.sha}.`);
|
|
18173
19342
|
return {
|
|
18174
19343
|
kind: "historical",
|
|
18175
19344
|
ref: matched.ref
|
|
18176
19345
|
};
|
|
18177
19346
|
};
|
|
18178
|
-
const currentCycleDispositionTargets = (historical, findingsFile) => historical ? findingsFile.reviews.flatMap((review) => review.findings) : [];
|
|
18179
19347
|
/** The flag combinations that describe no single run. */
|
|
18180
19348
|
const assertReviewFlagsCombine = (args) => {
|
|
18181
19349
|
if (args.assemble === true && args.findings !== void 0) throw new Error("--assemble and --findings cannot be combined; they are sequential steps.");
|
|
@@ -18225,8 +19393,7 @@ const cycleRecordFor = ({ binding, cwd, findingsFile, identity, kinds, startedAt
|
|
|
18225
19393
|
* with, cannot show which policy produced it: the verdict drops, exactly as the
|
|
18226
19394
|
* other-patch case does.
|
|
18227
19395
|
*/
|
|
18228
|
-
const carriedVerdictFor = ({
|
|
18229
|
-
const prior = readOptionalProofFor(PR_REVIEW_PROOF_DESCRIPTOR, cwd, output);
|
|
19396
|
+
const carriedVerdictFor = ({ patchId, policy, prior }) => {
|
|
18230
19397
|
if (!prior || !evidenceFresh({
|
|
18231
19398
|
candidate: { patchId },
|
|
18232
19399
|
recorded: { patchId: prior.patchId },
|
|
@@ -18240,11 +19407,324 @@ const carriedVerdictFor = ({ cwd, output, patchId, policy }) => {
|
|
|
18240
19407
|
stage: "pr:review"
|
|
18241
19408
|
}) ? [] : prior.reviews;
|
|
18242
19409
|
};
|
|
19410
|
+
/**
|
|
19411
|
+
* Bind the candidate to its base before anything is measured (#1131). A pull
|
|
19412
|
+
* request's live base wins and a disagreeing --base is refused; the SHA is
|
|
19413
|
+
* what every diff is computed against, the ref is what the proof records
|
|
19414
|
+
* (review-lineage keys on it). The pull request read here also serves the
|
|
19415
|
+
* policy-authority lookup, so one GitHub read answers both.
|
|
19416
|
+
*/
|
|
19417
|
+
const bindCandidateBase = ({ args, cwd, dependencies, git }) => {
|
|
19418
|
+
const pullRequestLookup = (dependencies.lookupPullRequestBase ?? lookupLivePullRequestBase)(cwd);
|
|
19419
|
+
const base = resolveCandidateBase({
|
|
19420
|
+
cwd,
|
|
19421
|
+
dependencies: {
|
|
19422
|
+
fetchOriginRef: git.fetchOriginRef ?? fetchOriginRef,
|
|
19423
|
+
liveBranchTipSha: dependencies.liveBranchTipSha ?? liveBranchTipSha,
|
|
19424
|
+
lookupPullRequestBase: () => pullRequestLookup,
|
|
19425
|
+
resolveCommitSha: git.resolveCommitSha ?? resolveCommitSha
|
|
19426
|
+
},
|
|
19427
|
+
requestedBase: args.base
|
|
19428
|
+
});
|
|
19429
|
+
console.log(`pr:review ${describeCandidateBase(base)}`);
|
|
19430
|
+
return {
|
|
19431
|
+
base,
|
|
19432
|
+
pullRequestLookup
|
|
19433
|
+
};
|
|
19434
|
+
};
|
|
19435
|
+
/**
|
|
19436
|
+
* The line that states a review cycle was recorded (#1131). Printed before
|
|
19437
|
+
* admission is judged, so "recorded" and "refused" are reported as two facts.
|
|
19438
|
+
*/
|
|
19439
|
+
const recordedReviewProofLine = ({ cycle, headSha, historical, output, patchId, recordingId }) => `pr:review recorded ${historical ? "historical " : ""}cycle ${cycle} findings for patch ${patchId} at ${headSha}: proof written to ${output} (recording ${recordingId}).`;
|
|
19440
|
+
/**
|
|
19441
|
+
* The line an identical retry prints (#1131, decision 6): the recording is
|
|
19442
|
+
* already on the ladder, so nothing is written and no cycle is consumed.
|
|
19443
|
+
*/
|
|
19444
|
+
const ALREADY_RECORDED_PREFIX = "already-recorded";
|
|
19445
|
+
const alreadyRecordedLine = ({ output, recordingId }) => `${ALREADY_RECORDED_PREFIX}: recording ${recordingId} is already on the ladder in ${output}; nothing written, no cycle consumed.`;
|
|
19446
|
+
/**
|
|
19447
|
+
* The findings file, read once as bytes and parsed from those bytes (#1131):
|
|
19448
|
+
* the recording identity digests, retains and records exactly what was
|
|
19449
|
+
* validated.
|
|
19450
|
+
*/
|
|
19451
|
+
const readFindingsInput = (findingsPath, cwd) => {
|
|
19452
|
+
const resolved = path.resolve(cwd, findingsPath);
|
|
19453
|
+
const bytes = readFileSync(resolved, "utf-8");
|
|
19454
|
+
return {
|
|
19455
|
+
bytes,
|
|
19456
|
+
file: parseReviewFindingsBytes(bytes, resolved)
|
|
19457
|
+
};
|
|
19458
|
+
};
|
|
19459
|
+
/**
|
|
19460
|
+
* The proof already on disk, parsed from the bytes whose digest this
|
|
19461
|
+
* recording is applied to. A file that does not parse is refused: recording
|
|
19462
|
+
* over it would replace history this run cannot read. A schema-2 proof is
|
|
19463
|
+
* converted here by the bounded import (#1068); its original bytes are
|
|
19464
|
+
* retained before the converted proof is written.
|
|
19465
|
+
*/
|
|
19466
|
+
const parsePriorProof = (bytes, output) => {
|
|
19467
|
+
try {
|
|
19468
|
+
return parseReviewProofBytes(bytes);
|
|
19469
|
+
} catch (error) {
|
|
19470
|
+
throw new Error(`pr:review refuses to record over an unreadable proof at ${output}: ${error instanceof Error ? error.message : String(error)} Move the file aside to start fresh ladder history, or re-record once it can be read.`, { cause: error });
|
|
19471
|
+
}
|
|
19472
|
+
};
|
|
19473
|
+
/**
|
|
19474
|
+
* The --output copy is optional (decision 6): a failure is reported on its
|
|
19475
|
+
* own line and never unrecords the canonical proof.
|
|
19476
|
+
*/
|
|
19477
|
+
const writeOptionalProofCopy = (proof, additional, canonical) => {
|
|
19478
|
+
if (additional === void 0 || additional === canonical) return;
|
|
19479
|
+
try {
|
|
19480
|
+
writeProofJson(proof, additional);
|
|
19481
|
+
} catch (error) {
|
|
19482
|
+
console.error(`pr:review optional copy not written to ${additional}: ${error instanceof Error ? error.message : String(error)}. The canonical proof at ${canonical} is recorded.`);
|
|
19483
|
+
}
|
|
19484
|
+
};
|
|
19485
|
+
/**
|
|
19486
|
+
* A proof file system whose rename raises the injected fault (#1131): the
|
|
19487
|
+
* temp file is written and fsynced, the canonical path is never replaced.
|
|
19488
|
+
*/
|
|
19489
|
+
const faultingProofFileSystem = (point) => ({
|
|
19490
|
+
closeSync: nodeFs.closeSync,
|
|
19491
|
+
fsyncSync: nodeFs.fsyncSync,
|
|
19492
|
+
mkdirSync: nodeFs.mkdirSync,
|
|
19493
|
+
openSync: nodeFs.openSync,
|
|
19494
|
+
readFileSync: nodeFs.readFileSync,
|
|
19495
|
+
renameSync: () => {
|
|
19496
|
+
throw new InjectedReviewFaultError(point);
|
|
19497
|
+
},
|
|
19498
|
+
statSync: nodeFs.statSync,
|
|
19499
|
+
unlinkSync: nodeFs.unlinkSync,
|
|
19500
|
+
writeFileSync: nodeFs.writeFileSync
|
|
19501
|
+
});
|
|
19502
|
+
/**
|
|
19503
|
+
* The cycle number to report on the recorded-proof line (#1131). This is the
|
|
19504
|
+
* ladder's own assignment — the same value `eventFor` in review-ladder.ts
|
|
19505
|
+
* stamps onto the trace event's `cycle` field — never a recomputation of it.
|
|
19506
|
+
* A requested `--cycle` can under-report when a prior ladder carries forward
|
|
19507
|
+
* onto this run, so it is only the fallback for the (practically unreachable)
|
|
19508
|
+
* case where a cycle was just appended but the evaluator emitted no event.
|
|
19509
|
+
*/
|
|
19510
|
+
const reportedLadderCycle = (evaluation, requestedCycle) => evaluation.event?.cycle ?? requestedCycle;
|
|
19511
|
+
/**
|
|
19512
|
+
* A schema-2 proof the bounded import just converted (#1068): its original
|
|
19513
|
+
* bytes are retained, content-addressed under the digest the converted proof
|
|
19514
|
+
* names, before that proof replaces them. Nothing is retained when the
|
|
19515
|
+
* lineage broke and the imported history is not being carried forward.
|
|
19516
|
+
*/
|
|
19517
|
+
const retainImportedProofBytes = ({ cwd, prior, priorLadder, proofState }) => {
|
|
19518
|
+
if (priorLadder === void 0 || !prior?.importedFrom || !proofState.present) return;
|
|
19519
|
+
retainReviewBytes({
|
|
19520
|
+
bytes: proofState.bytes,
|
|
19521
|
+
cwd,
|
|
19522
|
+
directory: REVIEW_PROOF_IMPORT_RETENTION_DIR
|
|
19523
|
+
});
|
|
19524
|
+
};
|
|
19525
|
+
/**
|
|
19526
|
+
* Import provenance rides with the imported history (#1068). Recovery items
|
|
19527
|
+
* ride too, until a recording carries an explicit --dispositions file: that
|
|
19528
|
+
* file is the operator's act after reading the candidate addresses, whatever
|
|
19529
|
+
* it declares.
|
|
19530
|
+
*/
|
|
19531
|
+
const importedHistoryFields = ({ declared, prior, priorLadder }) => {
|
|
19532
|
+
if (priorLadder === void 0 || !prior?.importedFrom) return {};
|
|
19533
|
+
const targets = new Set(declared.map((record) => record.target));
|
|
19534
|
+
const remaining = (prior.recoveryRequired ?? []).filter((item) => !item.candidates.some((candidate) => targets.has(candidate)));
|
|
19535
|
+
return {
|
|
19536
|
+
importedFrom: prior.importedFrom,
|
|
19537
|
+
recoveryRequired: remaining
|
|
19538
|
+
};
|
|
19539
|
+
};
|
|
19540
|
+
/**
|
|
19541
|
+
* Record one review cycle (#1131). The recording is a separate fact from
|
|
19542
|
+
* admission: this returns what was recorded (or that it already was), and
|
|
19543
|
+
* the caller judges admission afterwards.
|
|
19544
|
+
*
|
|
19545
|
+
* Order: read the proof bytes once (their digest is the state the recording
|
|
19546
|
+
* is applied to; an unreadable proof is a refusal) → compute the recording
|
|
19547
|
+
* identity and answer an identical retry with `already-recorded`, writing
|
|
19548
|
+
* nothing → translate declarations → retain the reviewer's bytes → fold →
|
|
19549
|
+
* compare-and-swap write → recorded line → best-effort copies.
|
|
19550
|
+
*/
|
|
19551
|
+
const recordReviewCycle = ({ additionalOutput, args, base, cwd, dependencies, files, findingsInput, git, headSha, kinds, ladderPolicy, maxCycles, output, patchId, policy, profile, startedAt, verifyProof }) => {
|
|
19552
|
+
const findingsFile = findingsInput.file;
|
|
19553
|
+
const fault = resolveReviewFault({ allowFaultInjection: args.allowFaultInjection });
|
|
19554
|
+
const binding = resolveFindingsBinding({
|
|
19555
|
+
args,
|
|
19556
|
+
base,
|
|
19557
|
+
cwd,
|
|
19558
|
+
findingsPatchId: findingsFile.patchId,
|
|
19559
|
+
git,
|
|
19560
|
+
headSha,
|
|
19561
|
+
patchId
|
|
19562
|
+
});
|
|
19563
|
+
assertIndependentSession({
|
|
19564
|
+
headSha,
|
|
19565
|
+
patchId,
|
|
19566
|
+
reviewSessionId: findingsFile.sessionId,
|
|
19567
|
+
verifyProof
|
|
19568
|
+
});
|
|
19569
|
+
const proofState = readProofFileState(output);
|
|
19570
|
+
const prior = proofState.present ? parsePriorProof(proofState.bytes, output) : void 0;
|
|
19571
|
+
const historical = binding.kind === "historical";
|
|
19572
|
+
const dispositionsInput = readDeclaredDispositions(args.dispositions, cwd);
|
|
19573
|
+
const { identity, reviews } = cycleRecordFor({
|
|
19574
|
+
binding,
|
|
19575
|
+
cwd,
|
|
19576
|
+
findingsFile,
|
|
19577
|
+
identity: {
|
|
19578
|
+
headSha,
|
|
19579
|
+
patchId
|
|
19580
|
+
},
|
|
19581
|
+
kinds,
|
|
19582
|
+
startedAt
|
|
19583
|
+
});
|
|
19584
|
+
const recordingId = reviewRecordingId({
|
|
19585
|
+
baseSha: base.sha,
|
|
19586
|
+
dispositionsBytes: dispositionsInput?.bytes,
|
|
19587
|
+
findingsBytes: findingsInput.bytes,
|
|
19588
|
+
historical,
|
|
19589
|
+
model: findingsFile.model,
|
|
19590
|
+
patchId: identity.patchId,
|
|
19591
|
+
producer: findingsFile.producer,
|
|
19592
|
+
rung: findingsFile.rung,
|
|
19593
|
+
sessionId: findingsFile.sessionId
|
|
19594
|
+
});
|
|
19595
|
+
const alreadyRecorded = prior?.ladder?.cycles.find((cycle) => "recordingId" in cycle.recording && cycle.recording.recordingId === recordingId);
|
|
19596
|
+
if (prior && alreadyRecorded) {
|
|
19597
|
+
console.log(alreadyRecordedLine({
|
|
19598
|
+
output,
|
|
19599
|
+
recordingId
|
|
19600
|
+
}));
|
|
19601
|
+
return {
|
|
19602
|
+
outcome: "already-recorded",
|
|
19603
|
+
proof: prior
|
|
19604
|
+
};
|
|
19605
|
+
}
|
|
19606
|
+
const priorLadder = priorLadderStateFor({
|
|
19607
|
+
base: base.ref,
|
|
19608
|
+
headSha,
|
|
19609
|
+
isAncestor: (ancestor, descendant) => git.isAncestor(cwd, ancestor, descendant),
|
|
19610
|
+
prior
|
|
19611
|
+
});
|
|
19612
|
+
const currentFindings = reviews.flatMap((review) => review.findings.map((finding) => toLadderFindingReport(finding)));
|
|
19613
|
+
const declared = resolveDeclaredDispositions({
|
|
19614
|
+
declarations: dispositionsInput?.declarations ?? [],
|
|
19615
|
+
index: occurrenceIndexFor([...priorLadder?.cycles ?? [], ...historical ? [{ findings: currentFindings }] : []]),
|
|
19616
|
+
resolutions: findingsFile.resolutions ?? []
|
|
19617
|
+
});
|
|
19618
|
+
retainImportedProofBytes({
|
|
19619
|
+
cwd,
|
|
19620
|
+
prior,
|
|
19621
|
+
priorLadder,
|
|
19622
|
+
proofState
|
|
19623
|
+
});
|
|
19624
|
+
const retainedFindings = retainReviewBytes({
|
|
19625
|
+
bytes: findingsInput.bytes,
|
|
19626
|
+
cwd
|
|
19627
|
+
});
|
|
19628
|
+
const retainedDispositions = dispositionsInput ? retainReviewBytes({
|
|
19629
|
+
bytes: dispositionsInput.bytes,
|
|
19630
|
+
cwd
|
|
19631
|
+
}) : void 0;
|
|
19632
|
+
faultAt(fault, "after-retain");
|
|
19633
|
+
const runs = reviews.map((review) => ({ review }));
|
|
19634
|
+
const { evaluation, ladder } = ladderStateForRun({
|
|
19635
|
+
historical,
|
|
19636
|
+
identity,
|
|
19637
|
+
model: findingsFile.model,
|
|
19638
|
+
policy: ladderPolicy,
|
|
19639
|
+
priorLadder,
|
|
19640
|
+
recording: {
|
|
19641
|
+
appliedTo: proofState.digest,
|
|
19642
|
+
base: {
|
|
19643
|
+
ref: base.ref,
|
|
19644
|
+
sha: base.sha
|
|
19645
|
+
},
|
|
19646
|
+
declarations: declared,
|
|
19647
|
+
...retainedDispositions === void 0 ? {} : { dispositionsDigest: retainedDispositions.digest },
|
|
19648
|
+
findingsDigest: retainedFindings.digest,
|
|
19649
|
+
recordingId
|
|
19650
|
+
},
|
|
19651
|
+
runs,
|
|
19652
|
+
sessionAttribution: { sessionId: findingsFile.sessionId }
|
|
19653
|
+
});
|
|
19654
|
+
const proof = {
|
|
19655
|
+
base: base.ref,
|
|
19656
|
+
changedFiles: files,
|
|
19657
|
+
cleanedPaths: [],
|
|
19658
|
+
findingProvenanceVersion: 1,
|
|
19659
|
+
headSha,
|
|
19660
|
+
...importedHistoryFields({
|
|
19661
|
+
declared,
|
|
19662
|
+
prior,
|
|
19663
|
+
priorLadder
|
|
19664
|
+
}),
|
|
19665
|
+
ladder,
|
|
19666
|
+
maxReviewCycles: maxCycles,
|
|
19667
|
+
patchId,
|
|
19668
|
+
policyResolution: policy.policyResolution,
|
|
19669
|
+
reviewCycle: args.cycle,
|
|
19670
|
+
reviews: historical ? carriedVerdictFor({
|
|
19671
|
+
patchId,
|
|
19672
|
+
policy,
|
|
19673
|
+
prior
|
|
19674
|
+
}) : reviews,
|
|
19675
|
+
schemaVersion: 3
|
|
19676
|
+
};
|
|
19677
|
+
dependencies.beforeProofWrite?.();
|
|
19678
|
+
writeProofJsonApplied(proof, output, proofState.digest, fault === "proof-rename" ? faultingProofFileSystem(fault) : void 0);
|
|
19679
|
+
faultAt(fault, "after-proof-write");
|
|
19680
|
+
console.log(recordedReviewProofLine({
|
|
19681
|
+
cycle: reportedLadderCycle(evaluation, args.cycle),
|
|
19682
|
+
headSha: identity.headSha,
|
|
19683
|
+
historical,
|
|
19684
|
+
output,
|
|
19685
|
+
patchId: identity.patchId,
|
|
19686
|
+
recordingId
|
|
19687
|
+
}));
|
|
19688
|
+
writeOptionalProofCopy(proof, additionalOutput, output);
|
|
19689
|
+
enqueueHqIngest({
|
|
19690
|
+
cwd,
|
|
19691
|
+
kind: "pr-review-proof",
|
|
19692
|
+
payload: proof,
|
|
19693
|
+
payloadPath: output,
|
|
19694
|
+
profile,
|
|
19695
|
+
...args.issue === void 0 ? {} : { subject: { issueNumber: args.issue } }
|
|
19696
|
+
}, dependencies.hq);
|
|
19697
|
+
emitLadderStageTrace({
|
|
19698
|
+
cwd,
|
|
19699
|
+
evaluation,
|
|
19700
|
+
headSha,
|
|
19701
|
+
issue: args.issue,
|
|
19702
|
+
patchId,
|
|
19703
|
+
profile
|
|
19704
|
+
});
|
|
19705
|
+
if (binding.kind === "historical") {
|
|
19706
|
+
console.error(`pr:review recorded a historical cycle for patch ${findingsFile.patchId} at commit ${binding.ref}. It renders no verdict of its own; admission needs a clean cycle at ${patchId}.`);
|
|
19707
|
+
return {
|
|
19708
|
+
outcome: "historical",
|
|
19709
|
+
proof
|
|
19710
|
+
};
|
|
19711
|
+
}
|
|
19712
|
+
return {
|
|
19713
|
+
outcome: "recorded",
|
|
19714
|
+
proof
|
|
19715
|
+
};
|
|
19716
|
+
};
|
|
18243
19717
|
async function runPrReview(args, dependencies = {}) {
|
|
18244
19718
|
assertReviewFlagsCombine(args);
|
|
18245
19719
|
const cwd = path.resolve(args.cwd ?? process.cwd());
|
|
18246
19720
|
const git = dependencies.git ?? defaultPrReviewGit();
|
|
18247
|
-
const
|
|
19721
|
+
const { base, pullRequestLookup } = bindCandidateBase({
|
|
19722
|
+
args,
|
|
19723
|
+
cwd,
|
|
19724
|
+
dependencies,
|
|
19725
|
+
git
|
|
19726
|
+
});
|
|
19727
|
+
const files = git.changedFiles(cwd, base.sha);
|
|
18248
19728
|
assertCleanWorktreeForProof({
|
|
18249
19729
|
changedFiles: files,
|
|
18250
19730
|
cwd,
|
|
@@ -18261,7 +19741,7 @@ async function runPrReview(args, dependencies = {}) {
|
|
|
18261
19741
|
const { headSha, patchId } = createCandidateIdentityResolver({
|
|
18262
19742
|
cwd,
|
|
18263
19743
|
git
|
|
18264
|
-
})(
|
|
19744
|
+
})(base.sha);
|
|
18265
19745
|
const { additional: additionalOutput, canonical: output } = resolveProofOutputPaths(cwd, DEFAULT_PR_REVIEW_PROOF_PATH, args.output);
|
|
18266
19746
|
const verifyProof = readOptionalProofFor(PR_VERIFY_PROOF_DESCRIPTOR$1, cwd, args.verifyProof ?? ".factory-memory/pr-verify.json");
|
|
18267
19747
|
const writeProofCopies = (proof) => {
|
|
@@ -18276,20 +19756,22 @@ async function runPrReview(args, dependencies = {}) {
|
|
|
18276
19756
|
}, dependencies.hq);
|
|
18277
19757
|
};
|
|
18278
19758
|
const startedAt = /* @__PURE__ */ new Date();
|
|
18279
|
-
const
|
|
19759
|
+
const findingsInput = args.findings ? readFindingsInput(args.findings, cwd) : void 0;
|
|
19760
|
+
const findingsFile = findingsInput?.file;
|
|
18280
19761
|
const policy = reviewPolicyIdentity({
|
|
18281
|
-
|
|
19762
|
+
baseRef: base.ref,
|
|
18282
19763
|
changedFiles: files,
|
|
18283
19764
|
cwd,
|
|
18284
19765
|
dependencies,
|
|
18285
19766
|
git,
|
|
18286
19767
|
profile,
|
|
18287
|
-
profilePath
|
|
19768
|
+
profilePath,
|
|
19769
|
+
pullRequestLookup
|
|
18288
19770
|
});
|
|
18289
19771
|
const kinds = enabledKinds(profile, args.mode, files, findingsFile?.reviews.map((review) => review.kind) ?? [], demandedReviewKinds(policy));
|
|
18290
19772
|
if (args.assemble === true) {
|
|
18291
19773
|
const assembleProof = () => emptyProof({
|
|
18292
|
-
base:
|
|
19774
|
+
base: base.ref,
|
|
18293
19775
|
cycle: args.cycle,
|
|
18294
19776
|
files,
|
|
18295
19777
|
headSha,
|
|
@@ -18302,7 +19784,7 @@ async function runPrReview(args, dependencies = {}) {
|
|
|
18302
19784
|
return assembleProof();
|
|
18303
19785
|
}
|
|
18304
19786
|
if ((git.recognizeCatchUp ?? recognizeCatchUpCandidate)({
|
|
18305
|
-
base:
|
|
19787
|
+
base: base.sha,
|
|
18306
19788
|
cwd
|
|
18307
19789
|
}).recognized) {
|
|
18308
19790
|
console.log(ASSEMBLE_FAITHFUL_MERGE_NOTICE);
|
|
@@ -18311,6 +19793,7 @@ async function runPrReview(args, dependencies = {}) {
|
|
|
18311
19793
|
const fetchIssueBody = dependencies.fetchIssueBody ?? defaultFetchIssueBody;
|
|
18312
19794
|
writeAssembledReviewPrompt({
|
|
18313
19795
|
args,
|
|
19796
|
+
baseRef: base.ref,
|
|
18314
19797
|
cwd,
|
|
18315
19798
|
fetchIssueBody,
|
|
18316
19799
|
files,
|
|
@@ -18333,7 +19816,7 @@ async function runPrReview(args, dependencies = {}) {
|
|
|
18333
19816
|
}
|
|
18334
19817
|
const mintNotRequiredProof = (reviewRequirement) => {
|
|
18335
19818
|
const proof = emptyProof({
|
|
18336
|
-
base:
|
|
19819
|
+
base: base.ref,
|
|
18337
19820
|
cycle: args.cycle,
|
|
18338
19821
|
files,
|
|
18339
19822
|
headSha,
|
|
@@ -18367,7 +19850,7 @@ async function runPrReview(args, dependencies = {}) {
|
|
|
18367
19850
|
});
|
|
18368
19851
|
if (!findingsFile) {
|
|
18369
19852
|
if ((git.recognizeCatchUp ?? recognizeCatchUpCandidate)({
|
|
18370
|
-
base:
|
|
19853
|
+
base: base.sha,
|
|
18371
19854
|
cwd
|
|
18372
19855
|
}).recognized) return mintNotRequiredProof({
|
|
18373
19856
|
reason: "faithful-merge",
|
|
@@ -18375,89 +19858,29 @@ async function runPrReview(args, dependencies = {}) {
|
|
|
18375
19858
|
});
|
|
18376
19859
|
throw new Error("--findings <path> is required when review kinds are enabled.");
|
|
18377
19860
|
}
|
|
18378
|
-
const
|
|
19861
|
+
const recorded = recordReviewCycle({
|
|
19862
|
+
additionalOutput,
|
|
18379
19863
|
args,
|
|
19864
|
+
base,
|
|
18380
19865
|
cwd,
|
|
18381
|
-
|
|
19866
|
+
dependencies,
|
|
19867
|
+
files,
|
|
19868
|
+
findingsInput,
|
|
18382
19869
|
git,
|
|
18383
19870
|
headSha,
|
|
18384
|
-
patchId
|
|
18385
|
-
});
|
|
18386
|
-
assertIndependentSession({
|
|
18387
|
-
headSha,
|
|
18388
|
-
patchId,
|
|
18389
|
-
reviewSessionId: findingsFile.sessionId,
|
|
18390
|
-
verifyProof
|
|
18391
|
-
});
|
|
18392
|
-
const priorLadder = priorLadderStateFor({
|
|
18393
|
-
base: args.base,
|
|
18394
|
-
cwd,
|
|
18395
|
-
headSha,
|
|
18396
|
-
isAncestor: (ancestor, descendant) => git.isAncestor(cwd, ancestor, descendant),
|
|
18397
|
-
output
|
|
18398
|
-
});
|
|
18399
|
-
const priorLedger = priorLedgerFor(priorLadder);
|
|
18400
|
-
const declaredDispositions = declaredDispositionsFor(args.dispositions, cwd);
|
|
18401
|
-
const historical = binding.kind === "historical";
|
|
18402
|
-
assertDeclaredDispositionsMatch(priorLedger, declaredDispositions, currentCycleDispositionTargets(historical, findingsFile));
|
|
18403
|
-
const { identity, reviews } = cycleRecordFor({
|
|
18404
|
-
binding,
|
|
18405
|
-
cwd,
|
|
18406
|
-
findingsFile,
|
|
18407
|
-
identity: {
|
|
18408
|
-
headSha,
|
|
18409
|
-
patchId
|
|
18410
|
-
},
|
|
18411
19871
|
kinds,
|
|
18412
|
-
|
|
18413
|
-
|
|
18414
|
-
|
|
18415
|
-
const { evaluation, ladder } = ladderStateForRun({
|
|
18416
|
-
cycle: args.cycle,
|
|
18417
|
-
declaredDispositions,
|
|
18418
|
-
historical,
|
|
18419
|
-
identity,
|
|
18420
|
-
model: findingsFile.model,
|
|
18421
|
-
policy: ladderPolicy,
|
|
18422
|
-
priorLadder,
|
|
18423
|
-
priorLedger,
|
|
18424
|
-
runs,
|
|
18425
|
-
sessionAttribution: { sessionId: findingsFile.sessionId }
|
|
18426
|
-
});
|
|
18427
|
-
const proof = {
|
|
18428
|
-
base: args.base,
|
|
18429
|
-
changedFiles: files,
|
|
18430
|
-
cleanedPaths: [],
|
|
18431
|
-
findingProvenanceVersion: 1,
|
|
18432
|
-
headSha,
|
|
18433
|
-
ladder,
|
|
18434
|
-
maxReviewCycles: maxCycles,
|
|
18435
|
-
patchId,
|
|
18436
|
-
policyResolution: policy.policyResolution,
|
|
18437
|
-
reviewCycle: args.cycle,
|
|
18438
|
-
reviews: historical ? carriedVerdictFor({
|
|
18439
|
-
cwd,
|
|
18440
|
-
output,
|
|
18441
|
-
patchId,
|
|
18442
|
-
policy
|
|
18443
|
-
}) : reviews,
|
|
18444
|
-
schemaVersion: 2
|
|
18445
|
-
};
|
|
18446
|
-
writeProofCopies(proof);
|
|
18447
|
-
emitLadderStageTrace({
|
|
18448
|
-
cwd,
|
|
18449
|
-
evaluation,
|
|
18450
|
-
headSha,
|
|
18451
|
-
issue: args.issue,
|
|
19872
|
+
ladderPolicy,
|
|
19873
|
+
maxCycles,
|
|
19874
|
+
output,
|
|
18452
19875
|
patchId,
|
|
18453
|
-
|
|
19876
|
+
policy,
|
|
19877
|
+
profile,
|
|
19878
|
+
startedAt,
|
|
19879
|
+
verifyProof
|
|
18454
19880
|
});
|
|
18455
|
-
if (
|
|
18456
|
-
|
|
18457
|
-
|
|
18458
|
-
}
|
|
18459
|
-
assertReviewOutcomes(proof, ladderPolicy, args.cycle, maxCycles);
|
|
18460
|
-
return proof;
|
|
19881
|
+
if (recorded.outcome !== "recorded") return recorded.proof;
|
|
19882
|
+
assertReviewOutcomes(recorded.proof, ladderPolicy, args.cycle, maxCycles);
|
|
19883
|
+
return recorded.proof;
|
|
18461
19884
|
}
|
|
18462
19885
|
//#endregion
|
|
18463
19886
|
//#region src/pr-publish.ts
|
|
@@ -18518,16 +19941,52 @@ const defaultFollowUpRunner = (factoryCliInvocation) => (followUp, cwd) => {
|
|
|
18518
19941
|
const prPublishCliInvocation = (explicit) => explicit ?? defaultFactoryCliInvocation(factoryCliEntry());
|
|
18519
19942
|
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
18520
19943
|
/**
|
|
18521
|
-
*
|
|
18522
|
-
*
|
|
18523
|
-
*
|
|
18524
|
-
*
|
|
19944
|
+
* The base one on-disk proof recorded, keeping the doctor's absent/invalid
|
|
19945
|
+
* split (#1121, #1151): no file is no recorded base, but a file that cannot
|
|
19946
|
+
* be read — a schema-version mismatch, corrupt JSON — refuses, because
|
|
19947
|
+
* reading it as absent would create the PR against the repository default,
|
|
19948
|
+
* the #1153 failure again.
|
|
18525
19949
|
*/
|
|
18526
|
-
|
|
18527
|
-
|
|
18528
|
-
|
|
18529
|
-
|
|
18530
|
-
|
|
19950
|
+
const recordedProofBase = (read, proofPath) => {
|
|
19951
|
+
if (read.proof) return read.proof.base;
|
|
19952
|
+
if (read.error?.code === "ENOENT") return;
|
|
19953
|
+
throw new Error(`pr:publish refuses to create the pull request: the proof at ${proofPath} is present but cannot be read (${errorMessage(read.error)}), so the base it recorded is unknown. Re-record it with the current CLI, or remove the file to create against the repository default branch.`);
|
|
19954
|
+
};
|
|
19955
|
+
/**
|
|
19956
|
+
* The base a missing pull request is created against (#1153).
|
|
19957
|
+
*
|
|
19958
|
+
* `pr:verify` and `pr:review` record the base they measured on their proofs
|
|
19959
|
+
* as the local ref (`origin/<branch>`). Before this, publish created the PR
|
|
19960
|
+
* with no `--base` unless the operator repeated it, so every each-to-epic
|
|
19961
|
+
* member of #1130 opened against `main`, was measured against `main` (an
|
|
19962
|
+
* empty diff and a zero patch id), and had to be re-based by hand. The
|
|
19963
|
+
* precedence is: an explicit `--base` on publish, else the base the on-disk
|
|
19964
|
+
* proofs recorded, else no `--base` at all — GitHub then targets the
|
|
19965
|
+
* repository default branch. Two bases that disagree are refused with both
|
|
19966
|
+
* printed, before anything is created; agreement is by branch name, because
|
|
19967
|
+
* that is what GitHub creates the PR against.
|
|
19968
|
+
*/
|
|
19969
|
+
function pullRequestCreationBase({ args, cwd, reviewProofPath, verifyProofPath }) {
|
|
19970
|
+
const verifyBase = recordedProofBase(tryReadProof((proofPath) => readProofFor(PR_VERIFY_PROOF_DESCRIPTOR$1, cwd, proofPath), verifyProofPath), verifyProofPath);
|
|
19971
|
+
const reviewBase = recordedProofBase(tryReadProof((proofPath) => readProofFor(PR_REVIEW_PROOF_DESCRIPTOR, cwd, proofPath), reviewProofPath), reviewProofPath);
|
|
19972
|
+
if (verifyBase !== void 0 && reviewBase !== void 0 && candidateBaseBranch(verifyBase) !== candidateBaseBranch(reviewBase)) throw new Error(`pr:publish refuses to create the pull request: the on-disk proofs disagree on the base — pr:verify recorded ${verifyBase} and pr:review recorded ${reviewBase}. Re-run the chain against one base, then publish.`);
|
|
19973
|
+
let recorded;
|
|
19974
|
+
if (verifyBase !== void 0) recorded = {
|
|
19975
|
+
ref: verifyBase,
|
|
19976
|
+
source: "the pr:verify proof"
|
|
19977
|
+
};
|
|
19978
|
+
else if (reviewBase !== void 0) recorded = {
|
|
19979
|
+
ref: reviewBase,
|
|
19980
|
+
source: "the pr:review proof"
|
|
19981
|
+
};
|
|
19982
|
+
if (args.base.length > 0) {
|
|
19983
|
+
if (recorded && candidateBaseBranch(args.base) !== candidateBaseBranch(recorded.ref)) throw new Error(`pr:publish refuses to create the pull request: --base ${args.base} disagrees with the base recorded on ${recorded.source}, ${recorded.ref}. Omit --base to create against the recorded base, or re-run the chain against ${args.base}.`);
|
|
19984
|
+
return candidateBaseBranch(args.base);
|
|
19985
|
+
}
|
|
19986
|
+
return recorded ? candidateBaseBranch(recorded.ref) : void 0;
|
|
19987
|
+
}
|
|
19988
|
+
function discoverOrCreatePullRequest({ args, branch, cwd, gh, reviewProofPath, verifyProofPath }) {
|
|
19989
|
+
if (args.pr !== void 0) return { number: args.pr };
|
|
18531
19990
|
if (!branch) throw new Error("pr:publish cannot discover a PR from a detached HEAD; pass --pr.");
|
|
18532
19991
|
const lookupArgs = [
|
|
18533
19992
|
"pr",
|
|
@@ -18535,24 +19994,43 @@ function discoverOrCreatePullRequestNumber({ args, branch, cwd, gh }) {
|
|
|
18535
19994
|
"--head",
|
|
18536
19995
|
branch,
|
|
18537
19996
|
"--json",
|
|
18538
|
-
"number",
|
|
19997
|
+
"number,baseRefName",
|
|
18539
19998
|
"--limit",
|
|
18540
19999
|
"1"
|
|
18541
20000
|
];
|
|
18542
|
-
const
|
|
18543
|
-
|
|
20001
|
+
const lookup = () => JSON.parse(gh(lookupArgs, cwd));
|
|
20002
|
+
const [existing] = lookup();
|
|
20003
|
+
if (existing) return { number: existing.number };
|
|
20004
|
+
const creationBase = pullRequestCreationBase({
|
|
20005
|
+
args,
|
|
20006
|
+
cwd,
|
|
20007
|
+
reviewProofPath,
|
|
20008
|
+
verifyProofPath
|
|
20009
|
+
});
|
|
18544
20010
|
const createArgs = [
|
|
18545
20011
|
"pr",
|
|
18546
20012
|
"create",
|
|
18547
20013
|
"--draft",
|
|
18548
20014
|
"--fill"
|
|
18549
20015
|
];
|
|
18550
|
-
if (
|
|
20016
|
+
if (creationBase !== void 0) createArgs.push("--base", creationBase);
|
|
18551
20017
|
gh(createArgs, cwd);
|
|
18552
|
-
const created =
|
|
18553
|
-
if (!created
|
|
18554
|
-
|
|
20018
|
+
const [created] = lookup();
|
|
20019
|
+
if (!created) throw new Error(`gh pr create completed but no PR was found for branch ${branch}.`);
|
|
20020
|
+
const base = created.baseRefName ?? creationBase ?? "the repository default branch";
|
|
20021
|
+
process.stderr.write(`${createdPullRequestLine({
|
|
20022
|
+
base,
|
|
20023
|
+
number: created.number
|
|
20024
|
+
})}\n`);
|
|
20025
|
+
return {
|
|
20026
|
+
created: {
|
|
20027
|
+
base,
|
|
20028
|
+
number: created.number
|
|
20029
|
+
},
|
|
20030
|
+
number: created.number
|
|
20031
|
+
};
|
|
18555
20032
|
}
|
|
20033
|
+
const createdPullRequestLine = ({ base, number }) => `pr:publish created PR #${number} against ${base}`;
|
|
18556
20034
|
const readOptionalVerifyProof = (cwd, proofPath) => readOptionalProofFor(PR_VERIFY_PROOF_DESCRIPTOR$1, cwd, proofPath);
|
|
18557
20035
|
const followUpSummaryLine = (outcome) => {
|
|
18558
20036
|
if (outcome.status === "succeeded") return `Follow-up succeeded: ${outcome.action.command} — ${outcome.reason}.`;
|
|
@@ -18563,6 +20041,7 @@ const verificationReuseSummaryLine = (reuse) => reuse.reused ? `Verification reu
|
|
|
18563
20041
|
const readySummaryLine = (handoff) => handoff.scheduled ? "pr:publish ready — the merge is on GitHub's schedule; no human actions owed." : "pr:publish admitted, not handed off — the candidate passed but nothing is scheduled to merge it. See the readiness proof's notices and follow-up.";
|
|
18564
20042
|
const renderPublishSummary = (result) => {
|
|
18565
20043
|
const lines = result.handoff.status === "ready" ? [readySummaryLine(result.handoff)] : [`pr:publish blocked (${result.handoff.status}). Human actions owed:`, ...result.handoff.humanActions.length === 0 ? ["- Route-owned repairs remain; see readiness proof repairs."] : result.handoff.humanActions.map((action) => `- ${action}`)];
|
|
20044
|
+
if (result.createdPullRequest) lines.unshift(createdPullRequestLine(result.createdPullRequest));
|
|
18566
20045
|
if (result.verificationReuse) lines.push(verificationReuseSummaryLine(result.verificationReuse));
|
|
18567
20046
|
lines.push(followUpSummaryLine(result.followUp));
|
|
18568
20047
|
const binding = result.verifyProofBinding ? verifyProofBindingSummaryLine(result.verifyProofBinding) : void 0;
|
|
@@ -18992,12 +20471,17 @@ async function runPrPublish(args, dependencies = {}) {
|
|
|
18992
20471
|
cwd,
|
|
18993
20472
|
profilePath: args.profilePath
|
|
18994
20473
|
});
|
|
18995
|
-
const
|
|
20474
|
+
const verifyProofPath = path.resolve(cwd, args.verifyProof ?? ".factory-memory/pr-verify.json");
|
|
20475
|
+
const reviewProofPath = path.resolve(cwd, args.reviewProof ?? ".factory-memory/pr-review.json");
|
|
20476
|
+
const discovered = discoverOrCreatePullRequest({
|
|
18996
20477
|
args,
|
|
18997
20478
|
branch: args.pr === void 0 ? (dependencies.currentBranch ?? currentBranchName)(cwd) : void 0,
|
|
18998
20479
|
cwd,
|
|
18999
|
-
gh: dependencies.gh ?? defaultGhRunner$1
|
|
20480
|
+
gh: dependencies.gh ?? defaultGhRunner$1,
|
|
20481
|
+
reviewProofPath,
|
|
20482
|
+
verifyProofPath
|
|
19000
20483
|
});
|
|
20484
|
+
const prNumber = discovered.number;
|
|
19001
20485
|
const readyArgs = {
|
|
19002
20486
|
...args,
|
|
19003
20487
|
pr: prNumber
|
|
@@ -19020,8 +20504,6 @@ async function runPrPublish(args, dependencies = {}) {
|
|
|
19020
20504
|
pr: fetchPr()
|
|
19021
20505
|
})
|
|
19022
20506
|
};
|
|
19023
|
-
const verifyProofPath = path.resolve(cwd, args.verifyProof ?? ".factory-memory/pr-verify.json");
|
|
19024
|
-
const reviewProofPath = path.resolve(cwd, args.reviewProof ?? ".factory-memory/pr-review.json");
|
|
19025
20507
|
const headSha = (dependencies.git?.currentHeadSha ?? currentHeadSha)(cwd);
|
|
19026
20508
|
const verified = await ensureVerifyProof({
|
|
19027
20509
|
args: publishArgs,
|
|
@@ -19115,6 +20597,7 @@ async function runPrPublish(args, dependencies = {}) {
|
|
|
19115
20597
|
onFailure: (failed, error) => {
|
|
19116
20598
|
throw new PrPublishFollowUpError({
|
|
19117
20599
|
bodyPatched,
|
|
20600
|
+
createdPullRequest: discovered.created,
|
|
19118
20601
|
followUp: failed,
|
|
19119
20602
|
handoff: initialHandoff,
|
|
19120
20603
|
proof: initialProof,
|
|
@@ -19140,6 +20623,7 @@ async function runPrPublish(args, dependencies = {}) {
|
|
|
19140
20623
|
const handoff = summarizeHumanHandoff(proof);
|
|
19141
20624
|
const result = {
|
|
19142
20625
|
bodyPatched,
|
|
20626
|
+
createdPullRequest: discovered.created,
|
|
19143
20627
|
followUp,
|
|
19144
20628
|
handoff,
|
|
19145
20629
|
proof,
|
|
@@ -19247,7 +20731,7 @@ function assertForegroundGate({ gate, env = process.env }) {
|
|
|
19247
20731
|
//#endregion
|
|
19248
20732
|
//#region src/commands/pr-review.ts
|
|
19249
20733
|
function createPrReviewCommand(output, action) {
|
|
19250
|
-
return markCwdOptionDefault(new Command("pr:review").description("Assemble a factory-owned review prompt, or validate clean-session findings and write typed review proof").option("--base <ref>", "base branch
|
|
20734
|
+
return markCwdOptionDefault(new Command("pr:review").description("Assemble a factory-owned review prompt, or validate clean-session findings and write typed review proof").option("--base <ref>", "base ref to assert. With a pull request open for this branch its live base is the base and a disagreeing value is refused; without one, this ref (default origin/main) is fetched fresh and its SHA recorded").option("--cwd <path>", "working directory to review", collectCwdOption).option("--cycle <number>", "review cycle number", positiveInteger$1("--cycle"), 1).option("--mode <mode>", "correctness, security, or all", "all").option("--assemble", "write the factory-owned review prompt for a clean session; do not validate findings").option("--epic <number>", "issue carrying the factory-boundary manifest, which may be the work issue itself; --assemble then asks the reviewer for the findings schema the wave-demanded review rung accepts (#1001)", positiveInteger$1("--epic")).option("--findings <path>", "typed clean-session findings JSON file").option("--historical", "record findings that read an earlier commit on this branch: the cycle joins the ladder as prior history and renders no verdict of its own, so it never admits. It leaves the candidate's recorded verdict untouched. This recording may carry --dispositions for the findings it flags; otherwise declare them on the next cycle once they are persisted ledger entries").option("--dispositions <path>", "JSON file of ladder disposition declarations (waived / follow-up-filed / fixed-in-thread) for previously flagged findings, or for the findings a --historical recording itself flags").option("--issue <number>", "issue number recorded on the review ladder trace; --assemble also loads Review focus from this issue", positiveInteger$1("--issue")).option("--output <path>", "write proof JSON to a file").option("--allow-fault-injection", "test-only: honour FACTORY_REVIEW_FAULT (after-retain | proof-rename | after-proof-write) to interrupt the recording at that point and demonstrate that a retry resolves to already-recorded or a clean re-record (#1131)").option("--profile <path>", "path to the project profile JSON file").option("--verify-proof <path>", "pr:verify proof JSON path").action(withGateTiming({
|
|
19251
20735
|
gate: "pr:review",
|
|
19252
20736
|
resolveCycle: (options) => options.cycle,
|
|
19253
20737
|
resolveLedgerRoot: (options) => resolveCwdOption(options.cwd),
|
|
@@ -19260,8 +20744,9 @@ function createPrReviewCommand(output, action) {
|
|
|
19260
20744
|
"all"
|
|
19261
20745
|
].includes(options.mode)) throw new Error("--mode requires correctness, security, or all");
|
|
19262
20746
|
const args = {
|
|
20747
|
+
...options.allowFaultInjection === true ? { allowFaultInjection: true } : {},
|
|
19263
20748
|
...options.assemble === true ? { assemble: true } : {},
|
|
19264
|
-
base: options.base,
|
|
20749
|
+
...options.base === void 0 ? {} : { base: options.base },
|
|
19265
20750
|
cwd: resolveCwdOption(options.cwd),
|
|
19266
20751
|
cycle: options.cycle,
|
|
19267
20752
|
dispositions: options.dispositions,
|
|
@@ -19280,7 +20765,7 @@ function createPrReviewCommand(output, action) {
|
|
|
19280
20765
|
//#endregion
|
|
19281
20766
|
//#region src/commands/pr-verify.ts
|
|
19282
20767
|
function createPrVerifyCommand(output, action = runPrVerify) {
|
|
19283
|
-
return markCwdOptionDefault(new Command("pr:verify").description("Run project-profile verification commands and write typed proof").option("--base <ref>", "base branch
|
|
20768
|
+
return markCwdOptionDefault(new Command("pr:verify").description("Run project-profile verification commands and write typed proof").option("--base <ref>", "base ref to assert. With a pull request open for this branch its live base is the base and a disagreeing value is refused; without one, this ref (default origin/main) is fetched fresh and its SHA recorded").option("--cwd <path>", "working directory to verify", collectCwdOption).option("--docs-only", "run the docs/process verification gate").option("--full", "run the full verification gate").option("--authoring-session <id>", "explicit authoring session identity (required for review evidence)").option("--json", "print the proof as JSON after verification").option("--output <path>", "write proof JSON to a file").option("--profile <path>", "path to the project profile JSON file").option("--no-status", "skip posting the patronage-factory/pr-verify commit status").action(withGateTiming({
|
|
19284
20769
|
gate: "pr:verify",
|
|
19285
20770
|
resolveLedgerRoot: (options) => resolveCwdOption(options.cwd),
|
|
19286
20771
|
stderr: output.stderr
|