@brainervirus/workit-core 8.22.3 → 8.24.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
@@ -1622,15 +1622,20 @@ export function checkVerdicts(
1622
1622
  ? newestCurrent.reasons
1623
1623
  : (["type_check_only"] as RejectReason[]),
1624
1624
  };
1625
- const selfEntry = failing
1625
+ // The author's latest pass or failure on the current code wins, across
1626
+ // kinds: a newer failure cancels an older pass (review finding mvkcoxgb).
1627
+ const latestSelf = failing
1626
1628
  ? undefined
1627
1629
  : verdicts.findLast(
1628
1630
  (entry) =>
1629
1631
  entry.current &&
1630
1632
  !entry.independent &&
1631
1633
  !untrusted(entry.reasons) &&
1632
- STRONG_RESULTS.has(String(entry.verdict.result)),
1634
+ (STRONG_RESULTS.has(String(entry.verdict.result)) ||
1635
+ FAILING.has(String(entry.verdict.result))),
1633
1636
  );
1637
+ const selfEntry =
1638
+ latestSelf && STRONG_RESULTS.has(String(latestSelf.verdict.result)) ? latestSelf : undefined;
1634
1639
  return {
1635
1640
  branch,
1636
1641
  head,
package/src/stack.ts CHANGED
@@ -73,6 +73,8 @@ import {
73
73
  type ReadRow,
74
74
  type VerdictBasis,
75
75
  } from "./ledger";
76
+ import type { LaneNeed, ReviewLane } from "./core/policy/lane";
77
+ import { prLaneGate } from "./core/policy/pr-lane";
76
78
 
77
79
  export const STACK_VERSION = 1;
78
80
 
@@ -1016,7 +1018,20 @@ export type BranchStatus = {
1016
1018
  next: NextAction | null;
1017
1019
  checks: ChecksState | null;
1018
1020
  blockers: string[];
1019
- verdict: { accepted: boolean; basis: VerdictBasis; reasons: string[]; verdictId: string | null };
1021
+ /**
1022
+ * `accepted`: an accepted independent verdict; `satisfied`: what the land
1023
+ * gate reads, the verdict the PR's review lane asks for (an accepted
1024
+ * independent one outside `verification: proportional`).
1025
+ */
1026
+ verdict: {
1027
+ accepted: boolean;
1028
+ satisfied: boolean;
1029
+ basis: VerdictBasis;
1030
+ reasons: string[];
1031
+ verdictId: string | null;
1032
+ lane: ReviewLane | null;
1033
+ need: LaneNeed;
1034
+ };
1020
1035
  };
1021
1036
 
1022
1037
  export type Qualification =
@@ -1081,16 +1096,27 @@ export function repoMismatch(ctx: Pick<Ctx, "resolved">, stack: StackFile): Stac
1081
1096
  return null;
1082
1097
  }
1083
1098
 
