dw-sdlc 0.21.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/agentic-config.d.ts +16 -0
  2. package/dist/agentic-config.js +28 -23
  3. package/dist/cannot-run.d.ts +27 -0
  4. package/dist/cannot-run.js +48 -0
  5. package/dist/catalogue.js +2 -1
  6. package/dist/checks/agents-map.js +7 -8
  7. package/dist/checks/check.d.ts +10 -8
  8. package/dist/checks/check.js +9 -7
  9. package/dist/checks/decision-records.js +3 -2
  10. package/dist/checks/file-size-ratchet.js +13 -17
  11. package/dist/checks/index.d.ts +6 -5
  12. package/dist/checks/index.js +10 -9
  13. package/dist/checks/markdown-refs.js +1 -1
  14. package/dist/checks/primitives.js +5 -5
  15. package/dist/checks/routed-docs.js +1 -3
  16. package/dist/checks/skill-sections.js +12 -20
  17. package/dist/checks/tree.js +7 -7
  18. package/dist/cli.d.ts +2 -1
  19. package/dist/cli.js +26 -5
  20. package/dist/commands/check.js +27 -42
  21. package/dist/commands/config.js +22 -27
  22. package/dist/commands/doctor.d.ts +2 -0
  23. package/dist/commands/doctor.js +11 -18
  24. package/dist/commands/gate.js +35 -40
  25. package/dist/commands/lead-start.js +28 -42
  26. package/dist/commands/pr-reviewed.js +41 -59
  27. package/dist/commands/pr-ship.js +71 -89
  28. package/dist/commands/primitives.js +38 -40
  29. package/dist/commands/recap.js +23 -74
  30. package/dist/commands/references-setup.js +28 -76
  31. package/dist/commands/references-sync.js +29 -53
  32. package/dist/commands/setup.d.ts +2 -7
  33. package/dist/commands/setup.js +38 -90
  34. package/dist/commands/sweep.js +46 -157
  35. package/dist/commands/worker-ls.js +15 -31
  36. package/dist/commands/worker-report.js +36 -52
  37. package/dist/commands/worker-start.js +43 -57
  38. package/dist/drift.js +2 -1
  39. package/dist/exec.d.ts +20 -0
  40. package/dist/exec.js +37 -4
  41. package/dist/gate-cache.js +2 -1
  42. package/dist/git.d.ts +32 -0
  43. package/dist/git.js +60 -0
  44. package/dist/json-schema.js +1 -3
  45. package/dist/landed.js +11 -22
  46. package/dist/paused-operation.d.ts +2 -0
  47. package/dist/paused-operation.js +17 -0
  48. package/dist/pins.js +2 -1
  49. package/dist/pr/decide.d.ts +4 -16
  50. package/dist/pr/decide.js +48 -84
  51. package/dist/pr/delta.d.ts +3 -4
  52. package/dist/pr/delta.js +10 -19
  53. package/dist/pr/read.d.ts +27 -30
  54. package/dist/pr/read.js +94 -182
  55. package/dist/pr/reply.js +3 -5
  56. package/dist/pr/route.d.ts +11 -3
  57. package/dist/pr/route.js +5 -5
  58. package/dist/pr/standing.d.ts +138 -0
  59. package/dist/pr/standing.js +181 -0
  60. package/dist/pr/state.d.ts +10 -69
  61. package/dist/pr/state.js +42 -72
  62. package/dist/pr/upstream.d.ts +1 -6
  63. package/dist/pr/upstream.js +2 -2
  64. package/dist/primitives-diff.js +1 -1
  65. package/dist/recap-next.js +34 -47
  66. package/dist/recap-primitives.js +1 -1
  67. package/dist/references-key.d.ts +0 -1
  68. package/dist/references-key.js +3 -4
  69. package/dist/run-command.d.ts +4 -1
  70. package/dist/run-command.js +32 -11
  71. package/dist/setup-gate.d.ts +1 -7
  72. package/dist/setup-rules.js +2 -6
  73. package/dist/setup-writer.d.ts +32 -0
  74. package/dist/setup-writer.js +60 -0
  75. package/dist/shell-line.d.ts +2 -0
  76. package/dist/shell-line.js +5 -0
  77. package/dist/sweep-removal.d.ts +31 -0
  78. package/dist/sweep-removal.js +133 -0
  79. package/dist/sweep-terminals.d.ts +1 -1
  80. package/dist/sweep-terminals.js +21 -57
  81. package/dist/sweep-trees.d.ts +19 -3
  82. package/dist/sweep-trees.js +67 -31
  83. package/dist/worker/herdr.d.ts +67 -23
  84. package/dist/worker/herdr.js +126 -77
  85. package/dist/worker/launch.js +32 -45
  86. package/dist/worker/origin.d.ts +1 -1
  87. package/dist/worker/origin.js +9 -11
  88. package/dist/worker/tabs.js +11 -7
  89. package/dist/working-tree.d.ts +0 -8
  90. package/dist/working-tree.js +6 -22
  91. package/dist/worktree-lock.js +1 -1
  92. package/package.json +1 -1
  93. package/dist/git-env.d.ts +0 -2
  94. package/dist/git-env.js +0 -10
