@brainervirus/workit-core 8.24.0 → 8.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ledger.ts CHANGED
@@ -33,7 +33,10 @@
33
33
  // `base`, the stable patch-id of `base...head` and an exact diff hash. It is
34
34
  // `fresh` (same head; verdicts on a dirty worktree are refused, so the head
35
35
  // is the judged tree), `carried` (new head, same patch-id AND same exact
36
- // diff: a rebase that only moved the base) or `stale`.
36
+ // diff AND, for a pass, byte-identical own files the base left alone: a
37
+ // rebase that only moved the base; or a trusted verdict whose
38
+ // follow-up change is lane none, or test-only for a unit verdict, see
39
+ // core/policy/carry.ts) or `stale`.
37
40
  // - Integrity (D18): every row the CLI appends carries `prevHash` (sha256 of
38
41
  // the previous line) and `rowHash` (sha256 over `prevHash` and the row), a
39
42
  // running chain. A row without it after the chain began, or whose hashes do
@@ -54,8 +57,16 @@ import { createHash, randomBytes } from "node:crypto";
54
57
  import fs from "node:fs";
55
58
  import path from "node:path";
56
59
  import { hostAwareLockTimeout } from "./core/store-lock";
57
- import { vcsConfig } from "./core/vcs-config";
58
- import { GIT_TIMEOUTS, currentBranch, headSha, patchId, worktreeTree } from "./git/rev";
60
+ import { verdictDrift, type CarryRule, type Drift } from "./core/policy/carry";
61
+ import { defaultBase } from "./git/default-base";
62
+ import {
63
+ GIT_TIMEOUTS,
64
+ currentBranch,
65
+ headSha,
66
+ patchId,
67
+ sameOwnChange,
68
+ worktreeTree,
69
+ } from "./git/rev";
59
70
  import { hostSessionFromEnv, issuedBinding } from "./host-session";
60
71
  import { bumpStandingRev } from "./hooks/standing-signal";
61
72
  import { resolveStore } from "./store/paths";
@@ -281,47 +292,8 @@ export function branchHead(cwd: string, branch: string): string | null {
281
292
  );
282
293
  }
283
294
 
284
- /**
285
- * The PR target of the release track the checkout is on, when the workspace
286
- * configures tracks (as `fanout plan` and `stack plan` pick their trunk); a
287
- * plain workspace default does not override origin's default branch.
288
- */
289
- const trackTrunk = (cwd: string): string | null => {
290
- try {
291
- const resolved = vcsConfig("resolve", cwd);
292
- if (resolved.ok === false || !resolved.releaseTrack || resolved.releaseTrack.blocking)
293
- return null;
294
- const target = String(resolved.defaultTargetBranch ?? "").trim();
295
- return target && safeRef(target) ? target : null;
296
- } catch {
297
- return null;
298
- }
299
- };
300
-
301
- /**
302
- * The default base for patch-ids and author trailers: the release track's PR
303
- * target (origin's copy, else the local one) when tracks are configured, else
304
- * origin's HEAD branch, else the first of origin/main, origin/master, main,
305
- * master, origin/develop, develop that exists.
306
- */
307
- export function defaultBase(cwd: string): string | null {
308
- const trunk = trackTrunk(cwd);
309
- if (trunk)
310
- for (const candidate of [`origin/${trunk}`, trunk])
311
- if (resolveCommit(cwd, candidate)) return candidate;
312
- const originHead = git(cwd, ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"]);
313
- if (originHead && resolveCommit(cwd, originHead)) return originHead;
314
- for (const candidate of [
315
- "origin/main",
316
- "origin/master",
317
- "main",
318
- "master",
319
- "origin/develop",
320
- "develop",
321
- ])
322
- if (resolveCommit(cwd, candidate)) return candidate;
323
- return null;
324
- }
295
+ // The default base lives in git/default-base.ts (shared with the review lane).
296
+ export { defaultBase };
325
297
 
326
298
  /**
327
299
  * sha256 of the exact `base...head` diff with the pinned options rev.ts uses
@@ -1136,9 +1108,21 @@ export type VerdictEntry = {
1136
1108
  /** current AND passing AND independent. */
1137
1109
  accepted: boolean;
1138
1110
  reasons: RejectReason[];
1111
+ /** Why a verdict on an older head still applies after a follow-up change (core/policy/carry.ts). */
1112
+ carry: CarryNote | null;
1139
1113
  verdict: ReadRow;
1140
1114
  };
