@nanonets/graft 0.16.0 → 0.19.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.
Files changed (167) hide show
  1. package/README.md +10 -3
  2. package/dist/app/brain-build-process.d.ts +22 -0
  3. package/dist/app/brain-build-process.d.ts.map +1 -0
  4. package/dist/app/brain-build-process.js +129 -0
  5. package/dist/app/brain-build-process.js.map +1 -0
  6. package/dist/app/brain-build-worker.d.ts +35 -0
  7. package/dist/app/brain-build-worker.d.ts.map +1 -0
  8. package/dist/app/brain-build-worker.js +56 -0
  9. package/dist/app/brain-build-worker.js.map +1 -0
  10. package/dist/app/brain-build.d.ts +160 -0
  11. package/dist/app/brain-build.d.ts.map +1 -0
  12. package/dist/app/brain-build.js +278 -0
  13. package/dist/app/brain-build.js.map +1 -0
  14. package/dist/app/checkout.d.ts +53 -3
  15. package/dist/app/checkout.d.ts.map +1 -1
  16. package/dist/app/checkout.js +195 -27
  17. package/dist/app/checkout.js.map +1 -1
  18. package/dist/app/events.d.ts +7 -0
  19. package/dist/app/events.d.ts.map +1 -1
  20. package/dist/app/events.js +7 -2
  21. package/dist/app/events.js.map +1 -1
  22. package/dist/app/history.d.ts +128 -0
  23. package/dist/app/history.d.ts.map +1 -0
  24. package/dist/app/history.js +266 -0
  25. package/dist/app/history.js.map +1 -0
  26. package/dist/app/identity.d.ts +56 -0
  27. package/dist/app/identity.d.ts.map +1 -1
  28. package/dist/app/identity.js +119 -0
  29. package/dist/app/identity.js.map +1 -1
  30. package/dist/app/main.js +14 -0
  31. package/dist/app/main.js.map +1 -1
  32. package/dist/app/pages.d.ts +17 -0
  33. package/dist/app/pages.d.ts.map +1 -1
  34. package/dist/app/pages.js +112 -5
  35. package/dist/app/pages.js.map +1 -1
  36. package/dist/app/review-process.d.ts +66 -0
  37. package/dist/app/review-process.d.ts.map +1 -0
  38. package/dist/app/review-process.js +199 -0
  39. package/dist/app/review-process.js.map +1 -0
  40. package/dist/app/review-worker.d.ts +2 -0
  41. package/dist/app/review-worker.d.ts.map +1 -0
  42. package/dist/app/review-worker.js +108 -0
  43. package/dist/app/review-worker.js.map +1 -0
  44. package/dist/app/review.d.ts.map +1 -1
  45. package/dist/app/review.js +4 -1
  46. package/dist/app/review.js.map +1 -1
  47. package/dist/app/server.d.ts +31 -2
  48. package/dist/app/server.d.ts.map +1 -1
  49. package/dist/app/server.js +130 -3
  50. package/dist/app/server.js.map +1 -1
  51. package/dist/app/sources.d.ts +68 -0
  52. package/dist/app/sources.d.ts.map +1 -0
  53. package/dist/app/sources.js +342 -0
  54. package/dist/app/sources.js.map +1 -0
  55. package/dist/ask/ask.d.ts +6 -0
  56. package/dist/ask/ask.d.ts.map +1 -1
  57. package/dist/ask/ask.js +30 -2
  58. package/dist/ask/ask.js.map +1 -1
  59. package/dist/brain/attach.d.ts +44 -0
  60. package/dist/brain/attach.d.ts.map +1 -0
  61. package/dist/brain/attach.js +63 -0
  62. package/dist/brain/attach.js.map +1 -0
  63. package/dist/brain/connect.d.ts +59 -0
  64. package/dist/brain/connect.d.ts.map +1 -0
  65. package/dist/brain/connect.js +80 -0
  66. package/dist/brain/connect.js.map +1 -0
  67. package/dist/brain/link.d.ts +89 -0
  68. package/dist/brain/link.d.ts.map +1 -0
  69. package/dist/brain/link.js +146 -0
  70. package/dist/brain/link.js.map +1 -0
  71. package/dist/brain/push.d.ts +72 -0
  72. package/dist/brain/push.d.ts.map +1 -0
  73. package/dist/brain/push.js +191 -0
  74. package/dist/brain/push.js.map +1 -0
  75. package/dist/brain/signup.d.ts +73 -0
  76. package/dist/brain/signup.d.ts.map +1 -0
  77. package/dist/brain/signup.js +186 -0
  78. package/dist/brain/signup.js.map +1 -0
  79. package/dist/brain/watch.d.ts +136 -0
  80. package/dist/brain/watch.d.ts.map +1 -0
  81. package/dist/brain/watch.js +172 -0
  82. package/dist/brain/watch.js.map +1 -0
  83. package/dist/brain/wire.d.ts +38 -0
  84. package/dist/brain/wire.d.ts.map +1 -0
  85. package/dist/brain/wire.js +133 -0
  86. package/dist/brain/wire.js.map +1 -0
  87. package/dist/claude/format.d.ts.map +1 -1
  88. package/dist/claude/format.js +10 -3
  89. package/dist/claude/format.js.map +1 -1
  90. package/dist/claude/hooks.d.ts.map +1 -1
  91. package/dist/claude/hooks.js +32 -1
  92. package/dist/claude/hooks.js.map +1 -1
  93. package/dist/claude/init.d.ts +5 -0
  94. package/dist/claude/init.d.ts.map +1 -1
  95. package/dist/claude/init.js +9 -1
  96. package/dist/claude/init.js.map +1 -1
  97. package/dist/claude/session-metrics.d.ts +10 -0
  98. package/dist/claude/session-metrics.d.ts.map +1 -1
  99. package/dist/claude/session-metrics.js +25 -0
  100. package/dist/claude/session-metrics.js.map +1 -1
  101. package/dist/claude/settings-merge.d.ts +18 -0
  102. package/dist/claude/settings-merge.d.ts.map +1 -1
  103. package/dist/claude/settings-merge.js +42 -8
  104. package/dist/claude/settings-merge.js.map +1 -1
  105. package/dist/claude/skill-template.d.ts.map +1 -1
  106. package/dist/claude/skill-template.js +6 -0
  107. package/dist/claude/skill-template.js.map +1 -1
  108. package/dist/claude/state.d.ts +14 -0
  109. package/dist/claude/state.d.ts.map +1 -1
  110. package/dist/claude/state.js.map +1 -1
  111. package/dist/claude/tally.d.ts +27 -0
  112. package/dist/claude/tally.d.ts.map +1 -1
  113. package/dist/claude/tally.js +74 -1
  114. package/dist/claude/tally.js.map +1 -1
  115. package/dist/cli.js +310 -3
  116. package/dist/cli.js.map +1 -1
  117. package/dist/context/node-file.d.ts +4 -10
  118. package/dist/context/node-file.d.ts.map +1 -1
  119. package/dist/context/node-file.js +6 -2
  120. package/dist/context/node-file.js.map +1 -1
  121. package/dist/context/price.d.ts +49 -0
  122. package/dist/context/price.d.ts.map +1 -0
  123. package/dist/context/price.js +83 -0
  124. package/dist/context/price.js.map +1 -0
  125. package/dist/context/savings.d.ts +13 -3
  126. package/dist/context/savings.d.ts.map +1 -1
  127. package/dist/context/savings.js +44 -5
  128. package/dist/context/savings.js.map +1 -1
  129. package/dist/hosts/claude-global.d.ts +33 -0
  130. package/dist/hosts/claude-global.d.ts.map +1 -0
  131. package/dist/hosts/claude-global.js +118 -0
  132. package/dist/hosts/claude-global.js.map +1 -0
  133. package/dist/hosts/plan.d.ts.map +1 -1
  134. package/dist/hosts/plan.js +5 -1
  135. package/dist/hosts/plan.js.map +1 -1
  136. package/dist/hosts/retract.d.ts.map +1 -1
  137. package/dist/hosts/retract.js +33 -13
  138. package/dist/hosts/retract.js.map +1 -1
  139. package/dist/hosts/sections.d.ts +23 -2
  140. package/dist/hosts/sections.d.ts.map +1 -1
  141. package/dist/hosts/sections.js +25 -9
  142. package/dist/hosts/sections.js.map +1 -1
  143. package/dist/mcp/server.d.ts.map +1 -1
  144. package/dist/mcp/server.js +33 -4
  145. package/dist/mcp/server.js.map +1 -1
  146. package/dist/mcp/tools.d.ts.map +1 -1
  147. package/dist/mcp/tools.js +5 -1
  148. package/dist/mcp/tools.js.map +1 -1
  149. package/dist/telemetry/contract.d.ts +12 -0
  150. package/dist/telemetry/contract.d.ts.map +1 -1
  151. package/dist/telemetry/contract.js +19 -0
  152. package/dist/telemetry/contract.js.map +1 -1
  153. package/dist/telemetry/gate.d.ts +14 -0
  154. package/dist/telemetry/gate.d.ts.map +1 -1
  155. package/dist/telemetry/gate.js +18 -2
  156. package/dist/telemetry/gate.js.map +1 -1
  157. package/dist/upkeep-run.d.ts.map +1 -1
  158. package/dist/upkeep-run.js +12 -2
  159. package/dist/upkeep-run.js.map +1 -1
  160. package/dist/upkeep.d.ts +18 -0
  161. package/dist/upkeep.d.ts.map +1 -1
  162. package/dist/upkeep.js +38 -0
  163. package/dist/upkeep.js.map +1 -1
  164. package/dist/util/state.d.ts +9 -0
  165. package/dist/util/state.d.ts.map +1 -1
  166. package/dist/util/state.js.map +1 -1
  167. package/package.json +1 -1
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=review-worker.d.ts.map
@@ -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":"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"}
@@ -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,CAmD/F"}
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"}
@@ -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
- const checkout = checkoutPullRequest({ owner: job.owner, repo: job.repo, number: job.number, baseRef: job.baseRef, token });
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);
@@ -1 +1 @@
1
- {"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/app/review.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/app/review.ts"],"names":[],"mappings":"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"}
@@ -9,24 +9,53 @@
9
9
  * GitHub retries a delivery it considers failed and gives up after ten seconds,