package/dist/pr/read.js CHANGED
@@ -1,18 +1,13 @@
1
1
  // The read the pr commands and recap make: gh as a child process, its auth, the pull request a command names,
2
- // its state from one GraphQL request with the compares that tell whether its head carries an earlier Review
3
- // and that size its changes, and beside them the default branch and the clock the decision reads.
4
- import { exec, TimedOut } from "../exec.js";
5
- import { behindOf, changedOf, changesOf, compareArgs, sameChanges } from "./delta.js";
2
+ // its state from one GraphQL request and the Review's standing at its head from GitHub's compares, through
3
+ // gh, and beside them the default branch and the clock the decision reads.
4
+ import { CannotRun } from "../cannot-run.js";
5
+ import { firstLine, need } from "../exec.js";
6
+ import { behindOf, changedOf, changesOf, compareArgs } from "./delta.js";
6
7
  import { ReplyError } from "./reply.js";
8
+ import { standingOf } from "./standing.js";
7
9
  import { awaitsReview, parseDefaultBranch, parsePr, parseView, QUERY } from "./state.js";
8
- /** What stops a pr command or recap before it answers: exit 2, the problem and its remedy on one line. */
9
- export class CannotRun extends Error {
10
- remedy;
11
- constructor(message, remedy) {
12
- super(message);
13
- this.remedy = remedy;
14
- }
15
- }
10
+ import { holds, renamedUpstream } from "./upstream.js";
16
11
  const REMEDIES = {
17
12
  missing: "install the GitHub CLI from https://cli.github.com",
18
13
  auth: "run gh auth login, or set GH_TOKEN",
@@ -31,39 +26,18 @@ export function namedProblem(number, repo) {
31
26
  }
32
27
  return undefined;
33
28
  }
34
- function firstLine(text) {
35
- return text.trim().split("\n")[0] ?? "";
36
- }
37
29
  /** What gh said went wrong: the first line of its stderr, without its `gh: ` prefix or its `X` icon. */
38
30
  export function said(stderr) {
39
- return firstLine(stderr).replace(/^(?:gh: |X )/, "");
31
+ return (firstLine(stderr) ?? "").replace(/^(?:gh: |X )/, "");
40
32
  }
41
33
  /** The problem `gh auth status` reports: its line marked `X` under the host, else its first line. */
42
34
  function authProblem(stderr) {
43
35
  const marked = stderr.split("\n").find((line) => line.trimStart().startsWith("X "));
44
- return marked === undefined ? firstLine(stderr) : marked.trim().slice(2);
45
- }
46
- /** `gh args` as a child process; past its limit it stops the command as any other failed read does. */
47
- function reachGh(args) {
48
- try {
49
- return exec("gh", args);
50
- }
51
- catch (error) {
52
- if (error instanceof TimedOut) {
53
- throw new CannotRun(error.message, REMEDIES.read);
54
- }
55
- throw error;
56
- }
36
+ return marked === undefined ? (firstLine(stderr) ?? "") : marked.trim().slice(2);
57
37
  }
