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.
- package/dist/agentic-config.d.ts +16 -0
- package/dist/agentic-config.js +28 -23
- package/dist/cannot-run.d.ts +27 -0
- package/dist/cannot-run.js +48 -0
- package/dist/catalogue.js +2 -1
- package/dist/checks/agents-map.js +7 -8
- package/dist/checks/check.d.ts +10 -8
- package/dist/checks/check.js +9 -7
- package/dist/checks/decision-records.js +3 -2
- package/dist/checks/file-size-ratchet.js +13 -17
- package/dist/checks/index.d.ts +6 -5
- package/dist/checks/index.js +10 -9
- package/dist/checks/markdown-refs.js +1 -1
- package/dist/checks/primitives.js +5 -5
- package/dist/checks/routed-docs.js +1 -3
- package/dist/checks/skill-sections.js +12 -20
- package/dist/checks/tree.js +7 -7
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +26 -5
- package/dist/commands/check.js +27 -42
- package/dist/commands/config.js +22 -27
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +11 -18
- package/dist/commands/gate.js +35 -40
- package/dist/commands/lead-start.js +28 -42
- package/dist/commands/pr-reviewed.js +41 -59
- package/dist/commands/pr-ship.js +71 -89
- package/dist/commands/primitives.js +38 -40
- package/dist/commands/recap.js +23 -74
- package/dist/commands/references-setup.js +28 -76
- package/dist/commands/references-sync.js +29 -53
- package/dist/commands/setup.d.ts +2 -7
- package/dist/commands/setup.js +38 -90
- package/dist/commands/sweep.js +46 -157
- package/dist/commands/worker-ls.js +15 -31
- package/dist/commands/worker-report.js +36 -52
- package/dist/commands/worker-start.js +43 -57
- package/dist/drift.js +2 -1
- package/dist/exec.d.ts +20 -0
- package/dist/exec.js +37 -4
- package/dist/gate-cache.js +2 -1
- package/dist/git.d.ts +32 -0
- package/dist/git.js +60 -0
- package/dist/json-schema.js +1 -3
- package/dist/landed.js +11 -22
- package/dist/paused-operation.d.ts +2 -0
- package/dist/paused-operation.js +17 -0
- package/dist/pins.js +2 -1
- package/dist/pr/decide.d.ts +4 -16
- package/dist/pr/decide.js +48 -84
- package/dist/pr/delta.d.ts +3 -4
- package/dist/pr/delta.js +10 -19
- package/dist/pr/read.d.ts +27 -30
- package/dist/pr/read.js +94 -182
- package/dist/pr/reply.js +3 -5
- package/dist/pr/route.d.ts +11 -3
- package/dist/pr/route.js +5 -5
- package/dist/pr/standing.d.ts +138 -0
- package/dist/pr/standing.js +181 -0
- package/dist/pr/state.d.ts +10 -69
- package/dist/pr/state.js +42 -72
- package/dist/pr/upstream.d.ts +1 -6
- package/dist/pr/upstream.js +2 -2
- package/dist/primitives-diff.js +1 -1
- package/dist/recap-next.js +34 -47
- package/dist/recap-primitives.js +1 -1
- package/dist/references-key.d.ts +0 -1
- package/dist/references-key.js +3 -4
- package/dist/run-command.d.ts +4 -1
- package/dist/run-command.js +32 -11
- package/dist/setup-gate.d.ts +1 -7
- package/dist/setup-rules.js +2 -6
- package/dist/setup-writer.d.ts +32 -0
- package/dist/setup-writer.js +60 -0
- package/dist/shell-line.d.ts +2 -0
- package/dist/shell-line.js +5 -0
- package/dist/sweep-removal.d.ts +31 -0
- package/dist/sweep-removal.js +133 -0
- package/dist/sweep-terminals.d.ts +1 -1
- package/dist/sweep-terminals.js +21 -57
- package/dist/sweep-trees.d.ts +19 -3
- package/dist/sweep-trees.js +67 -31
- package/dist/worker/herdr.d.ts +67 -23
- package/dist/worker/herdr.js +126 -77
- package/dist/worker/launch.js +32 -45
- package/dist/worker/origin.d.ts +1 -1
- package/dist/worker/origin.js +9 -11
- package/dist/worker/tabs.js +11 -7
- package/dist/working-tree.d.ts +0 -8
- package/dist/working-tree.js +6 -22
- package/dist/worktree-lock.js +1 -1
- package/package.json +1 -1
- package/dist/git-env.d.ts +0 -2
- 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
|
|
3
|
-
//
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
160
|
-
|
|
161
|
-
return
|
|
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
|
|
165
|
-
*
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
28
|
+
if (!isObject(value)) {
|
|
31
29
|
throw new ReplyError(`${at} is ${shown(value)}, not an object`);
|
|
32
30
|
}
|
|
33
31
|
return { value, at };
|
package/dist/pr/route.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { ReviewDepth } from "../agentic-config.ts";
|
|
2
|
-
|
|
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
|
|
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;
|