10
10
  * while a review takes tens of them — so the handler validates, queues, and
11
11
  * answers 202. Everything real happens on the queue.
12
+ *
13
+ * "On the queue" used to mean "on this event loop", and that was the whole of a
14
+ * multi-minute outage: a review is synchronous end to end, so one in flight left
15
+ * this server unable to answer /healthz or serve a page it had already produced.
16
+ * The queue now runs each review in its own process (`./review-process.ts`) and
17
+ * everything in here is I/O again.
12
18
  */
13
19
  import { type Server } from "node:http";
14
20
  import { type ReviewJob } from "./events.js";
15
21
  import { type AppCredentials, type Fetch } from "./identity.js";
22
+ import { buildRepoIntoBrain, checkRepoAccess } from "./brain-build.js";
16
23
  import { PageStore } from "./pages.js";
17
24
  import { WorkQueue } from "./queue.js";
18
- import { reviewPullRequest } from "./review.js";
25
+ import type { reviewPullRequest } from "./review.js";
19
26
  /** Seams for tests: nothing here has a default that touches the network. */
20
27
  export interface AppSeams {
21
28
  fetch?: Fetch;
22
- /** Swapped out to assert what the queue was handed, without a clone. */
29
+ /** Swapped out so a test can assert what the route handed the builder without
30
+ * cloning a repository or reaching GitHub. */
31
+ brainBuild?: typeof buildRepoIntoBrain;
32
+ /** Swapped out so a test can assert the access route's answers without
33
+ * reaching GitHub. */
34
+ brainAccess?: typeof checkRepoAccess;
35
+ /** Swapped out to assert what the queue was handed, without a clone — and, being
36
+ * called in-process, without the fork the default reviewer does. */
23
37
  review?: typeof reviewPullRequest;
24
38
  now?: () => number;
25
39
  }