38
+ /** `gh args` as a child process; not on PATH, or past its limit, it stops the command as any failed read does. */
58
39
  export function gh(args) {
59
- const result = reachGh(args);
60
- if (result.error !== undefined) {
61
- if ("code" in result.error && result.error.code === "ENOENT") {
62
- throw new CannotRun("gh is not on PATH", REMEDIES.missing);
63
- }
64
- throw result.error;
65
- }
66
- return { status: result.status ?? 1, stdout: result.stdout, stderr: result.stderr };
40
+ return need("gh", args, { missing: REMEDIES.missing, stalled: REMEDIES.read });
67
41
  }
68
42
  /** What `parse` returns; a reply it cannot parse stops the command, naming the field. */
69
43
  function parseOrCannotRun(parse) {
@@ -89,7 +63,7 @@ function targetOf({ number, repo }, command) {
89
63
  const named = [...(number === undefined ? [] : [String(number)]), ...(repo === undefined ? [] : ["-R", repo])];
90
64
  const view = gh(["pr", "view", ...named, "--json", "url"]);
91
65
  if (view.status !== 0) {
92
- throw new CannotRun(`gh pr view found no pull request: ${firstLine(view.stderr)}`, nameItRemedy(command));
66
+ throw new CannotRun(`gh pr view found no pull request: ${firstLine(view.stderr) ?? ""}`, nameItRemedy(command));
93
67
  }
94
68
  return parseOrCannotRun(() => parseView(view.stdout));
95
69
  }
@@ -101,41 +75,17 @@ function requireAuth() {
101
75
  throw new CannotRun(`gh auth status failed: ${authProblem(auth.stderr)}`, REMEDIES.auth);
102
76
  }
103
77
  }
104
- /**
105
- * The state of the pull request `named`; `command`, as `pr ship`, names the call in a remedy. With
106
- * `sized`, as `pr.review: auto` needs, the read sizes the changes a Review would read.
107
- */
108
- export function read(named, command, sized) {
109
- requireAuth();
110
- return query(targetOf(named, command), sized);
111
- }
112
- /** The pull request `gh pr view` resolves for the head branch `branch`, else the current one; null when none. */
78
+ /** The pull request `gh pr view` resolves for the head branch `branch`, else the current one, or what gh said of none. */
113
79
  function viewed(branch) {
114
80
  const view = gh(["pr", "view", ...(branch === undefined ? [] : [branch]), "--json", "url"]);
115
81
  if (view.status === 0) {
116
82
  return parseOrCannotRun(() => parseView(view.stdout));
117
83
  }
118
84
  if (said(view.stderr).startsWith("no pull requests found")) {
119
- return null;
85
+ return { none: said(view.stderr) };
120
86
  }
121
87
  throw new CannotRun(`gh pr view failed: ${said(view.stderr)}`, REMEDIES.read);
122
88
  }
123
- /**
124
- * The current branch's pull request, open or not, or null. gh tries `@{push}`, else the local name, which
125
- * misses a branch pushed under another name; then `upstreamName` is asked, and `byUpstream` says so.
126
- */
127
- export function readBranch(sized, upstreamName) {
128
- requireAuth();
129
- const own = viewed(undefined);
130
- if (own !== null) {
131
- return { pr: query(own, sized), byUpstream: false };
132
- }
133
- if (upstreamName === undefined) {
134
- return null;
135
- }
136
- const found = viewed(upstreamName);
137
- return found === null ? null : { pr: query(found, sized), byUpstream: true };
138
- }
139
89
  /** The default branch of the repository gh resolves from the current directory. */
140
90
  export function defaultBranch() {
141
91
  const view = gh(["repo", "view", "--json", "defaultBranchRef"]);
@@ -156,125 +106,88 @@ export function now() {
156
106
  }
157
107
  return time;
158
108
  }