1141
1115
 
1116
+ /** A verdict carried over a follow-up change: by which rule, from which judged head, over which files. */
1117
+ export type CarryNote = { rule: CarryRule; since: string; files: string[] };
1118
+
1119
+ /** A stale verdict whose follow-up change is small and off risk paths: re-verify only these files. */
1120
+ export type Reverify = { scope: "focused"; since: string; files: string[] };
1121
+
1122
+ /** `carried (lane none since <head>)`: how a carried verdict reads in `ledger check` and `pr status`. */
1123
+ export const carryLabel = (carry: CarryNote): string =>
1124
+ `carried (${carry.rule === "lane-none" ? "lane none" : "tests only"} since ${carry.since.slice(0, 12)})`;
1125
+
1142
1126
  export type VerdictCheck = {
1143
1127
  branch: string;
1144
1128
  head: string | null;
@@ -1146,6 +1130,10 @@ export type VerdictCheck = {
1146
1130
  patchId: string | null;
1147
1131
  /** Newest current verdict (else newest verdict): does it still apply? */
1148
1132
  current: { basis: VerdictBasis; verdict: ReadRow | null };
1133
+ /** The carry of the accepted verdict (else of the newest current one), when a follow-up change carried it. */
1134
+ carry: CarryNote | null;
1135
+ /** No verdict applies, but the newest passing one is stale only by a small change: re-verify that. */
1136
+ reverify: Reverify | null;
1149
1137
  /**
1150
1138
  * What merge gates read: an accepted strong (`verified`/`tests-verified`)
1151
1139
  * verdict and no current independent failure. `type-check-only` never counts.
@@ -1276,7 +1264,7 @@ const derivesFrom = (derived: string, bare: string): boolean =>
1276
1264
  * child that committed as `<lead>:<child>` and judges as `<child>` is still
1277
1265
  * the author). Two derived ids never match on a shared last segment.
1278
1266
  */
1279
- const authoredBy = (authors: Set<string>, session: string): boolean => {
1267
+ export const authoredBy = (authors: Set<string>, session: string): boolean => {
1280
1268
  if (authors.has(session)) return true;
1281
1269
  for (const author of authors)
1282
1270
  if (derivesFrom(author, session) || derivesFrom(session, author)) return true;
@@ -1414,15 +1402,39 @@ function effectiveOf(
1414
1402
  * each restack itself and records `stack.restacked` with `patchEqual` when
1415
1403
  * the change relative to the parent is identical (same patch-id AND same
1416
1404
  * exact diff hash, D18). A verdict carries when such observed rows link its
1417
- * head to the current head.
1405
+ * head to the current head. With `exactIn` (the checkout), each link is
1406
+ * re-read with exact blobs (sameOwnChange, #334), so a row recorded before
1407
+ * that check carries a pass only when the branch's own files are identical
1408
+ * across the restack. Null keeps every observed link: a failure keeps
1409
+ * applying through them (failing closed the other way).
1418
1410
  */
1411
+ const restackExact = new WeakMap<ReadRow, boolean>();
1419
1412
  function restackCarries(
1413
+ exactIn: string | null,
1420
1414
  rows: readonly ReadRow[],
1421
1415
  branch: string,
1422
1416
  from: string | null,
1423
1417
  to: string | null,
1424
1418
  ): boolean {
1425
1419
  if (!from || !to) return false;
1420
+ const exact = (row: ReadRow, fromHead: string, head: string): boolean => {
1421
+ if (exactIn === null) return true;
1422
+ const cwd = exactIn;
1423
+ let known = restackExact.get(row);
1424
+ if (known === undefined) {
1425
+ known =
1426
+ typeof row.fromBase === "string" &&
1427
+ typeof row.toBase === "string" &&
1428
+ sameOwnChange(cwd, {
1429
+ from: fromHead,
1430
+ to: head,
1431
+ fromBase: row.fromBase,
1432
+ toBase: row.toBase,
1433
+ });
1434
+ restackExact.set(row, known);
1435
+ }
1436
+ return known;
1437
+ };
1426
1438
  const edges = new Map<string, string[]>();
1427
1439
  for (const row of rows)
1428
1440
  if (
@@ -1432,7 +1444,8 @@ function restackCarries(
1432
1444
  row.branch === branch &&
1433
1445
  row.patchEqual === true &&
1434
1446
  typeof row.fromHead === "string" &&
1435
- row.head
1447
+ row.head &&
1448
+ exact(row, row.fromHead, row.head)
1436
1449
  )
1437
1450
  edges.set(row.fromHead, [...(edges.get(row.fromHead) ?? []), row.head]);
1438
1451
  const seen = new Set<string>([from]);
@@ -1471,9 +1484,73 @@ export function checkVerdicts(
1471
1484
  return key;
1472
1485
  };
1473
1486
  const keyFor = (base: string | null) => keysAt(base, head);
1474
- const basisOf = (row: ReadRow): Exclude<VerdictBasis, "none"> => {
1487
+ // The branch's own change since a trusted verdict's head, read once per
1488
+ // row (#334). A row the gates cannot trust never carries.
1489
+ const driftCache = new Map<ReadRow, Drift | null>();
1490
+ const driftOf = (row: ReadRow): Drift | null => {
1491
+ if (driftCache.has(row)) return driftCache.get(row) ?? null;
1492
+ let drift: Drift | null = null;
1493
+ const toBase = row.base ?? fallbackBase;
1494
+ const toBaseSha = toBase ? resolveCommit(cwd, toBase) : null;
1495
+ const fromBase = row.baseSha ?? toBaseSha;
1496
+ if (
1497
+ row.head &&
1498
+ head &&
1499
+ row.head !== head &&
1500
+ toBaseSha &&
1501
+ fromBase &&
1502
+ !untrusted(reasonsOf(row))
1503
+ )
1504
+ drift = verdictDrift(cwd, {
1505
+ from: row.head,
1506
+ to: head,
1507
+ fromBase,
1508
+ toBase: toBaseSha,
1509
+ kind: typeof row.kind === "string" ? row.kind : "review",
1510
+ });
1511
+ driftCache.set(row, drift);
1512
+ return drift;
1513
+ };
1514
+ // The same patch-id and diff hash are not enough to carry a pass (#334):
1515
+ // both drop hunk line numbers, so an edit moved within a file keeps them.
1516
+ // The branch's own files must be identical blobs too (sameOwnChange). A
1517
+ // failure keeps the looser carry: it fails closed by applying more.
1518
+ const exactCache = new Map<ReadRow, boolean>();
1519
+ const exactCarry = (row: ReadRow): boolean => {
1520
+ if (FAILING.has(String(row.result))) return true;
1521
+ let exact = exactCache.get(row);
1522
+ if (exact === undefined) {
1523
+ const toBase = row.base ?? fallbackBase;
1524
+ const toBaseSha = toBase ? resolveCommit(cwd, toBase) : null;
1525
+ const fromBase = row.baseSha ?? toBaseSha;
1526
+ exact = Boolean(
1527
+ row.head &&
1528
+ head &&
1529
+ toBaseSha &&
1530
+ fromBase &&
1531
+ sameOwnChange(cwd, { from: row.head, to: head, fromBase, toBase: toBaseSha }),
1532
+ );
1533
+ exactCache.set(row, exact);
1534
+ }
1535
+ return exact;
1536
+ };
1537
+ const keyBasis = (row: ReadRow): Exclude<VerdictBasis, "none"> => {
1475
1538
  const basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
1476
- return basis === "stale" && restackCarries(rows, branch, row.head, head) ? "carried" : basis;
1539
+ return basis === "carried" && !exactCarry(row) ? "stale" : basis;
1540
+ };
1541
+ const restacked = (row: ReadRow): boolean =>
1542
+ restackCarries(FAILING.has(String(row.result)) ? null : cwd, rows, branch, row.head, head);
1543
+ /** The follow-up carry of a verdict the exact keys and restacks do not carry. */
1544
+ const carryOf = (row: ReadRow): CarryNote | null => {
1545
+ if (keyBasis(row) !== "stale") return null;
1546
+ if (restacked(row)) return null;
1547
+ const drift = driftOf(row);
1548
+ return drift?.carries ? { rule: drift.rule, since: drift.since, files: drift.files } : null;
1549
+ };
1550
+ const basisOf = (row: ReadRow): Exclude<VerdictBasis, "none"> => {
1551
+ const basis = keyBasis(row);
1552
+ if (basis !== "stale") return basis;
1553
+ return restacked(row) || carryOf(row) ? "carried" : basis;
1477
1554
  };
1478
1555
  // A newer row is judged under the failure's own base, so a row recorded
1479
1556
  // with a different --base cannot make unchanged code look changed. When
@@ -1487,7 +1564,7 @@ export function checkVerdicts(
1487
1564
  const keys = stored ? { patchId: row.patchId, diffHash: row.diffHash } : keysAt(base, row.head);
1488
1565
  return (
1489
1566
  verdictBasis(failed, { head: row.head, ...keys }) !== "stale" ||
1490
- restackCarries(rows, branch, failed.head, row.head)
1567
+ restackCarries(null, rows, branch, failed.head, row.head)
1491
1568
  );
1492
1569
  };
1493
1570
  const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
@@ -1566,6 +1643,7 @@ export function checkVerdicts(
1566
1643
  independent: independence.length === 0,
1567
1644
  accepted: reasons.length === 0,
1568
1645
  reasons,
1646
+ carry: basis === "carried" ? carryOf(row) : null,
1569
1647
  verdict: row,
1570
1648
  });
1571
1649
  // The author's own failure never displaces an independent pass, but it
@@ -1636,6 +1714,24 @@ export function checkVerdicts(
1636
1714
  );
1637
1715
  const selfEntry =
1638
1716
  latestSelf && STRONG_RESULTS.has(String(latestSelf.verdict.result)) ? latestSelf : undefined;
1717
+ const carryEntry =
1718
+ verdicts.find((entry) => entry.verdict === accepted.verdict && entry.accepted) ?? newestCurrent;
1719
+ // A focused re-verify: nothing applies now, and the newest trusted pass is
1720
+ // stale only by a small change off risk paths (the drift's lane is self).
1721
+ const lastPass =
1722
+ accepted.accepted || failing || tampered.length
1723
+ ? undefined
1724
+ : verdicts.findLast(
1725
+ (entry) =>
1726
+ !entry.current &&
1727
+ !untrusted(entry.reasons) &&
1728
+ STRONG_RESULTS.has(String(entry.verdict.result)),
1729
+ );
1730
+ const drift = lastPass ? driftOf(lastPass.verdict) : null;
1731
+ const reverify: Reverify | null =
1732
+ drift && !drift.carries && drift.focused && drift.files
1733
+ ? { scope: "focused", since: drift.since, files: drift.files }
1734
+ : null;
1639
1735
  return {
1640
1736
  branch,
1641
1737
  head,
@@ -1644,6 +1740,8 @@ export function checkVerdicts(
1644
1740
  base: newestCurrent?.verdict.base ?? fallbackBase,
1645
1741
  patchId: keyFor(newestCurrent?.verdict.base ?? fallbackBase).patchId,
1646
1742
  current: { basis: newestCurrent?.basis ?? "none", verdict: newestCurrent?.verdict ?? null },
1743
+ carry: carryEntry?.carry ?? null,
1744
+ reverify,
1647
1745
  accepted,
1648
1746
  verdicts,
1649
1747
  authors: [...authors].toSorted(),
package/src/stack.ts CHANGED
@@ -62,10 +62,10 @@ import {
62
62
  pushRemoteName,
63
63
  remoteRefTip,
64
64
  resolveRef,
65
+ sameOwnChange,
65
66
  } from "./git/rev";
66
67
  import {
67
68
  appendObserved,
68
- checkVerdicts,
69
69
  diffHash,
70
70
  readLedger,
71
71
  storeRoot,
@@ -75,6 +75,7 @@ import {
75
75
  } from "./ledger";
76
76
  import type { LaneNeed, ReviewLane } from "./core/policy/lane";
77
77
  import { prLaneGate } from "./core/policy/pr-lane";
78
+ import { checkVerdictsCovered, type StackCover } from "./verdict-cover";
78
79
 
79
80
  export const STACK_VERSION = 1;
80
81
 
@@ -1031,6 +1032,8 @@ export type BranchStatus = {
1031
1032
  verdictId: string | null;
1032
1033
  lane: ReviewLane | null;
1033
1034
  need: LaneNeed;
1035
+ /** The verdict higher in the stack that covers this branch, when it has none of its own. */
1036
+ cover: StackCover | null;
1034
1037
  };
1035
1038
  };
1036
1039
 
@@ -1102,7 +1105,8 @@ const verdictOf = (
1102
1105
  rows: readonly ReadRow[],
1103
1106
  doc: PrStatusDoc | null,
1104
1107
  ): BranchStatus["verdict"] => {
1105
- const check = checkVerdicts(cwd, branch, rows);
1108
+ // A lower branch may be covered by the verdict at the top of the stack (#334).
1109
+ const check = checkVerdictsCovered(cwd, branch, rows);
1106
1110
  // Without a PR there is no base to diff against: the gate stays independent.
1107
1111
  const gate = doc ? prLaneGate(cwd, check, { branch, base: doc.base, head: doc.head.sha }) : null;
1108
1112
  return {
@@ -1117,6 +1121,7 @@ const verdictOf = (
1117
1121
  : null,
1118
1122
  lane: gate?.lane.lane ?? null,
1119
1123
  need: gate?.need ?? "non-author",
1124
+ cover: check.cover,
1120
1125
  };
1121
1126
  };
1122
1127
 
@@ -1257,6 +1262,7 @@ export function stackStatus(ctx: Ctx, stack: StackFile): StackResult<StatusOutco
1257
1262
  verdictId: null,
1258
1263
  lane: null,
1259
1264
  need: "non-author",
1265
+ cover: null,
1260
1266
  }
1261
1267
  : verdictOf(ctx.cwd, entry.branch, ledger.value.rows, doc),
1262
1268
  };
@@ -1536,7 +1542,7 @@ export function syncStack(
1536
1542
  return stackFail(
1537
1543
  "blocked",
1538
1544
  `a rebase is in progress in ${owner}`,
1539
- `git -C ${owner} rebase --continue # or --abort; then workit --cwd ${owner} stack sync`,
1545
+ `git -C ${owner} rebase --continue # or --abort; then workit stack --cwd ${owner} sync`,
1540
1546
  { conflict: entry.branch, worktree: owner, progress: outcome },
1541
1547
  );
1542
1548
  if (owner && trackedDirt(owner))
@@ -1666,7 +1672,11 @@ export function syncStack(
1666
1672
  };
1667
1673
  const after = { p: patchId(cwd, parentTip, newTip), d: diffHash(cwd, parentTip, newTip) };
1668
1674
  const equal =
1669
- before.d !== null && before.d === after.d && before.p !== null && before.p === after.p;
1675
+ before.d !== null &&
1676
+ before.d === after.d &&
1677
+ before.p !== null &&
1678
+ before.p === after.p &&
1679
+ sameOwnChange(cwd, { from: fromHead, to: newTip, fromBase, toBase: parentTip });
1670
1680
  appendObserved(cwd, {
1671
1681
  type: "stack.restacked",
1672
1682
  actor,
@@ -0,0 +1,151 @@
1
+ // Stack-wide verdicts (#334, decision mvk5q3io): one verifier at the top of
2
+ // a stack, after its last push, covers the lower PRs it judged. A branch with
3
+ // no accepted verdict of its own is covered by a branch above it in a cached
4
+ // stack (`<store>/stacks/*.json`, S12) when:
5
+ //
6
+ // - that branch has an accepted independent verdict now (fresh or carried);
7
+ // - the verdict's session is not an author of the lower branch either;
8
+ // - every commit of the lower branch since its fork from the trunk has a
9
+ // verbatim patch-id (whitespace counts) among the commits of the judged
10
+ // head (the verdict's own head, not the upper branch's current tip): the
11
+ // lower change was judged unchanged. No merge commits, at least one
12
+ // non-empty commit;
13
+ // - the judged head holds exactly the lower branch's resulting files (no
14
+ // `<lower>..<judged>` difference in any file the lower branch changed):
15
+ // an upper commit that reverts or rewrites a lower file means the lower
16
+ // PR's own code was never judged;
17
+ // - the lower branch's own review lane is below full (decision B3): a risk
18
+ // path or a large change needs its own verdict.
19
+ //
20
+ // A lower branch that changed after the verdict (a new commit, an amend, a
21
+ // conflict resolved differently) holds a patch-id the judged head lacks and
22
+ // is not covered. When git cannot read any of this, nothing is covered. A current independent failure or a tampered ledger on the
23
+ // lower branch is never covered over. `pr merge`, `stack land`, `stack
24
+ // status`, `pr status` and `ledger check` read this; the Stop hook does not.
25
+ import fs from "node:fs";
26
+ import path from "node:path";
27
+ import { branchLane } from "./core/policy/lane";
28
+ import { commitPatchIds, diffRaw, mergeBase } from "./git/rev";
29
+ import {
30
+ authorSessions,
31
+ authoredBy,
32
+ checkVerdicts,
33
+ defaultBase,
34
+ resolveCommit,
35
+ storeRoot,
36
+ type ReadRow,
37
+ type VerdictCheck,
38
+ } from "./ledger";
39
+
40
+ /** The verdict above `branch` in a stack that covers it. */
41
+ export type StackCover = {
42
+ stack: string;
43
+ /** The upper branch the verdict was recorded on. */
44
+ branch: string;
45
+ verdictId: string;
46
+ /** The head that verdict judged. */
47
+ head: string;
48
+ };
49
+
50
+ export type CoveredCheck = VerdictCheck & { cover: StackCover | null };
51
+
52
+ type Chain = { name: string; trunk: string; above: string[] };
53
+
54
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
55
+ typeof value === "object" && value !== null && !Array.isArray(value);
56
+
57
+ /** The open branches above `branch` in each cached stack holding it, top first. */
58
+ function chainsAbove(cwd: string, branch: string): Chain[] {
59
+ const root = storeRoot(cwd);
60
+ if (!root.ok) return [];
61
+ const dir = path.join(root.value.root, "stacks");
62
+ let names: string[] = [];
63
+ try {
64
+ names = fs.readdirSync(dir).filter((name) => name.endsWith(".json"));
65
+ } catch {
66
+ return [];
67
+ }
68
+ const chains: Chain[] = [];
69
+ for (const name of names.toSorted()) {
70
+ let parsed: unknown;
71
+ try {
72
+ parsed = JSON.parse(fs.readFileSync(path.join(dir, name), "utf8"));
73
+ } catch {
74
+ continue;
75
+ }
76
+ if (!isRecord(parsed) || !Array.isArray(parsed.branches) || typeof parsed.trunk !== "string")
77
+ continue;
78
+ const entries = parsed.branches.filter(isRecord);
79
+ const index = entries.findIndex((entry) => entry.branch === branch && !entry.merged);
80
+ if (index < 0) continue;
81
+ chains.push({
82
+ name: typeof parsed.name === "string" ? parsed.name : name,
83
+ trunk: parsed.trunk,
84
+ above: entries
85
+ .slice(index + 1)
86
+ .filter((entry) => typeof entry.branch === "string" && !entry.merged)
87
+ .map((entry) => entry.branch as string)
88
+ .toReversed(),
89
+ });
90
+ }
91
+ return chains;
92
+ }
93
+
94
+ /**
95
+ * Whether the verdict on `judged` covers the lower branch at `head` (see the
96
+ * header). Anything git cannot read is not covered.
97
+ */
98
+ function covers(cwd: string, trunk: string, head: string, judged: string): boolean {
99
+ const trunkSha = [`origin/${trunk}`, trunk].map((ref) => resolveCommit(cwd, ref)).find(Boolean);
100
+ const ownFork = trunkSha ? mergeBase(cwd, trunkSha, head) : null;
101
+ const judgedFork = trunkSha ? mergeBase(cwd, trunkSha, judged) : null;
102
+ if (!ownFork || !judgedFork) return false;
103
+ const own = commitPatchIds(cwd, ownFork, head);
104
+ const top = commitPatchIds(cwd, judgedFork, judged);
105
+ if (!own || !top || own.merges > 0 || own.patchIds.length === 0) return false;
106
+ const judgedIds = new Set(top.patchIds);
107
+ if (!own.patchIds.every((id) => judgedIds.has(id))) return false;
108
+ // The judged tree holds exactly the lower branch's resulting files.
109
+ const changed = diffRaw(cwd, ownFork, head);
110
+ const between = changed ? diffRaw(cwd, head, judged) : null;
111
+ if (!changed || !between) return false;
112
+ const files = new Set(changed.map((entry) => entry.path));
113
+ if (between.some((entry) => files.has(entry.path))) return false;
114
+ return branchLane(cwd, { base: trunk, head }).lane !== "full";
115
+ }
116
+
117
+ /**
118
+ * `checkVerdicts` for `branch`, and when it has no accepted verdict of its
119
+ * own, the stack verdict that covers it (see the header): then `accepted`
120
+ * names that verdict and `cover` says where it came from.
121
+ */
122
+ export function checkVerdictsCovered(
123
+ cwd: string,
124
+ branch: string,
125
+ rows: readonly ReadRow[],
126
+ ): CoveredCheck {
127
+ const check = checkVerdicts(cwd, branch, rows);
128
+ const blocked = check.accepted.reasons.some(
129
+ (reason) => reason === "failing_verdict" || reason === "ledger_tampered",
130
+ );
131
+ if (check.accepted.accepted || blocked || !check.head) return { ...check, cover: null };
132
+ let authors: Set<string> | null = null;
133
+ for (const chain of chainsAbove(cwd, branch))
134
+ for (const upper of chain.above) {
135
+ const verdict = checkVerdicts(cwd, upper, rows).accepted;
136
+ const row = verdict.accepted ? verdict.verdict : null;
137
+ const session = row?.actor.session;
138
+ if (!row?.head || !session || typeof row.id !== "string") continue;
139
+ authors ??= authorSessions(cwd, rows, branch, { base: defaultBase(cwd), head: check.head });
140
+ if (authoredBy(authors, session)) continue;
141
+ if (!covers(cwd, chain.trunk, check.head, row.head)) continue;
142
+ return {
143
+ ...check,
144
+ accepted: { accepted: true, verdict: row, reasons: [] },
145
+ review: "verified",
146
+ selfVerdict: null,
147
+ cover: { stack: chain.name, branch: upper, verdictId: row.id, head: row.head },
148
+ };
149
+ }
150
+ return { ...check, cover: null };
151
+ }