26
40
  export interface AppConfig extends AppCredentials {
27
41
  webhookSecret: string;
42
+ /** Shared secret for `POST /brain/build`. That route is called by the
43
+ * platform, not by GitHub, so it cannot use the webhook's payload signature —
44
+ * it takes a bearer token instead. Unset disables the route entirely, which
45
+ * is the right default for a deployment that only reviews pull requests. */
46
+ brainBuildSecret?: string;
47
+ /** Platform base URL the digest is posted to. Defaults to production. */
48
+ brainBaseUrl?: string;
49
+ /** Token for reading public repositories the App is not installed on.
50
+ * Optional — without it the read borrows one of our own installation's
51
+ * tokens, and only falls back to anonymous if the App has none. */
52
+ publicToken?: string;
53
+ /** Whose installation to borrow for that. Defaults to ours. */
54
+ publicOwner?: string;
28
55
  /** Public origin, for the links put in comments, e.g. https://graft.example.com */
29
56
  publicUrl: string;
57
+ /** Where pages are kept, so a restart does not strand the links already posted. */
58
+ pageDir?: string;
30
59
  port?: number;
31
60
  concurrency?: number;
32
61
  api?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/app/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AACjG,OAAO,EAAwB,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAuC,KAAK,cAAc,EAAE,KAAK,KAAK,EAAE,MAAM,eAAe,CAAC;AACrG,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAKhD,4EAA4E;AAC5E,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,wEAAwE;IACxE,MAAM,CAAC,EAAE,OAAO,iBAAiB,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAU,SAAQ,cAAc;IAC/C,aAAa,EAAE,MAAM,CAAC;IACtB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,wBAAgB,SAAS,CACvB,MAAM,EAAE,SAAS,EACjB,KAAK,GAAE,QAAa,GACnB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,CAkFnE"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/app/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAEjG,OAAO,EAAwB,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAuC,KAAK,cAAc,EAAE,KAAK,KAAK,EAAE,MAAM,eAAe,CAAC;AACrG,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAA8C,MAAM,kBAAkB,CAAC;AAEnH,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAKrD,4EAA4E;AAC5E,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd;kDAC8C;IAC9C,UAAU,CAAC,EAAE,OAAO,kBAAkB,CAAC;IACvC;0BACsB;IACtB,WAAW,CAAC,EAAE,OAAO,eAAe,CAAC;IACrC;wEACoE;IACpE,MAAM,CAAC,EAAE,OAAO,iBAAiB,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAU,SAAQ,cAAc;IAC/C,aAAa,EAAE,MAAM,CAAC;IACtB;;;gFAG4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;uEAEmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,wBAAgB,SAAS,CACvB,MAAM,EAAE,SAAS,EACjB,KAAK,GAAE,QAAa,GACnB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,CAqLnE"}