159
- /** The state of `pr` once more, after an action the script performed on it: the same pull request. */
160
- export function readAgain(pr, sized) {
161
- return query(pr, sized);
109
+ /** A commit as a reason names it: its first seven characters. */
110
+ function short(commit) {
111
+ return commit.slice(0, 7);
162
112
  }
163
113
  /**
164
- * The compares this process has read, by repository and the two commits, so reads that share one ask gh
165
- * once: a compare of two commits never changes.
114
+ * The gh adapter on `target`. A compare of two commits never changes, so it asks gh for each once, for its
115
+ * own life; a compare gh refuses stops the command, naming it.
166
116
  */
167
- const COMPARES = new Map();
168
- /** For a test that runs the command in its own process: each case starts with no compare read. */
169
- export function forgetCompares() {
170
- COMPARES.clear();
171
- }
172
- /** The reply of the compare from `from` to `commit`, read once per process; `what` names it when gh refuses it. */
173
- function compared(pr, from, commit, what) {
174
- const key = `${pr.repo} ${from}...${commit}`;
175
- const known = COMPARES.get(key);
176
- if (known !== undefined) {
177
- return known;
178
- }
179
- const reply = gh(compareArgs(pr.repo, from, commit));
180
- if (reply.status !== 0) {
181
- throw new CannotRun(`${pr.repo}#${pr.number}: ${what}: ${said(reply.stderr)}`, REMEDIES.read);
182
- }
183
- COMPARES.set(key, reply.stdout);
184
- return reply.stdout;
185
- }
186
- /** Whether the head of `after` carries the base `before` read: the compare shows it behind by none. */
187
- export function carriesBase(before, after) {
188
- const reply = compared(after, before.baseOid, after.head, `the compare with ${before.baseOid.slice(0, 7)}`);
189
- return parseOrCannotRun(() => behindOf(reply)) === 0;
190
- }
191
- /** The own changes of `pr` at `commit`, from the compare with its base; null when it cannot show them whole. */
192
- function ownChanges(pr, commit) {
193
- const reply = compared(pr, pr.baseOid, commit, `the compare with ${commit.slice(0, 7)}`);
194
- return parseOrCannotRun(() => changesOf(reply));
195
- }
196
- /** `pr` and, with no read in its history, the newest commit force pushes left behind that a Review read. */
197
- function withRewritten(pr, rewritten) {
198
- const [newest] = rewritten;
199
- if (newest === undefined || pr.lastRead !== null || pr.open !== null) {
200
- return pr;
201
- }
202
- return { ...pr, lastRead: { ...newest, baseOnly: true, baseMoved: true } };
203
- }
204
- /**
205
- * `pr` with its last read mark `open` when a merge of the base or a rebase came after it and no fix
206
- * shows: the own changes are as it read them, or a compare cannot show them whole (ADR 0023). The
207
- * compares run only when a Review awaits the head.
208
- */
209
- function withOpen(pr) {
210
- const last = pr.lastRead;
211
- if (last === null || last.clean || !last.baseMoved || pr.state !== "OPEN" || !awaitsReview(pr)) {
212
- return pr;
213
- }
214
- const atRead = ownChanges(pr, last.commit);
215
- const atHead = ownChanges(pr, pr.head);
216
- if (atRead !== null && atHead !== null && !sameChanges(atRead, atHead)) {
217
- return pr;
218
- }
219
- return { ...pr, lastRead: null, open: last.commit };
220
- }
221
- /**
222
- * `pr` and whether its head carries the success of the last commit a Review read: a clean read, only
223
- * the base's changes since it, and the same own changes at both. A read mark never carries. The
224
- * compares run only when a Review awaits the head.
225
- */
226
- function withCarried(pr) {
227
- const last = pr.lastRead;
228
- if (last === null || !last.clean || !last.baseOnly || pr.state !== "OPEN" || !awaitsReview(pr)) {
229
- return { ...pr, carried: false };
230
- }
231
- return { ...pr, carried: sameChanges(ownChanges(pr, last.commit), ownChanges(pr, pr.head)) };
232
- }
233
- /**
234
- * The files whose own change differs between `from` and the head of `pr`, a rename under both names:
235
- * what a fix touched, without the files a merge of the base brought in. Null when a compare cannot show
236
- * the own changes whole.
237
- */
238
- function ownScope(pr, from) {
239
- const before = ownChanges(pr, from);
240
- const after = ownChanges(pr, pr.head);
241
- if (before === null || after === null) {
242
- return null;
243
- }
244
- const moved = [
245
- ...after.filter((change) => !before.includes(change)),
246
- ...before.filter((change) => !after.includes(change))
247
- ];
248
- const names = moved.flatMap((change) => {
249
- const [path, , previous] = JSON.parse(change);
250
- return previous === null ? [path] : [path, previous];
251
- });
252
- return [...new Set(names)].toSorted();
253
- }
254
- /**
255
- * `pr` and, when a Review awaits a head that carries none, its `scope` after a merge of the base or a
256
- * rebase, and when `sized`, the files the Review would read: the compare from the last commit a Review
257
- * read, else from the base, to the head, kept to the scope when there is one.
258
- */
259
- function withChanged(pr, sized) {
260
- if (pr.carried || pr.state !== "OPEN" || !awaitsReview(pr)) {
261
- return { ...pr, changed: null, scope: null };
262
- }
263
- const scope = pr.lastRead?.baseMoved === true ? ownScope(pr, pr.lastRead.commit) : null;
264
- if (!sized) {
265
- return { ...pr, changed: null, scope };
266
- }
267
- const from = pr.lastRead?.commit ?? pr.baseOid;
268
- const reply = compared(pr, from, pr.head, `the compare of ${from.slice(0, 7)}...${pr.head.slice(0, 7)}`);
269
- const changed = parseOrCannotRun(() => changedOf(reply));
270
- if (scope === null || changed === "unsized") {
271
- return { ...pr, changed, scope };
272
- }
273
- const within = changed.filter(({ path, previous }) => scope.includes(path) || (previous !== null && scope.includes(previous)));
274
- return { ...pr, changed: within, scope };
275
- }
276
- /** The state of pull request `number` of `repo`, from one GraphQL request and the compares a Review needs. */
277
- function query({ repo, number }, sized) {
117
+ function ghCompares(target) {
118
+ const replies = new Map();
119
+ const replyOf = (from, to, what) => {
120
+ const key = `${from}...${to}`;
121
+ const known = replies.get(key);
122
+ if (known !== undefined) {
123
+ return known;
124
+ }
125
+ const reply = gh(compareArgs(target.repo, from, to));
126
+ if (reply.status !== 0) {
127
+ throw new CannotRun(`${target.repo}#${target.number}: ${what}: ${said(reply.stderr)}`, REMEDIES.read);
128
+ }
129
+ replies.set(key, reply.stdout);
130
+ return reply.stdout;
131
+ };
132
+ return {
133
+ ownChanges: (base, commit) => parseOrCannotRun(() => changesOf(replyOf(base, commit, `the compare with ${short(commit)}`))),
134
+ changed: (from, head) => parseOrCannotRun(() => changedOf(replyOf(from, head, `the compare of ${short(from)}...${short(head)}`))),
135
+ behind: (base, head) => parseOrCannotRun(() => behindOf(replyOf(base, head, `the compare with ${short(base)}`)))
136
+ };
137
+ }
138
+ /** The pull request `named` from the one GraphQL request alone, without the compares the Review's standing reads. */
139
+ export function pullRequestNamed(named, command) {
140
+ requireAuth();
141
+ const target = targetOf(named, command);
142
+ return parseOrCannotRun(() => parsePr(graphql(target), target.repo)).pr;
143
+ }
144
+ /** The reads of one command; with `sized`, as `pr.review: auto` needs, each sizes the changes a Review would read. */
145
+ export function ghReads(sized) {
146
+ const adapters = new Map();
147
+ const comparesOn = (target) => {
148
+ const key = `${target.repo}#${target.number}`;
149
+ const adapter = adapters.get(key) ?? ghCompares(target);
150
+ adapters.set(key, adapter);
151
+ return adapter;
152
+ };
153
+ const query = (target) => {
154
+ const { pr, history } = parseOrCannotRun(() => parsePr(graphql(target), target.repo));
155
+ const awaits = pr.state === "OPEN" && awaitsReview(pr);
156
+ return { ...pr, ...standingOf(history, comparesOn(target), { awaits, sized }) };
157
+ };
158
+ return {
159
+ named: (named, command) => {
160
+ requireAuth();
161
+ return query(targetOf(named, command));
162
+ },
163
+ checkout: (root) => {
164
+ requireAuth();
165
+ const own = viewed(undefined);
166
+ if (!("none" in own)) {
167
+ return query(own);
168
+ }
169
+ const renamed = renamedUpstream(root);
170
+ if (renamed === undefined) {
171
+ return { problem: `gh pr view found no pull request: ${own.none}`, lacksHead: false };
172
+ }
173
+ const { branch, head, upstream } = renamed;
174
+ const named = `${upstream}, the name its upstream gives`;
175
+ const found = viewed(upstream);
176
+ if ("none" in found) {
177
+ return { problem: `gh pr view found no pull request for ${branch}, nor for ${named}`, lacksHead: false };
178
+ }
179
+ // A branch started from another tracks that branch until its first push: its pull request is not this one.
180
+ const pr = query(found);
181
+ return holds(pr, head)
182
+ ? pr
183
+ : { problem: `${pr.url}, found by ${named}, lacks HEAD ${short(head)}`, lacksHead: true };
184
+ },
185
+ again: query,
186
+ carriesBase: (before, after) => comparesOn(after).behind(before.baseOid, after.head) === 0
187
+ };
188
+ }
189
+ /** The reply of the one GraphQL request for pull request `number` of `repo`. */
190
+ function graphql({ repo, number }) {
278
191
  const [owner = "", name = ""] = repo.split("/");
279
192
  const reply = gh([
280
193
  "api",
@@ -293,6 +206,5 @@ function query({ repo, number }, sized) {
293
206
  const remedy = problem.startsWith("Could not resolve to a") ? REMEDIES.target : REMEDIES.read;
294
207
  throw new CannotRun(`${repo}#${number}: ${problem}`, remedy);
295
208
  }
296
- const { pr, rewritten } = parseOrCannotRun(() => parsePr(reply.stdout, repo));
297
- return withChanged(withCarried(withOpen(withRewritten(pr, rewritten))), sized);
209
+ return reply.stdout;
298
210
  }
package/dist/pr/reply.js CHANGED
@@ -1,11 +1,9 @@
1
1
  // The reply of a gh call read as JSON: each field checked as the type the reader asks for, and a
2
2
  // reply that fails names the field by its path.
3
+ import { isObject } from "../json-text.js";
3
4
  /** A reply that is not the shape the query asks for; the message names the field. */
4
5
  export class ReplyError extends Error {
5
6
  }
6
- function isRecord(value) {
7
- return typeof value === "object" && value !== null && !Array.isArray(value);
8
- }
9
7
  export function shown(value) {
10
8
  return value === undefined ? "missing" : JSON.stringify(value);
11
9
  }
@@ -18,7 +16,7 @@ export function rootOf(reply, what) {
18
16
  catch {
19
17
  throw new ReplyError(`${what} is not JSON`);
20
18
  }
21
- if (!isRecord(json)) {
19
+ if (!isObject(json)) {
22
20
  throw new ReplyError(`${what} is not a JSON object`);
23
21
  }
24
22
  return { value: json, at: "" };
@@ -27,7 +25,7 @@ export function fieldOf(node, key) {
27
25
  return { value: node.value[key], at: node.at === "" ? key : `${node.at}.${key}` };
28
26
  }
29
27
  export function object(value, at) {
30
- if (!isRecord(value)) {
28
+ if (!isObject(value)) {
31
29
  throw new ReplyError(`${at} is ${shown(value)}, not an object`);
32
30
  }
33
31
  return { value, at };
@@ -1,5 +1,13 @@
1
1
  import type { ReviewDepth } from "../agentic-config.ts";
2
- import type { ChangedFile } from "./state.ts";
2
+ /**
3
+ * A file a Review would read: its name, its name before a rename, and the lines it adds and removes,
4
+ * null when the compare shows no patch for it, as for a binary file.
5
+ */
6
+ export type ChangedFile = {
7
+ path: string;
8
+ previous: string | null;
9
+ lines: number | null;
10
+ };
3
11
  /** A routed depth, and under off why no one reads the changes, as the status the script posts says. */
4
12
  export type Routed = {
5
13
  depth: Exclude<ReviewDepth, "off">;
@@ -9,6 +17,6 @@ export type Routed = {
9
17
  };
10
18
  /**
11
19
  * The depth the rules route `changed` to. A change not read or not sized reads in full, as does a file
12
- * outside lockfiles the compare shows no patch for.
20
+ * outside lockfiles the compare shows no patch for. With `findingsOpen` someone reads even lockfiles alone.
13
21
  */
14
- export declare function route(changed: ChangedFile[] | "unsized" | null): Routed;
22
+ export declare function route(changed: ChangedFile[] | "unsized" | null, findingsOpen: boolean): Routed;
package/dist/pr/route.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // The built-in rules `pr.review: auto` routes a Review depth by, from the files the Review would read:
2
- // lockfiles alone are off; a workflow file, more than 100 changed lines outside lockfiles, or a change
3
- // the compare cannot size is full; the rest is light. No config key moves them.
2
+ // lockfiles alone are off while no Finding is open; a workflow file, more than 100 changed lines outside
3
+ // lockfiles, or a change the compare cannot size is full; the rest is light. No config key moves them.
4
4
  /** The lockfiles a package manager writes, by file name in any directory. */
5
5
  const LOCKFILES = new Set([
6
6
  "pnpm-lock.yaml",
@@ -33,14 +33,14 @@ function isWorkflow({ path, previous }) {
33
33
  }
34
34
  /**
35
35
  * The depth the rules route `changed` to. A change not read or not sized reads in full, as does a file
36
- * outside lockfiles the compare shows no patch for.
36
+ * outside lockfiles the compare shows no patch for. With `findingsOpen` someone reads even lockfiles alone.
37
37
  */
38
- export function route(changed) {
38
+ export function route(changed, findingsOpen) {
39
39
  if (changed === null || changed === "unsized") {
40
40
  return { depth: "full" };
41
41
  }
42
42
  const code = changed.filter((file) => !isLockfile(file.path));
43
- if (changed.length > 0 && code.length === 0) {
43
+ if (!findingsOpen && changed.length > 0 && code.length === 0) {
44
44
  return { depth: "off", why: "lockfiles only" };
45
45
  }
46
46
  const lines = code.map((file) => file.lines);
@@ -0,0 +1,138 @@
1
+ import type { PrReview, ReviewDepth } from "../agentic-config.ts";
2
+ import { type ChangedFile } from "./route.ts";
3
+ /** What a commit's review status says: a success, the read mark, a success posted under off, or none. */
4
+ export type Posted = "clean" | "findings" | "off" | null;
5
+ /** A commit of the history and the review status it carries; `merge` when it has more than one parent. */
6
+ export type Commit = {
7
+ oid: string;
8
+ posted: Posted;
9
+ merge: boolean;
10
+ };
11
+ /**
12
+ * A commit a force push left behind and its review status; `forksAt` the index in the history of the
13
+ * commit its line was pushed on, as under an amend, -1 when its line holds none, as after a rebase.
14
+ */
15
+ export type LeftBehind = {
16
+ oid: string;
17
+ posted: Posted;
18
+ forksAt: number;
19
+ };
20
+ /** What the standing is read from. */
21
+ export type History = {
22
+ /** The pull request's last 100 commits, oldest first, the head last. */
23
+ commits: Commit[];
24
+ /** The commits the last force pushes left behind, newest push first, none of them among `commits`. */
25
+ leftBehind: LeftBehind[];
26
+ head: string;
27
+ /** The commit the base branch points at, the one the own changes are compared with. */
28
+ base: string;
29
+ };
30
+ /**
31
+ * A file's own change: its name, its status, its name before a rename, and the lines its patch adds and
32
+ * removes, in order, without the hunk headers and context a moved base shifts.
33
+ */
34
+ export type OwnChange = {
35
+ path: string;
36
+ status: string;
37
+ previous: string | null;
38
+ lines: string[];
39
+ };
40
+ /** GitHub's compares as the standing asks them. A compare of two commits never changes: an adapter may keep it. */
41
+ export type Compares = {
42
+ /** The pull request's own changes at `commit`, from `base`; null when the compare cannot show them whole. */
43
+ ownChanges: (base: string, commit: string) => OwnChange[] | null;
44
+ /** The files from `from` to `head` and their changed lines; `unsized` when the compare may hold some back. */
45
+ changed: (from: string, head: string) => ChangedFile[] | "unsized";
46
+ };
47
+ /**
48
+ * A commit a Review read, `clean` when it left a success there and not a read mark; `baseOnly` when every
49
+ * change after it is the base's, the head included: only merges of the base, or a rebase past no other
50
+ * read; `baseMoved` when a merge of the base or a rebase came after it.
51
+ */
52
+ export type LastRead = {
53
+ commit: string;
54
+ clean: boolean;
55
+ baseOnly: boolean;
56
+ baseMoved: boolean;
57
+ };
58
+ /**
59
+ * The Reviews that left a Finding open since the last review success, a success under off and the dev's
60
+ * acceptance included: `count` their read marks, the head's own among them, and `since` that success's
61
+ * commit, null when no commit read holds one.
62
+ */
63
+ export type ReadMarks = {
64
+ count: number;
65
+ since: string | null;
66
+ };
67
+ /** The Review's standing at the head, as the decision reads it and `pr ship --verbose` shows it. */
68
+ export type Standing = {
69
+ /**
70
+ * A read mark force pushes left behind after every read of the history, else the last commit before the
71
+ * head a Review read, else a commit force pushes left behind that one read.
72
+ */
73
+ lastRead: LastRead | null;
74
+ /**
75
+ * A read mark whose Findings no later commit answers: the head's own, or one the commits since leave with
76
+ * no fix showing, merges of the base and rebases among them. Then `lastRead` is null and nothing carries
77
+ * (ADR 0023).
78
+ */
79
+ open: string | null;
80
+ /** The read marks posted since the last success, on the history and on the commits force pushes left behind. */
81
+ marks: ReadMarks;
82
+ /** Whether the head carries the success of `lastRead`, which two compares decide. */
83
+ carried: boolean;
84
+ /**
85
+ * The files a Review would read, from `lastRead` or the base to the head, read only for `pr.review:
86
+ * auto` to route on: `unsized` when the compare cannot list them all, null when not read.
87
+ */
88
+ changed: ChangedFile[] | "unsized" | null;
89
+ /**
90
+ * The files whose own change moved since `lastRead`, when a merge of the base or a rebase came after it:
91
+ * the files a fix touched, the base's own left out. Null with neither, or when a compare cannot show them.
92
+ */
93
+ scope: string[] | null;
94
+ };
95
+ /**
96
+ * The standing at the head of `history`. The compares run only when a Review `awaits` the head, and the
97
+ * files that Review would read are read only when `sized`, as `pr.review: auto` routes on them.
98
+ */
99
+ export declare function standingOf(history: History, compares: Compares, { awaits, sized }: {
100
+ awaits: boolean;
101
+ sized: boolean;
102
+ }): Standing;
103
+ /** Who set the depth: the dev's `--review` for this call, or `pr.review`, a depth or `auto`. */
104
+ export type ReviewPolicy = {
105
+ depth: ReviewDepth;
106
+ source: "flag";
107
+ } | {
108
+ depth: PrReview;
109
+ source: "config";
110
+ };
111
+ /** Where a Review depth comes from: the dev's word, `pr.review`, or the rules `pr.review: auto` routes by. */
112
+ export type DepthSource = "flag" | "config" | "auto";
113
+ /** A depth as the answer names it, and under off why no one reads the head. */
114
+ export type Depth = {
115
+ depth: Exclude<ReviewDepth, "off">;
116
+ source: DepthSource;
117
+ } | {
118
+ depth: "off";
119
+ source: DepthSource;
120
+ why: string;
121
+ };
122
+ /**
123
+ * The Review the head gets: the success of the read it carries; a Review at a depth that reads from `since`,
124
+ * kept to `paths`, over the read mark left `open`, where under off the script posts the success; or the park.
125
+ */
126
+ export type ReviewCall = {
127
+ kind: "carry";
128
+ from: string;
129
+ } | (Depth & {
130
+ kind: "review";
131
+ since: string | null;
132
+ paths: string[] | null;
133
+ open: string | null;
134
+ }) | (ReadMarks & {
135
+ kind: "limit";
136
+ });
137
+ /** The Review the head gets at `standing` under `review`; the dev's word goes over a carry. */
138
+ export declare function reviewCall(standing: Standing, review: ReviewPolicy): ReviewCall;