1084
- const verdictOf = (cwd: string, branch: string, rows: readonly ReadRow[]) => {
1099
+ const verdictOf = (
1100
+ cwd: string,
1101
+ branch: string,
1102
+ rows: readonly ReadRow[],
1103
+ doc: PrStatusDoc | null,
1104
+ ): BranchStatus["verdict"] => {
1085
1105
  const check = checkVerdicts(cwd, branch, rows);
1106
+ // Without a PR there is no base to diff against: the gate stays independent.
1107
+ const gate = doc ? prLaneGate(cwd, check, { branch, base: doc.base, head: doc.head.sha }) : null;
1086
1108
  return {
1087
1109
  accepted: check.accepted.accepted,
1110
+ satisfied: gate ? gate.satisfied : check.accepted.accepted,
1088
1111
  basis: check.current.basis,
1089
- reasons: check.accepted.reasons as string[],
1090
- verdictId:
1091
- typeof check.accepted.verdict?.id === "string" && check.accepted.accepted
1112
+ reasons: check.accepted.reasons,
1113
+ verdictId: gate
1114
+ ? gate.verdictId
1115
+ : typeof check.accepted.verdict?.id === "string" && check.accepted.accepted
1092
1116
  ? check.accepted.verdict.id
1093
1117
  : null,
1118
+ lane: gate?.lane.lane ?? null,
1119
+ need: gate?.need ?? "non-author",
1094
1120
  };
1095
1121
  };
1096
1122
 
@@ -1161,12 +1187,20 @@ export function qualify(
1161
1187
  `${label} has a current independent failed verdict for ${doc.head.sha.slice(0, 12)}; --unverified never lands over a rejection`,
1162
1188
  "a new independent verdict on the head supersedes it",
1163
1189
  );
1164
- if (!(bypass && allowUnverified) && !verdict.accepted)
1190
+ if (!(bypass && allowUnverified) && !verdict.satisfied) {
1191
+ const bypassHint = allowUnverified
1192
+ ? `; or, only if the user asks to land unverified: workit stack land --unverified --reason "<why>"`
1193
+ : "";
1165
1194
  return stop(
1166
1195
  verdict.reasons.includes("no_verdict") ? "no_verdict" : "needs_verdict",
1167
- `${label} has no accepted independent verdict for ${doc.head.sha.slice(0, 12)} (${verdict.reasons.join(", ") || "none"})`,
1168
- `an independent session verifies ${entry.branch} and runs: workit ledger verdict verified --how "<what was exercised>" --branch ${entry.branch}${allowUnverified ? `; or, only if the user asks to land unverified: workit stack land --unverified --reason "<why>"` : ""}`,
1196
+ verdict.need === "self"
1197
+ ? `${label} has no verdict for ${doc.head.sha.slice(0, 12)} (lane self: the author's own --self verdict is enough; ${verdict.reasons.join(", ") || "none"})`
1198
+ : `${label} has no accepted independent verdict for ${doc.head.sha.slice(0, 12)}${verdict.lane && verdict.lane !== "none" ? ` (lane ${verdict.lane})` : ""} (${verdict.reasons.join(", ") || "none"})`,
1199
+ verdict.need === "self"
1200
+ ? `after an observed passing \`workit check test\`, the author runs: workit ledger verdict tests-verified --self --how "<what you ran>" --branch ${entry.branch}${bypassHint}`
1201
+ : `an independent session verifies ${entry.branch} and runs: workit ledger verdict verified --how "<what was exercised>" --branch ${entry.branch}${bypassHint}`,
1169
1202
  );
1203
+ }
1170
1204
  if (!grant.allowed)
1171
1205
  return stop(
1172
1206
  "grant_required",
@@ -1215,8 +1249,16 @@ export function stackStatus(ctx: Ctx, stack: StackFile): StackResult<StatusOutco
1215
1249
  checks: doc?.checks.state ?? null,
1216
1250
  blockers: doc?.blockers ?? [],
1217
1251
  verdict: merged
1218
- ? { accepted: true, basis: "none", reasons: [], verdictId: null }
1219
- : verdictOf(ctx.cwd, entry.branch, ledger.value.rows),
1252
+ ? {
1253
+ accepted: true,
1254
+ satisfied: true,
1255
+ basis: "none",
1256
+ reasons: [],
1257
+ verdictId: null,
1258
+ lane: null,
1259
+ need: "non-author",
1260
+ }
1261
+ : verdictOf(ctx.cwd, entry.branch, ledger.value.rows, doc),
1220
1262
  };
1221
1263
  branches.push(row);
1222
1264
  if (merged) {
@@ -1866,7 +1908,7 @@ export async function landStack(
1866
1908
  }
1867
1909
  }
1868
1910
  const grant = requireGrant(ctx.cwd, "merge");
1869
- const verdict = verdictOf(ctx.cwd, entry.branch, ledgerRows());
1911
+ const verdict = verdictOf(ctx.cwd, entry.branch, ledgerRows(), doc);
1870
1912
  // A dry run predicts the retarget the real run would do first.
1871
1913
  const effective =
1872
1914
  options.dryRun && simulated.size > 0 && doc ? { ...doc, base: stack.trunk } : doc;
@@ -1906,7 +1948,7 @@ export async function landStack(
1906
1948
  deleteBranch: false,
1907
1949
  actor: ctx.actor,
1908
1950
  expect: { base: stack.trunk, branch: entry.branch },
1909
- ...(bypass && !verdict.accepted ? { unverified: bypass } : {}),
1951
+ ...(bypass && !verdict.satisfied ? { unverified: bypass } : {}),
1910
1952
  },
1911
1953
  ctx.sleep,
1912
1954
  );