@@ -9,21 +9,30 @@
9
9
  * GitHub retries a delivery it considers failed and gives up after ten seconds,
10
10
  * while a review takes tens of them — so the handler validates, queues, and
11
11
  * answers 202. Everything real happens on the queue.
12
+ *
13
+ * "On the queue" used to mean "on this event loop", and that was the whole of a
14
+ * multi-minute outage: a review is synchronous end to end, so one in flight left
15
+ * this server unable to answer /healthz or serve a page it had already produced.
16
+ * The queue now runs each review in its own process (`./review-process.ts`) and
17
+ * everything in here is I/O again.
12
18
  */
13
19
  import { createServer } from "node:http";
20
+ import { timingSafeEqual } from "node:crypto";
14
21
  import { jobKey, reviewJobFor } from "./events.js";
15
22
  import { InstallationTokens, verifySignature } from "./identity.js";
23
+ import { checkRepoAccess, RepoNotAccessibleError } from "./brain-build.js";
24
+ import { buildRepoInChildProcess } from "./brain-build-process.js";
16
25
  import { PageStore } from "./pages.js";
17
26
  import { WorkQueue } from "./queue.js";
18
- import { reviewPullRequest } from "./review.js";
27
+ import { reviewInChildProcess } from "./review-process.js";
19
28
  /** A webhook body larger than this is not a pull_request event we can use. */
