@nanonets/graft 0.15.0 → 0.18.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/README.md +17 -5
- package/dist/ai/crux.d.ts +14 -1
- package/dist/ai/crux.d.ts.map +1 -1
- package/dist/ai/crux.js +31 -1
- package/dist/ai/crux.js.map +1 -1
- package/dist/ai/failure.d.ts +13 -2
- package/dist/ai/failure.d.ts.map +1 -1
- package/dist/ai/failure.js +16 -4
- package/dist/ai/failure.js.map +1 -1
- package/dist/ai/llm/factory.d.ts +6 -1
- package/dist/ai/llm/factory.d.ts.map +1 -1
- package/dist/ai/llm/factory.js +8 -0
- package/dist/ai/llm/factory.js.map +1 -1
- package/dist/ai/llm/orcarouter.d.ts +34 -0
- package/dist/ai/llm/orcarouter.d.ts.map +1 -0
- package/dist/ai/llm/orcarouter.js +45 -0
- package/dist/ai/llm/orcarouter.js.map +1 -0
- package/dist/ai/providers.d.ts.map +1 -1
- package/dist/ai/providers.js +13 -3
- package/dist/ai/providers.js.map +1 -1
- package/dist/app/brain-build.d.ts +66 -0
- package/dist/app/brain-build.d.ts.map +1 -0
- package/dist/app/brain-build.js +169 -0
- package/dist/app/brain-build.js.map +1 -0
- package/dist/app/checkout.d.ts +53 -3
- package/dist/app/checkout.d.ts.map +1 -1
- package/dist/app/checkout.js +195 -27
- package/dist/app/checkout.js.map +1 -1
- package/dist/app/events.d.ts +7 -0
- package/dist/app/events.d.ts.map +1 -1
- package/dist/app/events.js +7 -2
- package/dist/app/events.js.map +1 -1
- package/dist/app/history.d.ts +128 -0
- package/dist/app/history.d.ts.map +1 -0
- package/dist/app/history.js +269 -0
- package/dist/app/history.js.map +1 -0
- package/dist/app/identity.d.ts +33 -0
- package/dist/app/identity.d.ts.map +1 -1
- package/dist/app/identity.js +72 -0
- package/dist/app/identity.js.map +1 -1
- package/dist/app/main.js +12 -0
- package/dist/app/main.js.map +1 -1
- package/dist/app/pages.d.ts +17 -0
- package/dist/app/pages.d.ts.map +1 -1
- package/dist/app/pages.js +112 -5
- package/dist/app/pages.js.map +1 -1
- package/dist/app/review-process.d.ts +66 -0
- package/dist/app/review-process.d.ts.map +1 -0
- package/dist/app/review-process.js +199 -0
- package/dist/app/review-process.js.map +1 -0
- package/dist/app/review-worker.d.ts +2 -0
- package/dist/app/review-worker.d.ts.map +1 -0
- package/dist/app/review-worker.js +108 -0
- package/dist/app/review-worker.js.map +1 -0
- package/dist/app/review.d.ts.map +1 -1
- package/dist/app/review.js +14 -1
- package/dist/app/review.js.map +1 -1
- package/dist/app/server.d.ts +22 -2
- package/dist/app/server.d.ts.map +1 -1
- package/dist/app/server.js +77 -3
- package/dist/app/server.js.map +1 -1
- package/dist/app/sources.d.ts +68 -0
- package/dist/app/sources.d.ts.map +1 -0
- package/dist/app/sources.js +341 -0
- package/dist/app/sources.js.map +1 -0
- package/dist/ask/ask.d.ts +6 -0
- package/dist/ask/ask.d.ts.map +1 -1
- package/dist/ask/ask.js +30 -2
- package/dist/ask/ask.js.map +1 -1
- package/dist/blast/blast-cli.d.ts.map +1 -1
- package/dist/blast/blast-cli.js +6 -30
- package/dist/blast/blast-cli.js.map +1 -1
- package/dist/blast/name.d.ts +13 -0
- package/dist/blast/name.d.ts.map +1 -1
- package/dist/blast/name.js +36 -0
- package/dist/blast/name.js.map +1 -1
- package/dist/brain/attach.d.ts +44 -0
- package/dist/brain/attach.d.ts.map +1 -0
- package/dist/brain/attach.js +63 -0
- package/dist/brain/attach.js.map +1 -0
- package/dist/brain/connect.d.ts +59 -0
- package/dist/brain/connect.d.ts.map +1 -0
- package/dist/brain/connect.js +80 -0
- package/dist/brain/connect.js.map +1 -0
- package/dist/brain/link.d.ts +82 -0
- package/dist/brain/link.d.ts.map +1 -0
- package/dist/brain/link.js +136 -0
- package/dist/brain/link.js.map +1 -0
- package/dist/brain/push.d.ts +72 -0
- package/dist/brain/push.d.ts.map +1 -0
- package/dist/brain/push.js +191 -0
- package/dist/brain/push.js.map +1 -0
- package/dist/brain/wire.d.ts +38 -0
- package/dist/brain/wire.d.ts.map +1 -0
- package/dist/brain/wire.js +133 -0
- package/dist/brain/wire.js.map +1 -0
- package/dist/claude/format.d.ts.map +1 -1
- package/dist/claude/format.js +10 -3
- package/dist/claude/format.js.map +1 -1
- package/dist/claude/hooks.d.ts.map +1 -1
- package/dist/claude/hooks.js +125 -24
- package/dist/claude/hooks.js.map +1 -1
- package/dist/claude/init.d.ts +6 -0
- package/dist/claude/init.d.ts.map +1 -1
- package/dist/claude/init.js +10 -2
- package/dist/claude/init.js.map +1 -1
- package/dist/claude/session-metrics.d.ts +73 -0
- package/dist/claude/session-metrics.d.ts.map +1 -0
- package/dist/claude/session-metrics.js +206 -0
- package/dist/claude/session-metrics.js.map +1 -0
- package/dist/claude/settings-merge.d.ts +25 -1
- package/dist/claude/settings-merge.d.ts.map +1 -1
- package/dist/claude/settings-merge.js +88 -25
- package/dist/claude/settings-merge.js.map +1 -1
- package/dist/claude/skill-template.d.ts.map +1 -1
- package/dist/claude/skill-template.js +6 -0
- package/dist/claude/skill-template.js.map +1 -1
- package/dist/claude/state.d.ts +26 -0
- package/dist/claude/state.d.ts.map +1 -1
- package/dist/claude/state.js +14 -1
- package/dist/claude/state.js.map +1 -1
- package/dist/claude/tally.d.ts +27 -0
- package/dist/claude/tally.d.ts.map +1 -1
- package/dist/claude/tally.js +74 -1
- package/dist/claude/tally.js.map +1 -1
- package/dist/cli.js +237 -4
- package/dist/cli.js.map +1 -1
- package/dist/context/build.d.ts +11 -0
- package/dist/context/build.d.ts.map +1 -1
- package/dist/context/build.js +27 -6
- package/dist/context/build.js.map +1 -1
- package/dist/context/check.d.ts.map +1 -1
- package/dist/context/check.js +5 -13
- package/dist/context/check.js.map +1 -1
- package/dist/context/node-file.d.ts +1 -1
- package/dist/context/node-file.d.ts.map +1 -1
- package/dist/context/node-file.js +38 -6
- package/dist/context/node-file.js.map +1 -1
- package/dist/context/price.d.ts +49 -0
- package/dist/context/price.d.ts.map +1 -0
- package/dist/context/price.js +83 -0
- package/dist/context/price.js.map +1 -0
- package/dist/context/savings.d.ts +20 -2
- package/dist/context/savings.d.ts.map +1 -1
- package/dist/context/savings.js +57 -4
- package/dist/context/savings.js.map +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -0
- package/dist/engine.js.map +1 -1
- package/dist/graph/cards.d.ts +4 -3
- package/dist/graph/cards.d.ts.map +1 -1
- package/dist/graph/cards.js +34 -6
- package/dist/graph/cards.js.map +1 -1
- package/dist/graph/enrich.d.ts +4 -4
- package/dist/graph/enrich.d.ts.map +1 -1
- package/dist/graph/enrich.js +32 -6
- package/dist/graph/enrich.js.map +1 -1
- package/dist/graph/generic.d.ts +10 -0
- package/dist/graph/generic.d.ts.map +1 -1
- package/dist/graph/generic.js +20 -2
- package/dist/graph/generic.js.map +1 -1
- package/dist/graph/workspace.d.ts +2 -0
- package/dist/graph/workspace.d.ts.map +1 -1
- package/dist/graph/workspace.js +173 -6
- package/dist/graph/workspace.js.map +1 -1
- package/dist/hosts/antigravity.d.ts +2 -2
- package/dist/hosts/antigravity.d.ts.map +1 -1
- package/dist/hosts/claude-global.d.ts +33 -0
- package/dist/hosts/claude-global.d.ts.map +1 -0
- package/dist/hosts/claude-global.js +118 -0
- package/dist/hosts/claude-global.js.map +1 -0
- package/dist/hosts/codex-hooks.d.ts +2 -6
- package/dist/hosts/codex-hooks.d.ts.map +1 -1
- package/dist/hosts/codex-hooks.js +7 -30
- package/dist/hosts/codex-hooks.js.map +1 -1
- package/dist/hosts/config-write.d.ts +36 -0
- package/dist/hosts/config-write.d.ts.map +1 -0
- package/dist/hosts/config-write.js +63 -0
- package/dist/hosts/config-write.js.map +1 -0
- package/dist/hosts/cursor-hooks.d.ts +18 -0
- package/dist/hosts/cursor-hooks.d.ts.map +1 -0
- package/dist/hosts/cursor-hooks.js +100 -0
- package/dist/hosts/cursor-hooks.js.map +1 -0
- package/dist/hosts/init.d.ts +2 -2
- package/dist/hosts/init.d.ts.map +1 -1
- package/dist/hosts/init.js +8 -2
- package/dist/hosts/init.js.map +1 -1
- package/dist/hosts/mcp-config.d.ts +3 -5
- package/dist/hosts/mcp-config.d.ts.map +1 -1
- package/dist/hosts/mcp-config.js +5 -10
- package/dist/hosts/mcp-config.js.map +1 -1
- package/dist/hosts/plan.d.ts.map +1 -1
- package/dist/hosts/plan.js +7 -1
- package/dist/hosts/plan.js.map +1 -1
- package/dist/hosts/retract.d.ts.map +1 -1
- package/dist/hosts/retract.js +33 -13
- package/dist/hosts/retract.js.map +1 -1
- package/dist/hosts/sections.d.ts +23 -2
- package/dist/hosts/sections.d.ts.map +1 -1
- package/dist/hosts/sections.js +25 -9
- package/dist/hosts/sections.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/ingest/fs.d.ts +20 -0
- package/dist/ingest/fs.d.ts.map +1 -1
- package/dist/ingest/fs.js +76 -3
- package/dist/ingest/fs.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +33 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-names.d.ts +20 -0
- package/dist/mcp/tool-names.d.ts.map +1 -0
- package/dist/mcp/tool-names.js +36 -0
- package/dist/mcp/tool-names.js.map +1 -0
- package/dist/mcp/tools.d.ts +11 -2
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +14 -22
- package/dist/mcp/tools.js.map +1 -1
- package/dist/telemetry/contract.d.ts.map +1 -1
- package/dist/telemetry/contract.js +10 -1
- package/dist/telemetry/contract.js.map +1 -1
- package/dist/telemetry/flush.d.ts.map +1 -1
- package/dist/telemetry/flush.js +1 -0
- package/dist/telemetry/flush.js.map +1 -1
- package/dist/telemetry/gate.d.ts +14 -0
- package/dist/telemetry/gate.d.ts.map +1 -1
- package/dist/telemetry/gate.js +18 -2
- package/dist/telemetry/gate.js.map +1 -1
- package/dist/telemetry/identity.d.ts +7 -0
- package/dist/telemetry/identity.d.ts.map +1 -1
- package/dist/telemetry/identity.js.map +1 -1
- package/dist/telemetry/index.d.ts +2 -2
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +2 -2
- package/dist/telemetry/index.js.map +1 -1
- package/dist/telemetry/sessions.d.ts +19 -2
- package/dist/telemetry/sessions.d.ts.map +1 -1
- package/dist/telemetry/sessions.js +57 -40
- package/dist/telemetry/sessions.js.map +1 -1
- package/dist/telemetry/track.d.ts +22 -3
- package/dist/telemetry/track.d.ts.map +1 -1
- package/dist/telemetry/track.js +34 -5
- package/dist/telemetry/track.js.map +1 -1
- package/dist/upkeep-run.d.ts.map +1 -1
- package/dist/upkeep-run.js +12 -2
- package/dist/upkeep-run.js.map +1 -1
- package/dist/upkeep.d.ts +25 -4
- package/dist/upkeep.d.ts.map +1 -1
- package/dist/upkeep.js +40 -1
- package/dist/upkeep.js.map +1 -1
- package/dist/util/state.d.ts +9 -0
- package/dist/util/state.d.ts.map +1 -1
- package/dist/util/state.js.map +1 -1
- package/package.json +2 -2
- package/scripts/postinstall.mjs +42 -3
- package/scripts/run-tests.mjs +14 -0
package/dist/app/pages.js
CHANGED
|
@@ -10,23 +10,38 @@
|
|
|
10
10
|
* capability, not a login. Anyone with the link can read that one page until it
|
|
11
11
|
* expires, which is the same property a GitHub artifact link has, and the link
|
|
12
12
|
* only ever appears in a comment on the pull request it describes.
|
|
13
|
+
*
|
|
14
|
+
* Pages are mirrored to `dir`, when one is given, because the token is DERIVED
|
|
15
|
+
* from the id rather than stored: a link keeps passing the signature check for as
|
|
16
|
+
* long as the secret lives, so a process that lost its pages answers 404 to a
|
|
17
|
+
* link that looks perfectly valid. That failure is silent at both ends — the
|
|
18
|
+
* comment still sits on the pull request, the reviewer just sees "not found" —
|
|
19
|
+
* and one restart of a container with nothing mounted stranded 29 of them. Disk
|
|
20
|
+
* is the fix; without a directory this behaves exactly as it did before.
|
|
13
21
|
*/
|
|
14
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
22
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
23
|
+
import { mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
15
25
|
/** Long enough to review a PR at leisure, short enough that a leaked link dies. */
|
|
16
26
|
const DEFAULT_TTL_MS = 14 * 24 * 60 * 60 * 1000;
|
|
17
27
|
/** A page is ~50 kB; a few hundred of them is nothing, and eviction is by age. */
|
|
18
28
|
const DEFAULT_MAX_PAGES = 500;
|
|
29
|
+
/** One file per page, named by its id. The extension also means "fully written". */
|
|
30
|
+
const SUFFIX = ".json";
|
|
19
31
|
export class PageStore {
|
|
20
32
|
pages = new Map();
|
|
21
33
|
secret;
|
|
22
34
|
ttlMs;
|
|
23
35
|
maxPages;
|
|
24
36
|
now;
|
|
37
|
+
dir;
|
|
25
38
|
constructor(opts) {
|
|
26
39
|
this.secret = opts.secret;
|
|
27
40
|
this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
28
41
|
this.maxPages = opts.maxPages ?? DEFAULT_MAX_PAGES;
|
|
29
42
|
this.now = opts.now ?? Date.now;
|
|
43
|
+
this.dir = opts.dir;
|
|
44
|
+
this.hydrate();
|
|
30
45
|
}
|
|
31
46
|
/** `owner/repo#number` → a path segment safe to put in a URL. */
|
|
32
47
|
static idFor(owner, repo, number) {
|
|
@@ -35,7 +50,9 @@ export class PageStore {
|
|
|
35
50
|
/** Store (replacing any previous page for the same PR) and return `id` + `token`. */
|
|
36
51
|
put(owner, repo, number, html) {
|
|
37
52
|
const id = PageStore.idFor(owner, repo, number);
|
|
38
|
-
|
|
53
|
+
const page = { html, storedMs: this.now() };
|
|
54
|
+
this.pages.set(id, page);
|
|
55
|
+
this.write(id, page);
|
|
39
56
|
this.evict();
|
|
40
57
|
return { id, token: this.sign(id) };
|
|
41
58
|
}
|
|
@@ -45,13 +62,16 @@ export class PageStore {
|
|
|
45
62
|
* Expiry is checked against the STORED time rather than encoded in the token,
|
|
46
63
|
* so a link cannot outlive the data it points at — and re-reviewing a PR
|
|
47
64
|
* refreshes both together.
|
|
65
|
+
*
|
|
66
|
+
* Answered from memory even when there is a directory: `id` comes off a URL,
|
|
67
|
+
* and an id that never reaches the filesystem cannot be walked out of it.
|
|
48
68
|
*/
|
|
49
69
|
get(id, token) {
|
|
50
70
|
const page = this.pages.get(id);
|
|
51
71
|
if (!page || !token || !this.valid(id, token))
|
|
52
72
|
return null;
|
|
53
73
|
if (this.now() - page.storedMs > this.ttlMs) {
|
|
54
|
-
this.
|
|
74
|
+
this.remove(id);
|
|
55
75
|
return null;
|
|
56
76
|
}
|
|
57
77
|
return page.html;
|
|
@@ -74,13 +94,100 @@ export class PageStore {
|
|
|
74
94
|
const cutoff = this.now() - this.ttlMs;
|
|
75
95
|
for (const [id, page] of this.pages)
|
|
76
96
|
if (page.storedMs < cutoff)
|
|
77
|
-
this.
|
|
97
|
+
this.remove(id);
|
|
78
98
|
while (this.pages.size > this.maxPages) {
|
|
79
99
|
const oldest = this.pages.keys().next();
|
|
80
100
|
if (oldest.done)
|
|
81
101
|
break;
|
|
82
|
-
this.
|
|
102
|
+
this.remove(oldest.value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Take back what a previous process left on disk. */
|
|
106
|
+
hydrate() {
|
|
107
|
+
const dir = this.dir;
|
|
108
|
+
if (!dir)
|
|
109
|
+
return;
|
|
110
|
+
let names;
|
|
111
|
+
try {
|
|
112
|
+
mkdirSync(dir, { recursive: true });
|
|
113
|
+
names = readdirSync(dir);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
// Nothing usable there: carry on in memory, which is exactly what every
|
|
117
|
+
// deployment without a directory does anyway.
|
|
118
|
+
console.warn(`page store: ${dir} is unusable, pages will not survive a restart (${reason(err)})`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const loaded = [];
|
|
122
|
+
for (const name of names) {
|
|
123
|
+
// The suffix also skips the temp files a write leaves behind when it dies
|
|
124
|
+
// partway, so a half-written page is never a candidate to begin with.
|
|
125
|
+
if (!name.endsWith(SUFFIX))
|
|
126
|
+
continue;
|
|
127
|
+
const page = this.read(join(dir, name));
|
|
128
|
+
if (page)
|
|
129
|
+
loaded.push([name.slice(0, -SUFFIX.length), page]);
|
|
130
|
+
}
|
|
131
|
+
// Oldest first, because eviction takes the front of the Map and readdir order
|
|
132
|
+
// is the filesystem's business rather than a record of what happened when.
|
|
133
|
+
loaded.sort((a, b) => a[1].storedMs - b[1].storedMs);
|
|
134
|
+
for (const [id, page] of loaded)
|
|
135
|
+
this.pages.set(id, page);
|
|
136
|
+
// A restart is also when pages that expired while the process was down go.
|
|
137
|
+
this.evict();
|
|
138
|
+
}
|
|
139
|
+
/** A stored page, or nothing: truncated, corrupt and gone are one answer here. */
|
|
140
|
+
read(file) {
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
143
|
+
const { html, storedMs } = (parsed ?? {});
|
|
144
|
+
if (typeof html !== "string" || typeof storedMs !== "number" || !Number.isFinite(storedMs))
|
|
145
|
+
return null;
|
|
146
|
+
return { html, storedMs };
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Temp file, then rename: a reader only ever sees a whole page, and two reviews
|
|
154
|
+
* of the same pull request racing each other leave one of them intact rather
|
|
155
|
+
* than the two interleaved.
|
|
156
|
+
*/
|
|
157
|
+
write(id, page) {
|
|
158
|
+
const dir = this.dir;
|
|
159
|
+
if (!dir)
|
|
160
|
+
return;
|
|
161
|
+
const tmp = join(dir, `${id}.${randomBytes(6).toString("hex")}.tmp`);
|
|
162
|
+
try {
|
|
163
|
+
mkdirSync(dir, { recursive: true });
|
|
164
|
+
writeFileSync(tmp, JSON.stringify(page));
|
|
165
|
+
renameSync(tmp, join(dir, `${id}${SUFFIX}`));
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
// The page is in memory and its link works; it just will not outlive this
|
|
169
|
+
// process. Worth a line in the log and nothing more: throwing here would
|
|
170
|
+
// fail a review that has already done all of its work.
|
|
171
|
+
console.warn(`page store: could not write ${id} (${reason(err)})`);
|
|
172
|
+
try {
|
|
173
|
+
unlinkSync(tmp);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Never created, or already renamed into place.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
remove(id) {
|
|
181
|
+
this.pages.delete(id);
|
|
182
|
+
if (!this.dir)
|
|
183
|
+
return;
|
|
184
|
+
try {
|
|
185
|
+
unlinkSync(join(this.dir, `${id}${SUFFIX}`));
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Already gone: deleted underneath us, or it never reached the disk.
|
|
83
189
|
}
|
|
84
190
|
}
|
|
85
191
|
}
|
|
192
|
+
const reason = (err) => (err instanceof Error ? err.message : String(err));
|
|
86
193
|
//# sourceMappingURL=pages.js.map
|
package/dist/app/pages.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pages.js","sourceRoot":"","sources":["../../src/app/pages.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pages.js","sourceRoot":"","sources":["../../src/app/pages.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type ReviewJob } from "./events.js";
|
|
2
|
+
import type { reviewPullRequest, ReviewResult } from "./review.js";
|
|
3
|
+
/** Parent → child, once, immediately after the fork. */
|
|
4
|
+
export interface StartMessage {
|
|
5
|
+
t: "start";
|
|
6
|
+
job: ReviewJob;
|
|
7
|
+
/** Minted by the parent; the child never sees the App's private key. */
|
|
8
|
+
token: string;
|
|
9
|
+
api?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Parent → child: the URL a `publish` request ended up at, or null. */
|
|
12
|
+
export interface PublishedMessage {
|
|
13
|
+
t: "published";
|
|
14
|
+
seq: number;
|
|
15
|
+
url: string | null;
|
|
16
|
+
}
|
|
17
|
+
export type ToChild = StartMessage | PublishedMessage;
|
|
18
|
+
/** Child → parent: a line for the App's log, so a review is still traceable. */
|
|
19
|
+
export interface LogMessage {
|
|
20
|
+
t: "log";
|
|
21
|
+
msg: string;
|
|
22
|
+
}
|
|
23
|
+
/** Child → parent: store this page and tell me its URL. */
|
|
24
|
+
export interface PublishMessage {
|
|
25
|
+
t: "publish";
|
|
26
|
+
seq: number;
|
|
27
|
+
html: string;
|
|
28
|
+
}
|
|
29
|
+
export interface DoneMessage {
|
|
30
|
+
t: "done";
|
|
31
|
+
result: ReviewResult;
|
|
32
|
+
}
|
|
33
|
+
export interface FailedMessage {
|
|
34
|
+
t: "failed";
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
export type FromChild = LogMessage | PublishMessage | DoneMessage | FailedMessage;
|
|
38
|
+
export interface ChildReviewerOptions {
|
|
39
|
+
/** The module the child runs. Only tests pass this. */
|
|
40
|
+
entry?: string;
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The module a review runs in.
|
|
45
|
+
*
|
|
46
|
+
* `dist/app/review-worker.js` in an installed or containerised App. Run from a
|
|
47
|
+
* checkout there is no `dist`, so fall back to the TypeScript source next door:
|
|
48
|
+
* `fork` inherits `process.execArgv`, so a parent started through tsx hands the
|
|
49
|
+
* child the same loader and a `.ts` entry runs. Resolved once, at import.
|
|
50
|
+
*
|
|
51
|
+
* Exported so a test can assert the path a deployed App will fork actually exists —
|
|
52
|
+
* getting this wrong is the one failure here that no unit test of the protocol
|
|
53
|
+
* would catch and every review would hit.
|
|
54
|
+
*/
|
|
55
|
+
export declare function reviewWorkerEntry(): string;
|
|
56
|
+
/**
|
|
57
|
+
* A reviewer with `reviewPullRequest`'s signature that runs it in a child process.
|
|
58
|
+
*
|
|
59
|
+
* Same shape on purpose: `server.ts` swaps one for the other in a single line, and
|
|
60
|
+
* every existing test that injects its own reviewer through `AppSeams.review` keeps
|
|
61
|
+
* running in-process, which is what a test wants.
|
|
62
|
+
*/
|
|
63
|
+
export declare function childReviewer(opts?: ChildReviewerOptions): typeof reviewPullRequest;
|
|
64
|
+
/** The App's default reviewer. */
|
|
65
|
+
export declare const reviewInChildProcess: typeof reviewPullRequest;
|
|
66
|
+
//# sourceMappingURL=review-process.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-process.d.ts","sourceRoot":"","sources":["../../src/app/review-process.ts"],"names":[],"mappings":"AAqDA,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAc,YAAY,EAAE,MAAM,aAAa,CAAC;AAa/E,wDAAwD;AACxD,MAAM,WAAW,YAAY;IAC3B,CAAC,EAAE,OAAO,CAAC;IACX,GAAG,EAAE,SAAS,CAAC;IACf,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,WAAW,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,MAAM,OAAO,GAAG,YAAY,GAAG,gBAAgB,CAAC;AAEtD,gFAAgF;AAChF,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,KAAK,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;CACb;AAED,2DAA2D;AAC3D,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,SAAS,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,CAAC,EAAE,QAAQ,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,cAAc,GAAG,WAAW,GAAG,aAAa,CAAC;AAElF,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAK1C;AAsCD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,GAAE,oBAAyB,GAAG,OAAO,iBAAiB,CAIvF;AAoFD,kCAAkC;AAClC,eAAO,MAAM,oBAAoB,0BAAkB,CAAC"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running a review somewhere it cannot stop the App answering.
|
|
3
|
+
*
|
|
4
|
+
* A review is synchronous from end to end, and not in one place we could fix: the
|
|
5
|
+
* pull request is fetched with `spawnSync` (`./checkout.ts`), the whole repository
|
|
6
|
+
* is parsed in one `files.forEach` loop of native tree-sitter calls
|
|
7
|
+
* (`graph/build.ts`), the graph is written with a blocking `JSON.stringify`, and
|
|
8
|
+
* the viewer page is exported synchronously too. In-process that holds the event
|
|
9
|
+
* loop for the entire review.
|
|
10
|
+
*
|
|
11
|
+
* That is not a theory. On the deployed App, probing `GET /p/<id>` from outside
|
|
12
|
+
* every ten seconds while one review ran: three consecutive timeouts (no response
|
|
13
|
+
* at all, not even with `--max-time 45`), then 404 in 0.9s the moment the queue
|
|
14
|
+
* emptied. Meanwhile the box's load average was 0.96 on eight vCPUs — one thread
|
|
15
|
+
* pegged, seven cores idle — and the App's own logs put a single review at 92s to
|
|
16
|
+
* 274s ("3 changed → 0 affected in 180134ms"). So a *single* in-flight review took
|
|
17
|
+
* the whole HTTP surface down for minutes: the container's 5s `HEALTHCHECK` failed,
|
|
18
|
+
* Docker marked the container unhealthy, and pages the App had already produced
|
|
19
|
+
* were unreachable. On a busy repository that is most of the working day.
|
|
20
|
+
*
|
|
21
|
+
* Neither bounding the queue nor lowering `GRAFT_CONCURRENCY` touches this —
|
|
22
|
+
* concurrency was already 2 and one job is enough. Cooperative yielding inside the
|
|
23
|
+
* parse loop does not either: a `git fetch` of a large repository is one
|
|
24
|
+
* uninterruptible `spawnSync` that can legitimately take a minute, and it is not
|
|
25
|
+
* even using a core while it blocks. The work has to leave the process.
|
|
26
|
+
*
|
|
27
|
+
* A child process rather than a `worker_threads` worker, deliberately:
|
|
28
|
+
*
|
|
29
|
+
* - **The native grammars keep loading the way they already do.** `graph/extract.ts`
|
|
30
|
+
* imports nine tree-sitter addons at module scope and constructs a `Parser` there
|
|
31
|
+
* too, and `graph/generic.ts` loads more through `createRequire`. In a worker all
|
|
32
|
+
* of that is re-instantiated per thread, which puts the N-API-in-worker behaviour
|
|
33
|
+
* of nine third-party native modules between the App and every review. A fork runs
|
|
34
|
+
* byte-identical code to what production runs today.
|
|
35
|
+
* - **A review can be killed.** A wedged parse or a 10-minute clone is a process
|
|
36
|
+
* to SIGKILL, not a thread with no safe way to stop it.
|
|
37
|
+
* - **A crash stops being an outage.** An OOM kill or a grammar segfault on a
|
|
38
|
+
* stranger's source now costs one review and is reported as a failure, instead
|
|
39
|
+
* of taking the HTTP server with it. This process clones code written by
|
|
40
|
+
* people who opened a pull request; that isolation is worth having on its own.
|
|
41
|
+
* - **The heap goes away.** A repository's graph is freed by the child exiting
|
|
42
|
+
* rather than left for the server process to fragment around.
|
|
43
|
+
*
|
|
44
|
+
* The parent keeps exactly the two things a review cannot mint for itself, and
|
|
45
|
+
* hands each across the IPC channel rather than argv or the environment (`ps` and
|
|
46
|
+
* `/proc/<pid>/environ` are readable; an installation token belongs in neither):
|
|
47
|
+
* the token, so the App's private key never crosses the boundary, and page
|
|
48
|
+
* publishing, because the store and the secret it signs URLs with live with the
|
|
49
|
+
* server.
|
|
50
|
+
*/
|
|
51
|
+
import { fork } from "node:child_process";
|
|
52
|
+
import { existsSync } from "node:fs";
|
|
53
|
+
import { fileURLToPath } from "node:url";
|
|
54
|
+
import { jobKey } from "./events.js";
|
|
55
|
+
/**
|
|
56
|
+
* How long one review may take before the process running it is killed.
|
|
57
|
+
*
|
|
58
|
+
* Generously above the worst honest review observed in production (274s) and above
|
|
59
|
+
* what `checkout.ts`'s own 120s-per-git-call budget can add up to, because the cost
|
|
60
|
+
* of being wrong here is a review that never happens. It is a ceiling on a *lost*
|
|
61
|
+
* queue slot, not on responsiveness — the server no longer cares how long a review
|
|
62
|
+
* runs, only that the slot comes back.
|
|
63
|
+
*/
|
|
64
|
+
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
65
|
+
/**
|
|
66
|
+
* The module a review runs in.
|
|
67
|
+
*
|
|
68
|
+
* `dist/app/review-worker.js` in an installed or containerised App. Run from a
|
|
69
|
+
* checkout there is no `dist`, so fall back to the TypeScript source next door:
|
|
70
|
+
* `fork` inherits `process.execArgv`, so a parent started through tsx hands the
|
|
71
|
+
* child the same loader and a `.ts` entry runs. Resolved once, at import.
|
|
72
|
+
*
|
|
73
|
+
* Exported so a test can assert the path a deployed App will fork actually exists —
|
|
74
|
+
* getting this wrong is the one failure here that no unit test of the protocol
|
|
75
|
+
* would catch and every review would hit.
|
|
76
|
+
*/
|
|
77
|
+
export function reviewWorkerEntry() {
|
|
78
|
+
const js = fileURLToPath(new URL("./review-worker.js", import.meta.url));
|
|
79
|
+
if (existsSync(js))
|
|
80
|
+
return js;
|
|
81
|
+
const ts = js.replace(/\.js$/, ".ts");
|
|
82
|
+
return existsSync(ts) ? ts : js;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Live review processes.
|
|
86
|
+
*
|
|
87
|
+
* `main.ts` force-exits 25 seconds after SIGTERM if a review has not finished
|
|
88
|
+
* draining. That used to kill the review along with the process; now the review is
|
|
89
|
+
* a separate process that would outlive us, still holding a clone made with an
|
|
90
|
+
* installation token. So the last thing this process does is take them with it.
|
|
91
|
+
*
|
|
92
|
+
* A container stop is unaffected either way: Docker signals PID 1, not the process
|
|
93
|
+
* group, so a review keeps going through the grace period and the drain in `main.ts`
|
|
94
|
+
* means what it says. Ctrl-C in a terminal signals the whole group, so the children
|
|
95
|
+
* die with the default disposition and their reviews are reported as killed — which
|
|
96
|
+
* is what an interrupted review was before this too.
|
|
97
|
+
*/
|
|
98
|
+
const live = new Set();
|
|
99
|
+
let hooked = false;
|
|
100
|
+
function track(child) {
|
|
101
|
+
live.add(child);
|
|
102
|
+
if (hooked)
|
|
103
|
+
return;
|
|
104
|
+
hooked = true;
|
|
105
|
+
process.on("exit", () => {
|
|
106
|
+
for (const c of live)
|
|
107
|
+
c.kill("SIGKILL");
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** A dead channel is not worth an exception: the exit handler already covers it. */
|
|
111
|
+
function post(child, msg) {
|
|
112
|
+
if (!child.connected)
|
|
113
|
+
return;
|
|
114
|
+
try {
|
|
115
|
+
child.send(msg);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* channel closed under us — `exit` will settle the review */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A reviewer with `reviewPullRequest`'s signature that runs it in a child process.
|
|
123
|
+
*
|
|
124
|
+
* Same shape on purpose: `server.ts` swaps one for the other in a single line, and
|
|
125
|
+
* every existing test that injects its own reviewer through `AppSeams.review` keeps
|
|
126
|
+
* running in-process, which is what a test wants.
|
|
127
|
+
*/
|
|
128
|
+
export function childReviewer(opts = {}) {
|
|
129
|
+
const entry = opts.entry ?? reviewWorkerEntry();
|
|
130
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
131
|
+
return (job, deps) => runInChild(job, deps, entry, timeoutMs);
|
|
132
|
+
}
|
|
133
|
+
async function runInChild(job, deps, entry, timeoutMs) {
|
|
134
|
+
const log = deps.log ?? (() => { });
|
|
135
|
+
// Minted here rather than in the child: this is a network round trip, never a
|
|
136
|
+
// blocking one, and the cache that owns installation tokens — including the
|
|
137
|
+
// forget-a-rejected-token behaviour — belongs to the one process that sees every
|
|
138
|
+
// delivery.
|
|
139
|
+
const token = await deps.token(job.installationId);
|
|
140
|
+
// The job key as argv makes `ps` in a container say which pull request each
|
|
141
|
+
// review is; the token stays out of it.
|
|
142
|
+
const child = fork(entry, [jobKey(job)], { stdio: ["ignore", "inherit", "inherit", "ipc"] });
|
|
143
|
+
track(child);
|
|
144
|
+
try {
|
|
145
|
+
return await new Promise((resolve, reject) => {
|
|
146
|
+
let settled = false;
|
|
147
|
+
const settle = (fn) => {
|
|
148
|
+
if (settled)
|
|
149
|
+
return;
|
|
150
|
+
settled = true;
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
fn();
|
|
153
|
+
};
|
|
154
|
+
const timer = setTimeout(() => {
|
|
155
|
+
settle(() => reject(new Error(`review exceeded ${timeoutMs}ms and was killed`)));
|
|
156
|
+
}, timeoutMs);
|
|
157
|
+
timer.unref();
|
|
158
|
+
child.on("message", (raw) => {
|
|
159
|
+
const msg = raw;
|
|
160
|
+
if (msg.t === "log") {
|
|
161
|
+
log(msg.msg);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (msg.t === "publish") {
|
|
165
|
+
// Publishing happens here because the store and its signing secret do.
|
|
166
|
+
// A page that cannot be stored costs the link, never the review — the
|
|
167
|
+
// same contract `review.ts` already has with a null `publish`.
|
|
168
|
+
const stored = deps.publish ? deps.publish(job, msg.html) : Promise.resolve(null);
|
|
169
|
+
void stored
|
|
170
|
+
.catch(() => null)
|
|
171
|
+
.then((url) => post(child, { t: "published", seq: msg.seq, url }));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (msg.t === "done") {
|
|
175
|
+
settle(() => resolve(msg.result));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
settle(() => reject(new Error(msg.message)));
|
|
179
|
+
});
|
|
180
|
+
child.on("error", (err) => settle(() => reject(err)));
|
|
181
|
+
// The case that used to be an App-wide outage: a child that dies without
|
|
182
|
+
// reporting. An OOM kill, a segfault in a grammar on hostile source. Now it
|
|
183
|
+
// is one failed review, named, with the signal that did it.
|
|
184
|
+
child.on("exit", (code, signal) => settle(() => reject(new Error(`review process exited ${signal ? `on ${signal}` : `with code ${code}`} before reporting a result`))));
|
|
185
|
+
post(child, { t: "start", job, token, api: deps.api });
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
live.delete(child);
|
|
190
|
+
// Whatever happened — done, failed, timed out, or a delivery superseded by the
|
|
191
|
+
// queue — nothing forked here is allowed to outlive the review. `review.ts`
|
|
192
|
+
// deletes the checkout before it reports, so this leaves nothing behind.
|
|
193
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
194
|
+
child.kill("SIGKILL");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** The App's default reviewer. */
|
|
198
|
+
export const reviewInChildProcess = childReviewer();
|
|
199
|
+
//# sourceMappingURL=review-process.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-process.js","sourceRoot":"","sources":["../../src/app/review-process.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-worker.d.ts","sourceRoot":"","sources":["../../src/app/review-worker.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One review, in its own process. Forked by `./review-process.ts`, never imported.
|
|
3
|
+
*
|
|
4
|
+
* The whole file is glue: it waits for a `start`, calls the same
|
|
5
|
+
* `reviewPullRequest` the server used to call directly, and reports back. Nothing
|
|
6
|
+
* about a review changes by running here — that is the point. The two dependencies
|
|
7
|
+
* it cannot satisfy locally are wired to the parent instead:
|
|
8
|
+
*
|
|
9
|
+
* - `token` is handed over in the start message. This process never holds the
|
|
10
|
+
* App's private key, so a hostile repository that somehow got code running here
|
|
11
|
+
* finds one repository's short-lived token and not the App's identity.
|
|
12
|
+
* - `publish` is a round trip. The page store and the webhook secret its signed
|
|
13
|
+
* URLs are keyed on live with the HTTP server, and a page is only useful to the
|
|
14
|
+
* process that will serve it.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately no `graft` state of its own: no lock, no cache directory, no
|
|
17
|
+
* telemetry. A review works on a throwaway clone in /tmp and takes its findings
|
|
18
|
+
* with it when it exits.
|
|
19
|
+
*/
|
|
20
|
+
import { reviewPullRequest } from "./review.js";
|
|
21
|
+
/** Publish requests waiting on the parent, by sequence number. */
|
|
22
|
+
const awaiting = new Map();
|
|
23
|
+
let seq = 0;
|
|
24
|
+
/** Fire and forget. `process.send` throws on a closed channel, and a log line is
|
|
25
|
+
* not worth crashing a review over — the `disconnect` handler below has already
|
|
26
|
+
* decided what a closed channel means. */
|
|
27
|
+
const send = (msg) => {
|
|
28
|
+
if (!process.connected)
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
process.send?.(msg);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* parent went away mid-review */
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/** Close the channel, or leave if it is already gone. Either way this is the end of
|
|
38
|
+
* the process — see the `disconnect` handler below. */
|
|
39
|
+
function bye() {
|
|
40
|
+
try {
|
|
41
|
+
if (process.connected)
|
|
42
|
+
process.disconnect?.();
|
|
43
|
+
else
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The last word, then close the channel.
|
|
52
|
+
*
|
|
53
|
+
* Sent with a callback because that callback is the flush: `done` has to be on the
|
|
54
|
+
* wire before the channel closes, or the parent sees a process that exited without
|
|
55
|
+
* reporting and turns a finished review into a failure.
|
|
56
|
+
*/
|
|
57
|
+
function finish(msg) {
|
|
58
|
+
if (!process.connected)
|
|
59
|
+
return bye();
|
|
60
|
+
try {
|
|
61
|
+
process.send?.(msg, () => bye());
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
bye();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function run(start) {
|
|
68
|
+
try {
|
|
69
|
+
const result = await reviewPullRequest(start.job, {
|
|
70
|
+
token: async () => start.token,
|
|
71
|
+
fetch: globalThis.fetch,
|
|
72
|
+
api: start.api,
|
|
73
|
+
publish: (_job, html) => new Promise((resolve) => {
|
|
74
|
+
const id = (seq += 1);
|
|
75
|
+
awaiting.set(id, resolve);
|
|
76
|
+
send({ t: "publish", seq: id, html });
|
|
77
|
+
}),
|
|
78
|
+
log: (msg) => send({ t: "log", msg }),
|
|
79
|
+
});
|
|
80
|
+
finish({ t: "done", result });
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
// `reviewPullRequest` has already run the token through `redact`, so this
|
|
84
|
+
// message is safe to put in the parent's log — which is where it is going.
|
|
85
|
+
finish({ t: "failed", message: err instanceof Error ? err.message : String(err) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* No parent, no point.
|
|
90
|
+
*
|
|
91
|
+
* A review whose channel has closed has nowhere to publish a page and nobody to
|
|
92
|
+
* report to. If the server process died, nothing will SIGKILL this one either, and
|
|
93
|
+
* without this it would sit in a clone for the rest of the parent's 15-minute
|
|
94
|
+
* ceiling. Going now leaves the same throwaway tree behind that a SIGKILL would —
|
|
95
|
+
* what it does not do is spend a quarter of an hour producing a review no one can
|
|
96
|
+
* receive.
|
|
97
|
+
*/
|
|
98
|
+
process.on("disconnect", () => process.exit(0));
|
|
99
|
+
process.on("message", (raw) => {
|
|
100
|
+
const msg = raw;
|
|
101
|
+
if (msg.t === "start") {
|
|
102
|
+
void run(msg);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
awaiting.get(msg.seq)?.(msg.url);
|
|
106
|
+
awaiting.delete(msg.seq);
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=review-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-worker.js","sourceRoot":"","sources":["../../src/app/review-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAIhD,kEAAkE;AAClE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAwC,CAAC;AACjE,IAAI,GAAG,GAAG,CAAC,CAAC;AAEZ;;0CAE0C;AAC1C,MAAM,IAAI,GAAG,CAAC,GAAc,EAAQ,EAAE;IACpC,IAAI,CAAC,OAAO,CAAC,SAAS;QAAE,OAAO;IAC/B,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,iCAAiC;IACnC,CAAC;AACH,CAAC,CAAC;AAEF;uDACuD;AACvD,SAAS,GAAG;IACV,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,SAAS;YAAE,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;;YACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,MAAM,CAAC,GAAc;IAC5B,IAAI,CAAC,OAAO,CAAC,SAAS;QAAE,OAAO,GAAG,EAAE,CAAC;IACrC,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,GAAG,EAAE,CAAC;IACR,CAAC;AACH,CAAC;AAED,KAAK,UAAU,GAAG,CAAC,KAAmB;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,KAAK,CAAC,GAAG,EAAE;YAChD,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK;YAC9B,KAAK,EAAE,UAAU,CAAC,KAAyB;YAC3C,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CACtB,IAAI,OAAO,CAAgB,CAAC,OAAO,EAAE,EAAE;gBACrC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;gBACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;gBAC1B,IAAI,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;YACxC,CAAC,CAAC;YACJ,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;SACtC,CAAC,CAAC;QACH,MAAM,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,0EAA0E;QAC1E,2EAA2E;QAC3E,MAAM,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACrF,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,OAAO,CAAC,EAAE,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAEhD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE;IAC5B,MAAM,GAAG,GAAG,GAAc,CAAC;IAC3B,IAAI,GAAG,CAAC,CAAC,KAAK,OAAO,EAAE,CAAC;QACtB,KAAK,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,OAAO;IACT,CAAC;IACD,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC,CAAC,CAAC"}
|
package/dist/app/review.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../../src/app/review.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C,iFAAiF;AACjF,eAAO,MAAM,MAAM,gCAAgC,CAAC;AAIpD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,KAAK,EAAE,KAAK,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;qBAEiB;IACjB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../../src/app/review.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C,iFAAiF;AACjF,eAAO,MAAM,MAAM,gCAAgC,CAAC;AAIpD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,KAAK,EAAE,KAAK,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;qBAEiB;IACjB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAsD/F"}
|
package/dist/app/review.js
CHANGED
|
@@ -21,7 +21,10 @@ const DEPTH = 2;
|
|
|
21
21
|
export async function reviewPullRequest(job, deps) {
|
|
22
22
|
const log = deps.log ?? (() => { });
|
|
23
23
|
const token = await deps.token(job.installationId);
|
|
24
|
-
|
|
24
|
+
// `log` goes into the checkout because a closed pull request is reviewed from
|
|
25
|
+
// `refs/pull/N/head` against a different basis than an open one, and the only
|
|
26
|
+
// place that difference is visible afterwards is this line.
|
|
27
|
+
const checkout = checkoutPullRequest({ owner: job.owner, repo: job.repo, number: job.number, baseRef: job.baseRef, token, log });
|
|
25
28
|
try {
|
|
26
29
|
log(`${job.owner}/${job.repo}#${job.number}: building`);
|
|
27
30
|
await buildGraph(checkout.dir);
|
|
@@ -33,6 +36,16 @@ export async function reviewPullRequest(job, deps) {
|
|
|
33
36
|
if (!diff)
|
|
34
37
|
throw new Error(`no diff against ${job.baseRef}`);
|
|
35
38
|
const report = blastRadiusIn(graph, contextDir, diff.files, diff.basis, DEPTH);
|
|
39
|
+
// Same two passes `graft blast --name` runs, in the same order: a reviewer's
|
|
40
|
+
// reason cites area labels, and naming is what gives an area its final one.
|
|
41
|
+
// Both are best-effort — without GRAFT_API_KEY the areas keep their symbol
|
|
42
|
+
// names and the comment still goes out.
|
|
43
|
+
const { nameReport } = await import("../blast/name.js");
|
|
44
|
+
const { note } = await nameReport(graph, report, contextDir);
|
|
45
|
+
if (note)
|
|
46
|
+
log(`${job.owner}/${job.repo}#${job.number}: ${note}`);
|
|
47
|
+
const { attachOwners, diffAuthors } = await import("../blast/owners.js");
|
|
48
|
+
attachOwners(checkout.dir, report, { exclude: diffAuthors(checkout.dir, checkout.base) });
|
|
36
49
|
// NOTE: once #180 lands, pass `{ root: checkout.dir }` so the collapsed list
|
|
37
50
|
// quotes the reaching line here too. It is additive, and the App works without it.
|
|
38
51
|
let body = `${MARKER}\n${markdownReport(report)}`;
|