@@ -0,0 +1,142 @@
1
+ // Dependencies for a fresh worktree (issue #310): a linked worktree has no
2
+ // node_modules, so a resolver walks up to whatever node_modules is above it
3
+ // (the main checkout's, for a worktree nested inside it) and a workspace
4
+ // package resolves to the main checkout's source: tests then run the wrong
5
+ // code. Instead of a package-manager install, the worktree gets its own
6
+ // node_modules that mirrors the main checkout's one level deep:
7
+ //
8
+ // - a symlink is copied as it is: a relative one (bun, npm and yarn link
9
+ // workspace packages as `../../packages/<name>`, and `.bin` entries as
10
+ // `../<pkg>/bin/x`) then resolves inside the worktree, which has the same
11
+ // layout; an absolute one into the main checkout (a Windows junction) is
12
+ // re-pointed at the worktree's counterpart;
13
+ // - `@scope` directories and `.bin` are mirrored the same way, one level;
14
+ // - anything else (a third-party package) is linked to the main checkout's,
15
+ // so installed packages are shared, never copied.
16
+ //
17
+ // It runs only where the worktree has no node_modules of its own and git
18
+ // ignores node_modules there (else the links would show as changes and block
19
+ // release). `git worktree remove` unlinks the links and never follows them.
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { gitRun } from "./fanout";
23
+
24
+ const MODULES = "node_modules";
25
+ /** Directories searched for node_modules below the checkout root (packages/<name>). */
26
+ const MAX_DEPTH = 2;
27
+
28
+ export type LinkedDependencies = {
29
+ /** The main checkout the links point at. */
30
+ from: string;
31
+ /** The node_modules directories made, relative to the worktree. */
32
+ dirs: string[];
33
+ /** Links made across them. */
34
+ entries: number;
35
+ ms: number;
36
+ };
37
+
38
+ const within = (child: string, parent: string): boolean => {
39
+ const relative = path.relative(parent, child);
40
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
41
+ };
42
+
43
+ /** node_modules directories of the main checkout, as paths relative to it. */
44
+ function moduleDirs(main: string): string[] {
45
+ const out: string[] = [];
46
+ const walk = (relative: string, depth: number) => {
47
+ let entries: fs.Dirent[];
48
+ try {
49
+ entries = fs.readdirSync(path.join(main, relative), { withFileTypes: true });
50
+ } catch {
51
+ return;
52
+ }
53
+ for (const entry of entries) {
54
+ if (!entry.isDirectory()) continue;
55
+ const child = path.join(relative, entry.name);
56
+ if (entry.name === MODULES) out.push(child);
57
+ else if (depth < MAX_DEPTH && !entry.name.startsWith(".")) walk(child, depth + 1);
58
+ }
59
+ };
60
+ walk("", 0);
61
+ return out;
62
+ }
63
+
64
+ function linkEntry(main: string, worktree: string, source: string, target: string): void {
65
+ const stat = fs.lstatSync(source);
66
+ if (stat.isSymbolicLink()) {
67
+ const link = fs.readlinkSync(source);
68
+ // An absolute link into the main checkout (outside its node_modules) is a
69
+ // workspace package: point it at the worktree's copy.
70
+ const outside =
71
+ path.isAbsolute(link) &&
72
+ within(link, main) &&
73
+ !path.relative(main, link).split(path.sep).includes(MODULES);
74
+ const to = outside ? path.join(worktree, path.relative(main, link)) : link;
75
+ fs.symlinkSync(to, target, process.platform === "win32" ? "junction" : undefined);
76
+ return;
77
+ }
78
+ fs.symlinkSync(
79
+ source,
80
+ target,
81
+ process.platform === "win32" ? (stat.isDirectory() ? "junction" : "file") : undefined,
82
+ );
83
+ }
84
+
85
+ /** Mirror `source` into a new directory `target`; returns the links made. */
86
+ function mirror(main: string, worktree: string, source: string, target: string, top: boolean) {
87
+ fs.mkdirSync(target);
88
+ let count = 0;
89
+ for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
90
+ const from = path.join(source, entry.name);
91
+ const to = path.join(target, entry.name);
92
+ if (top && entry.isDirectory() && (entry.name.startsWith("@") || entry.name === ".bin"))
93
+ count += mirror(main, worktree, from, to, false);
94
+ else {
95
+ linkEntry(main, worktree, from, to);
96
+ count += 1;
97
+ }
98
+ }
99
+ return count;
100
+ }
101
+
102
+ /**
103
+ * Give `worktree` node_modules that resolve its own workspace packages and
104
+ * share the main checkout's installed ones. Null when there is nothing to
105
+ * link (no node_modules in the main checkout), with a note when it was
106
+ * skipped; never throws.
107
+ */
108
+ export function linkWorktreeDependencies(
109
+ main: string,
110
+ worktree: string,
111
+ ): { linked: LinkedDependencies | null; notes: string[] } {
112
+ const started = performance.now();
113
+ const notes: string[] = [];
114
+ const dirs: string[] = [];
115
+ let entries = 0;
116
+ for (const relative of moduleDirs(main)) {
117
+ const target = path.join(worktree, relative);
118
+ if (!fs.existsSync(path.dirname(target)) || fs.existsSync(target)) continue;
119
+ // The trailing slash: the directory does not exist yet, and `node_modules/` only matches one.
120
+ if (!gitRun(worktree, ["check-ignore", "-q", `${relative.split(path.sep).join("/")}/`]).ok) {
121
+ notes.push(
122
+ `${relative.split(path.sep).join("/")} is not ignored by git, so it was not linked from ${main}; install dependencies in the worktree yourself`,
123
+ );
124
+ continue;
125
+ }
126
+ try {
127
+ entries += mirror(main, worktree, path.join(main, relative), target, true);
128
+ dirs.push(relative.split(path.sep).join("/"));
129
+ } catch (error) {
130
+ fs.rmSync(target, { recursive: true, force: true });
131
+ notes.push(
132
+ `could not link ${relative} from ${main} (${(error as Error).message}); install dependencies in the worktree yourself`,
133
+ );
134
+ }
135
+ }
136
+ if (dirs.length === 0) return { linked: null, notes };
137
+ const ms = Math.round(performance.now() - started);
138
+ notes.push(
139
+ `linked ${dirs.join(", ")} from ${main} (${entries} entries, ${ms} ms): workspace packages resolve to this worktree, installed packages are shared`,
140
+ );
141
+ return { linked: { from: main, dirs, entries, ms }, notes };
142
+ }