20
29
  const MAX_BODY_BYTES = 2 * 1024 * 1024;
21
30
  export function createApp(config, seams = {}) {
22
31
  const log = config.log ?? ((msg) => console.log(msg));
23
32
  const fetchImpl = seams.fetch ?? globalThis.fetch;
24
- const review = seams.review ?? reviewPullRequest;
33
+ const review = seams.review ?? reviewInChildProcess;
25
34
  const tokens = new InstallationTokens(config, fetchImpl, seams.now ?? Date.now, config.api);
26
- const pages = new PageStore({ secret: config.webhookSecret });
35
+ const pages = new PageStore({ secret: config.webhookSecret, dir: config.pageDir });
27
36
  const origin = config.publicUrl.replace(/\/$/, "");
28
37
  const queue = new WorkQueue(async (job) => {
29
38
  const started = Date.now();
@@ -88,6 +97,112 @@ export function createApp(config, seams = {}) {
88
97
  log(`${jobKey(decided.job)}: queued${decided.job.fromFork ? " (fork)" : ""}`);
89
98
  return send(res, 202, "application/json", JSON.stringify({ queued: true }));
90
99
  }
100
+ // Can we read this repository? The cheap question, answered on its own.
101
+ //
102
+ // Same secret as /brain/build, because it is the same caller and the same
103
+ // trust. Kept separate from it because it costs a second where the build
104
+ // costs minutes, and the platform needs the answer before it can decide
105
+ // which screen to show.
106
+ if (req.method === "POST" && url.pathname === "/brain/access") {
107
+ if (!config.brainBuildSecret)
108
+ return send(res, 404, "text/plain", "not found");
109
+ if (!bearerMatches(header(req, "authorization"), config.brainBuildSecret)) {
110
+ return send(res, 401, "text/plain", "unauthorized");
111
+ }
112
+ const body = await readBody(req);
113
+ if (body === null)
114
+ return send(res, 413, "text/plain", "payload too large");
115
+ let ask;
116
+ try {
117
+ ask = JSON.parse(body);
118
+ }
119
+ catch {
120
+ return send(res, 400, "text/plain", "bad json");
121
+ }
122
+ if (!ask.owner || !ask.repo) {
123
+ return send(res, 400, "application/json", JSON.stringify({ error: "owner and repo are required" }));
124
+ }
125
+ try {
126
+ const access = await (seams.brainAccess ?? checkRepoAccess)({ owner: ask.owner, repo: ask.repo }, {
127
+ creds: config,
128
+ fetch: fetchImpl,
129
+ api: config.api,
130
+ publicToken: config.publicToken,
131
+ publicOwner: config.publicOwner,
132
+ log,
133
+ now: seams.now,
134
+ });
135
+ // 200 either way: "we cannot see it" is an answer, not a failure, and
136
+ // the platform turns it into a choice rather than an error.
137
+ return send(res, 200, "application/json", JSON.stringify(access));
138
+ }
139
+ catch (e) {
140
+ const msg = e instanceof Error ? e.message : String(e);
141
+ log(`brain access ${ask.owner}/${ask.repo} failed: ${msg}`);
142
+ return send(res, 502, "application/json", JSON.stringify({ error: msg }));
143
+ }
144
+ }
145
+ // Build one repository into a brain, on demand. Called by the platform when
146
+ // someone pastes a repository URL during onboarding — there is no webhook
147
+ // behind it, so it authenticates with a bearer token rather than a payload
148
+ // signature.
149
+ if (req.method === "POST" && url.pathname === "/brain/build") {
150
+ if (!config.brainBuildSecret)
151
+ return send(res, 404, "text/plain", "not found");
152
+ // Compared in constant time for the same reason verifySignature is: this
153
+ // is a long-lived shared secret, and a timing oracle on it is worth more
154
+ // to an attacker than one on a per-payload signature.
155
+ if (!bearerMatches(header(req, "authorization"), config.brainBuildSecret)) {
156
+ return send(res, 401, "text/plain", "unauthorized");
157
+ }
158
+ const body = await readBody(req);
159
+ if (body === null)
160
+ return send(res, 413, "text/plain", "payload too large");
161
+ let job;
162
+ try {
163
+ job = JSON.parse(body);
164
+ }
165
+ catch {
166
+ return send(res, 400, "text/plain", "bad json");
167
+ }
168
+ // brainId/brainToken are optional: without them the digest comes back in
169
+ // the response for the caller to ingest itself. See BrainBuildJob.
170
+ if (!job.owner || !job.repo) {
171
+ return send(res, 400, "application/json", JSON.stringify({ error: "owner and repo are required" }));
172
+ }
173
+ // Synchronous on purpose: the caller is a person waiting on a screen, and
174
+ // the platform's own import job is what makes the SLOW half (extraction and
175
+ // placement) asynchronous. This half is a shallow clone and a graph build.
176
+ try {
177
+ // The deployment's platform URL wins over anything a caller sends, so a
178
+ // request cannot redirect a repository's history to another host.
179
+ if (config.brainBaseUrl)
180
+ job.brainBaseUrl = config.brainBaseUrl;
181
+ // Out of this process by default. A read is minutes of blocking work
182
+ // and the App has to keep answering — including the one-second access
183
+ // question the next person's onboarding is waiting on. A test that
184
+ // injects its own builder still runs in-process, which is what a test
185
+ // wants.
186
+ const built = await (seams.brainBuild ?? buildRepoInChildProcess)(job, {
187
+ creds: config,
188
+ fetch: fetchImpl,
189
+ api: config.api,
190
+ publicToken: config.publicToken,
191
+ publicOwner: config.publicOwner,
192
+ log,
193
+ now: seams.now,
194
+ });
195
+ return send(res, 202, "application/json", JSON.stringify(built));
196
+ }
197
+ catch (e) {
198
+ if (e instanceof RepoNotAccessibleError) {
199
+ return send(res, 404, "application/json", JSON.stringify({ error: e.message, ...e.gap }));
200
+ }
201
+ const msg = e instanceof Error ? e.message : String(e);
202
+ log(`brain build ${job.owner}/${job.repo} failed: ${msg}`);
203
+ return send(res, 502, "application/json", JSON.stringify({ error: msg }));
204
+ }
205
+ }
91
206
  return send(res, 404, "text/plain", "not found");
92
207
  }
93
208
  if (config.port !== undefined)
@@ -98,6 +213,18 @@ const header = (req, name) => {
98
213
  const v = req.headers[name];
99
214
  return Array.isArray(v) ? v[0] : v;
100
215
  };
216
+ /** Constant-time `Authorization: Bearer <secret>` check. */
217
+ function bearerMatches(headerValue, secret) {
218
+ const prefix = "Bearer ";
219
+ if (!headerValue || !headerValue.startsWith(prefix))
220
+ return false;
221
+ const given = Buffer.from(headerValue.slice(prefix.length));
222
+ const want = Buffer.from(secret);
223
+ // timingSafeEqual throws on a length mismatch, which is itself a leak of the
224
+ // secret's length — but one byte of length is not worth a branch that skips
225
+ // the comparison, and the lengths here are fixed by deployment config.
226
+ return given.length === want.length && timingSafeEqual(given, want);
227
+ }
101
228
  function send(res, status, type, body, extra = {}) {
102
229
  res.writeHead(status, { "content-type": type, ...extra });
103
230
  res.end(body);