@nanonets/graft 0.12.1 → 0.15.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 (242) hide show
  1. package/README.md +20 -7
  2. package/dist/ai/crux.d.ts.map +1 -1
  3. package/dist/ai/crux.js +22 -1
  4. package/dist/ai/crux.js.map +1 -1
  5. package/dist/ai/llm/factory.d.ts +5 -1
  6. package/dist/ai/llm/factory.d.ts.map +1 -1
  7. package/dist/ai/llm/factory.js +8 -0
  8. package/dist/ai/llm/factory.js.map +1 -1
  9. package/dist/ai/llm/litellm.d.ts +34 -0
  10. package/dist/ai/llm/litellm.d.ts.map +1 -0
  11. package/dist/ai/llm/litellm.js +45 -0
  12. package/dist/ai/llm/litellm.js.map +1 -0
  13. package/dist/ai/llm/openai.d.ts.map +1 -1
  14. package/dist/ai/llm/openai.js +66 -7
  15. package/dist/ai/llm/openai.js.map +1 -1
  16. package/dist/ai/llm/recover-tool.d.ts +19 -0
  17. package/dist/ai/llm/recover-tool.d.ts.map +1 -0
  18. package/dist/ai/llm/recover-tool.js +109 -0
  19. package/dist/ai/llm/recover-tool.js.map +1 -0
  20. package/dist/ai/providers.d.ts.map +1 -1
  21. package/dist/ai/providers.js +2 -0
  22. package/dist/ai/providers.js.map +1 -1
  23. package/dist/ai/synthesize.d.ts.map +1 -1
  24. package/dist/ai/synthesize.js +19 -5
  25. package/dist/ai/synthesize.js.map +1 -1
  26. package/dist/app/checkout.d.ts +28 -0
  27. package/dist/app/checkout.d.ts.map +1 -0
  28. package/dist/app/checkout.js +100 -0
  29. package/dist/app/checkout.js.map +1 -0
  30. package/dist/app/events.d.ts +37 -0
  31. package/dist/app/events.d.ts.map +1 -0
  32. package/dist/app/events.js +59 -0
  33. package/dist/app/events.js.map +1 -0
  34. package/dist/app/identity.d.ts +54 -0
  35. package/dist/app/identity.d.ts.map +1 -0
  36. package/dist/app/identity.js +96 -0
  37. package/dist/app/identity.js.map +1 -0
  38. package/dist/app/main.d.ts +2 -0
  39. package/dist/app/main.d.ts.map +1 -0
  40. package/dist/app/main.js +56 -0
  41. package/dist/app/main.js.map +1 -0
  42. package/dist/app/pages.d.ts +39 -0
  43. package/dist/app/pages.d.ts.map +1 -0
  44. package/dist/app/pages.js +86 -0
  45. package/dist/app/pages.js.map +1 -0
  46. package/dist/app/queue.d.ts +40 -0
  47. package/dist/app/queue.d.ts.map +1 -0
  48. package/dist/app/queue.js +68 -0
  49. package/dist/app/queue.js.map +1 -0
  50. package/dist/app/review.d.ts +23 -0
  51. package/dist/app/review.d.ts.map +1 -0
  52. package/dist/app/review.js +129 -0
  53. package/dist/app/review.js.map +1 -0
  54. package/dist/app/server.d.ts +40 -0
  55. package/dist/app/server.d.ts.map +1 -0
  56. package/dist/app/server.js +117 -0
  57. package/dist/app/server.js.map +1 -0
  58. package/dist/ask/ask.d.ts +42 -0
  59. package/dist/ask/ask.d.ts.map +1 -1
  60. package/dist/ask/ask.js +442 -57
  61. package/dist/ask/ask.js.map +1 -1
  62. package/dist/ask/file-rank.d.ts +72 -0
  63. package/dist/ask/file-rank.d.ts.map +1 -0
  64. package/dist/ask/file-rank.js +160 -0
  65. package/dist/ask/file-rank.js.map +1 -0
  66. package/dist/ask/file-selection.d.ts +22 -0
  67. package/dist/ask/file-selection.d.ts.map +1 -0
  68. package/dist/ask/file-selection.js +66 -0
  69. package/dist/ask/file-selection.js.map +1 -0
  70. package/dist/ask/fuse.d.ts +16 -0
  71. package/dist/ask/fuse.d.ts.map +1 -1
  72. package/dist/ask/fuse.js +21 -4
  73. package/dist/ask/fuse.js.map +1 -1
  74. package/dist/ask/graphrank.d.ts +18 -0
  75. package/dist/ask/graphrank.d.ts.map +1 -1
  76. package/dist/ask/graphrank.js +65 -19
  77. package/dist/ask/graphrank.js.map +1 -1
  78. package/dist/blast/blast-cli.d.ts +5 -0
  79. package/dist/blast/blast-cli.d.ts.map +1 -1
  80. package/dist/blast/blast-cli.js +16 -1
  81. package/dist/blast/blast-cli.js.map +1 -1
  82. package/dist/blast/blast.d.ts +9 -0
  83. package/dist/blast/blast.d.ts.map +1 -1
  84. package/dist/blast/blast.js.map +1 -1
  85. package/dist/blast/evidence.d.ts +51 -0
  86. package/dist/blast/evidence.d.ts.map +1 -0
  87. package/dist/blast/evidence.js +162 -0
  88. package/dist/blast/evidence.js.map +1 -0
  89. package/dist/blast/owners.d.ts +85 -0
  90. package/dist/blast/owners.d.ts.map +1 -0
  91. package/dist/blast/owners.js +272 -0
  92. package/dist/blast/owners.js.map +1 -0
  93. package/dist/blast/render.d.ts +11 -2
  94. package/dist/blast/render.d.ts.map +1 -1
  95. package/dist/blast/render.js +125 -4
  96. package/dist/blast/render.js.map +1 -1
  97. package/dist/blast/viz.d.ts +20 -0
  98. package/dist/blast/viz.d.ts.map +1 -1
  99. package/dist/blast/viz.js +13 -157
  100. package/dist/blast/viz.js.map +1 -1
  101. package/dist/claude/format.js +1 -1
  102. package/dist/claude/format.js.map +1 -1
  103. package/dist/claude/hooks.d.ts.map +1 -1
  104. package/dist/claude/hooks.js +99 -11
  105. package/dist/claude/hooks.js.map +1 -1
  106. package/dist/claude/settings-merge.d.ts +11 -0
  107. package/dist/claude/settings-merge.d.ts.map +1 -1
  108. package/dist/claude/settings-merge.js +24 -10
  109. package/dist/claude/settings-merge.js.map +1 -1
  110. package/dist/claude/state.d.ts +14 -1
  111. package/dist/claude/state.d.ts.map +1 -1
  112. package/dist/claude/state.js +1 -1
  113. package/dist/claude/state.js.map +1 -1
  114. package/dist/claude/stats.d.ts.map +1 -1
  115. package/dist/claude/stats.js +2 -1
  116. package/dist/claude/stats.js.map +1 -1
  117. package/dist/claude/statusline.d.ts +5 -3
  118. package/dist/claude/statusline.d.ts.map +1 -1
  119. package/dist/claude/statusline.js +6 -4
  120. package/dist/claude/statusline.js.map +1 -1
  121. package/dist/claude/sync-run.d.ts.map +1 -1
  122. package/dist/claude/sync-run.js +10 -2
  123. package/dist/claude/sync-run.js.map +1 -1
  124. package/dist/claude/tally.d.ts +23 -0
  125. package/dist/claude/tally.d.ts.map +1 -0
  126. package/dist/claude/tally.js +138 -0
  127. package/dist/claude/tally.js.map +1 -0
  128. package/dist/cli.js +138 -1
  129. package/dist/cli.js.map +1 -1
  130. package/dist/context/build.d.ts.map +1 -1
  131. package/dist/context/build.js +23 -4
  132. package/dist/context/build.js.map +1 -1
  133. package/dist/context/check.d.ts.map +1 -1
  134. package/dist/context/check.js +2 -1
  135. package/dist/context/check.js.map +1 -1
  136. package/dist/context/node-file.d.ts +2 -1
  137. package/dist/context/node-file.d.ts.map +1 -1
  138. package/dist/context/node-file.js +37 -2
  139. package/dist/context/node-file.js.map +1 -1
  140. package/dist/engine.d.ts +2 -0
  141. package/dist/engine.d.ts.map +1 -1
  142. package/dist/engine.js +1 -0
  143. package/dist/engine.js.map +1 -1
  144. package/dist/graph/bindings.d.ts +25 -3
  145. package/dist/graph/bindings.d.ts.map +1 -1
  146. package/dist/graph/bindings.js +168 -2
  147. package/dist/graph/bindings.js.map +1 -1
  148. package/dist/graph/build.d.ts +4 -0
  149. package/dist/graph/build.d.ts.map +1 -1
  150. package/dist/graph/build.js +7 -4
  151. package/dist/graph/build.js.map +1 -1
  152. package/dist/graph/check.d.ts +3 -0
  153. package/dist/graph/check.d.ts.map +1 -1
  154. package/dist/graph/check.js +58 -11
  155. package/dist/graph/check.js.map +1 -1
  156. package/dist/graph/enrich.d.ts.map +1 -1
  157. package/dist/graph/enrich.js +29 -9
  158. package/dist/graph/enrich.js.map +1 -1
  159. package/dist/graph/extract.d.ts +22 -2
  160. package/dist/graph/extract.d.ts.map +1 -1
  161. package/dist/graph/extract.js +1444 -44
  162. package/dist/graph/extract.js.map +1 -1
  163. package/dist/graph/fingerprint.d.ts +6 -1
  164. package/dist/graph/fingerprint.d.ts.map +1 -1
  165. package/dist/graph/fingerprint.js +7 -3
  166. package/dist/graph/fingerprint.js.map +1 -1
  167. package/dist/graph/generic.d.ts.map +1 -1
  168. package/dist/graph/generic.js +39 -7
  169. package/dist/graph/generic.js.map +1 -1
  170. package/dist/graph/invariants.d.ts +3 -2
  171. package/dist/graph/invariants.d.ts.map +1 -1
  172. package/dist/graph/invariants.js +6 -3
  173. package/dist/graph/invariants.js.map +1 -1
  174. package/dist/graph/queries/dart.scm +42 -0
  175. package/dist/graph/queries/lua.scm +36 -0
  176. package/dist/graph/queries/nix.scm +32 -0
  177. package/dist/graph/refresh.d.ts.map +1 -1
  178. package/dist/graph/refresh.js +6 -2
  179. package/dist/graph/refresh.js.map +1 -1
  180. package/dist/graph/resolve.d.ts +1 -1
  181. package/dist/graph/resolve.d.ts.map +1 -1
  182. package/dist/graph/resolve.js +211 -26
  183. package/dist/graph/resolve.js.map +1 -1
  184. package/dist/graph/scopes.d.ts.map +1 -1
  185. package/dist/graph/scopes.js +2 -1
  186. package/dist/graph/scopes.js.map +1 -1
  187. package/dist/graph/source-files.d.ts +7 -2
  188. package/dist/graph/source-files.d.ts.map +1 -1
  189. package/dist/graph/source-files.js +19 -6
  190. package/dist/graph/source-files.js.map +1 -1
  191. package/dist/graph/traverse-cli.d.ts +13 -2
  192. package/dist/graph/traverse-cli.d.ts.map +1 -1
  193. package/dist/graph/traverse-cli.js +25 -4
  194. package/dist/graph/traverse-cli.js.map +1 -1
  195. package/dist/graph/workspace-cli.d.ts +2 -0
  196. package/dist/graph/workspace-cli.d.ts.map +1 -1
  197. package/dist/graph/workspace-cli.js +3 -0
  198. package/dist/graph/workspace-cli.js.map +1 -1
  199. package/dist/graph/workspace.d.ts.map +1 -1
  200. package/dist/graph/workspace.js +26 -2
  201. package/dist/graph/workspace.js.map +1 -1
  202. package/dist/hosts/mcp-config.d.ts +12 -0
  203. package/dist/hosts/mcp-config.d.ts.map +1 -1
  204. package/dist/hosts/mcp-config.js +54 -5
  205. package/dist/hosts/mcp-config.js.map +1 -1
  206. package/dist/hosts/registry.d.ts.map +1 -1
  207. package/dist/hosts/registry.js +20 -0
  208. package/dist/hosts/registry.js.map +1 -1
  209. package/dist/hosts/retract.d.ts +47 -0
  210. package/dist/hosts/retract.d.ts.map +1 -0
  211. package/dist/hosts/retract.js +473 -0
  212. package/dist/hosts/retract.js.map +1 -0
  213. package/dist/ingest/fs.d.ts +19 -5
  214. package/dist/ingest/fs.d.ts.map +1 -1
  215. package/dist/ingest/fs.js +35 -9
  216. package/dist/ingest/fs.js.map +1 -1
  217. package/dist/telemetry/contract.d.ts.map +1 -1
  218. package/dist/telemetry/contract.js +7 -2
  219. package/dist/telemetry/contract.js.map +1 -1
  220. package/dist/telemetry/sessions.d.ts.map +1 -1
  221. package/dist/telemetry/sessions.js +6 -0
  222. package/dist/telemetry/sessions.js.map +1 -1
  223. package/dist/util/state.d.ts +25 -2
  224. package/dist/util/state.d.ts.map +1 -1
  225. package/dist/util/state.js +24 -2
  226. package/dist/util/state.js.map +1 -1
  227. package/dist/viewer/app.js +2 -1
  228. package/dist/viewer/style.css +14 -0
  229. package/dist/viz/assemble.d.ts +17 -0
  230. package/dist/viz/assemble.d.ts.map +1 -1
  231. package/dist/viz/assemble.js.map +1 -1
  232. package/package.json +19 -4
  233. package/scripts/graph-quality.mjs +3 -3
  234. package/scripts/tally-audit.mjs +140 -0
  235. package/dist/ai/llm/claude-cli.d.ts +0 -60
  236. package/dist/ai/llm/claude-cli.d.ts.map +0 -1
  237. package/dist/ai/llm/claude-cli.js +0 -417
  238. package/dist/ai/llm/claude-cli.js.map +0 -1
  239. package/dist/util/num.d.ts +0 -3
  240. package/dist/util/num.d.ts.map +0 -1
  241. package/dist/util/num.js +0 -20
  242. package/dist/util/num.js.map +0 -1
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Where a reviewed pull request's page lives.
3
+ *
4
+ * This is the part GitHub Pages could not do. A private repository's graph must
5
+ * not be readable by anyone who guesses a URL, and the Actions route had only two
6
+ * options: publish to a public branch, or hand the reader a zip. So the page is
7
+ * held here and linked with a signed, expiring URL.
8
+ *
9
+ * The signature is the whole access-control story, deliberately: it is a
10
+ * capability, not a login. Anyone with the link can read that one page until it
11
+ * expires, which is the same property a GitHub artifact link has, and the link
12
+ * only ever appears in a comment on the pull request it describes.
13
+ */
14
+ import { createHmac, timingSafeEqual } from "node:crypto";
15
+ /** Long enough to review a PR at leisure, short enough that a leaked link dies. */
16
+ const DEFAULT_TTL_MS = 14 * 24 * 60 * 60 * 1000;
17
+ /** A page is ~50 kB; a few hundred of them is nothing, and eviction is by age. */
18
+ const DEFAULT_MAX_PAGES = 500;
19
+ export class PageStore {
20
+ pages = new Map();
21
+ secret;
22
+ ttlMs;
23
+ maxPages;
24
+ now;
25
+ constructor(opts) {
26
+ this.secret = opts.secret;
27
+ this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
28
+ this.maxPages = opts.maxPages ?? DEFAULT_MAX_PAGES;
29
+ this.now = opts.now ?? Date.now;
30
+ }
31
+ /** `owner/repo#number` → a path segment safe to put in a URL. */
32
+ static idFor(owner, repo, number) {
33
+ return `${owner}__${repo}__${number}`.replace(/[^A-Za-z0-9_.-]/g, "-");
34
+ }
35
+ /** Store (replacing any previous page for the same PR) and return `id` + `token`. */
36
+ put(owner, repo, number, html) {
37
+ const id = PageStore.idFor(owner, repo, number);
38
+ this.pages.set(id, { html, storedMs: this.now() });
39
+ this.evict();
40
+ return { id, token: this.sign(id) };
41
+ }
42
+ /**
43
+ * The page, if the token is valid and it has not expired.
44
+ *
45
+ * Expiry is checked against the STORED time rather than encoded in the token,
46
+ * so a link cannot outlive the data it points at — and re-reviewing a PR
47
+ * refreshes both together.
48
+ */
49
+ get(id, token) {
50
+ const page = this.pages.get(id);
51
+ if (!page || !token || !this.valid(id, token))
52
+ return null;
53
+ if (this.now() - page.storedMs > this.ttlMs) {
54
+ this.pages.delete(id);
55
+ return null;
56
+ }
57
+ return page.html;
58
+ }
59
+ get size() {
60
+ return this.pages.size;
61
+ }
62
+ sign(id) {
63
+ return createHmac("sha256", this.secret).update(id).digest("hex").slice(0, 32);
64
+ }
65
+ valid(id, token) {
66
+ const a = Buffer.from(this.sign(id));
67
+ const b = Buffer.from(token);
68
+ // Length first: timingSafeEqual throws on a mismatch, which would turn a
69
+ // truncated token into a 500 instead of a 404.
70
+ return a.length === b.length && timingSafeEqual(a, b);
71
+ }
72
+ /** Oldest out first, plus anything past its TTL. */
73
+ evict() {
74
+ const cutoff = this.now() - this.ttlMs;
75
+ for (const [id, page] of this.pages)
76
+ if (page.storedMs < cutoff)
77
+ this.pages.delete(id);
78
+ while (this.pages.size > this.maxPages) {
79
+ const oldest = this.pages.keys().next();
80
+ if (oldest.done)
81
+ break;
82
+ this.pages.delete(oldest.value);
83
+ }
84
+ }
85
+ }
86
+ //# sourceMappingURL=pages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.js","sourceRoot":"","sources":["../../src/app/pages.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The work queue.
3
+ *
4
+ * A webhook must be answered in seconds, and a review takes tens of them, so the
5
+ * HTTP handler's only job is to enqueue. Two properties matter and neither comes
6
+ * free from a bare array:
7
+ *
8
+ * - **Superseding.** Five pushes to one pull request in a minute is normal, and
9
+ * reviewing the first four is wasted work whose comments are immediately
10
+ * overwritten. A queued job for the same PR is replaced, not appended.
11
+ * - **A ceiling.** One repository must not be able to starve every other
12
+ * installation, so a fixed number of workers drain the queue.
13
+ */
14
+ export interface QueueOptions {
15
+ concurrency?: number;
16
+ /** Called for every failure: a job that throws must not take the process down,
17
+ * and a silent catch would hide a broken installation forever. */
18
+ onError?: (err: unknown, key: string) => void;
19
+ }
20
+ export declare class WorkQueue<T> {
21
+ private readonly run;
22
+ private readonly pending;
23
+ private readonly running;
24
+ private readonly concurrency;
25
+ private readonly onError;
26
+ private idle;
27
+ constructor(run: (item: T) => Promise<void>, opts?: QueueOptions);
28
+ /**
29
+ * Queue work under `key`, replacing anything queued under it and not yet started.
30
+ *
31
+ * A job already RUNNING is left alone: cancelling mid-clone buys nothing, and
32
+ * the newer job simply runs after it and overwrites the comment.
33
+ */
34
+ push(key: string, item: T): void;
35
+ get size(): number;
36
+ /** Resolves when nothing is queued or running — for tests and for shutdown. */
37
+ drain(): Promise<void>;
38
+ private pump;
39
+ }
40
+ //# sourceMappingURL=queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../../src/app/queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;sEACkE;IAClE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/C;AAED,qBAAa,SAAS,CAAC,CAAC;IAQpB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D,OAAO,CAAC,IAAI,CAAyB;gBAGlB,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,EAChD,IAAI,GAAE,YAAiB;IAMzB;;;;;OAKG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI;IAKhC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,+EAA+E;IACzE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B,OAAO,CAAC,IAAI;CAoBb"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The work queue.
3
+ *
4
+ * A webhook must be answered in seconds, and a review takes tens of them, so the
5
+ * HTTP handler's only job is to enqueue. Two properties matter and neither comes
6
+ * free from a bare array:
7
+ *
8
+ * - **Superseding.** Five pushes to one pull request in a minute is normal, and
9
+ * reviewing the first four is wasted work whose comments are immediately
10
+ * overwritten. A queued job for the same PR is replaced, not appended.
11
+ * - **A ceiling.** One repository must not be able to starve every other
12
+ * installation, so a fixed number of workers drain the queue.
13
+ */
14
+ export class WorkQueue {
15
+ run;
16
+ pending = new Map();
17
+ running = new Set();
18
+ concurrency;
19
+ onError;
20
+ idle = [];
21
+ constructor(run, opts = {}) {
22
+ this.run = run;
23
+ this.concurrency = Math.max(1, opts.concurrency ?? 2);
24
+ this.onError = opts.onError ?? (() => { });
25
+ }
26
+ /**
27
+ * Queue work under `key`, replacing anything queued under it and not yet started.
28
+ *
29
+ * A job already RUNNING is left alone: cancelling mid-clone buys nothing, and
30
+ * the newer job simply runs after it and overwrites the comment.
31
+ */
32
+ push(key, item) {
33
+ this.pending.set(key, item);
34
+ this.pump();
35
+ }
36
+ get size() {
37
+ return this.pending.size + this.running.size;
38
+ }
39
+ /** Resolves when nothing is queued or running — for tests and for shutdown. */
40
+ async drain() {
41
+ if (this.size === 0)
42
+ return;
43
+ await new Promise((resolve) => this.idle.push(resolve));
44
+ }
45
+ pump() {
46
+ while (this.running.size < this.concurrency) {
47
+ const next = this.pending.entries().next();
48
+ if (next.done)
49
+ break;
50
+ const [key, item] = next.value;
51
+ this.pending.delete(key);
52
+ this.running.add(key);
53
+ void this.run(item)
54
+ .catch((err) => this.onError(err, key))
55
+ .finally(() => {
56
+ this.running.delete(key);
57
+ this.pump();
58
+ if (this.size === 0) {
59
+ const waiting = this.idle;
60
+ this.idle = [];
61
+ for (const resolve of waiting)
62
+ resolve();
63
+ }
64
+ });
65
+ }
66
+ }
67
+ }
68
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.js","sourceRoot":"","sources":["../../src/app/queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,MAAM,OAAO,SAAS;IAQD;IAPF,OAAO,GAAG,IAAI,GAAG,EAAa,CAAC;IAC/B,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAC5B,WAAW,CAAS;IACpB,OAAO,CAAsC;IACtD,IAAI,GAAsB,EAAE,CAAC;IAErC,YACmB,GAA+B,EAChD,OAAqB,EAAE;QADN,QAAG,GAAH,GAAG,CAA4B;QAGhD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC,CAAC;QACtD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;OAKG;IACH,IAAI,CAAC,GAAW,EAAE,IAAO;QACvB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC5B,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC/C,CAAC;IAED,+EAA+E;IAC/E,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO;QAC5B,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAChE,CAAC;IAEO,IAAI;QACV,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC;YAC3C,IAAI,IAAI,CAAC,IAAI;gBAAE,MAAM;YACrB,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC;YAC/B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACtB,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;iBAChB,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;iBACtC,OAAO,CAAC,GAAG,EAAE;gBACZ,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACzB,IAAI,CAAC,IAAI,EAAE,CAAC;gBACZ,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;oBACpB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC;oBAC1B,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC;oBACf,KAAK,MAAM,OAAO,IAAI,OAAO;wBAAE,OAAO,EAAE,CAAC;gBAC3C,CAAC;YACH,CAAC,CAAC,CAAC;QACP,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,23 @@
1
+ import type { ReviewJob } from "./events.js";
2
+ import type { Fetch } from "./identity.js";
3
+ /** Hidden marker: how the App finds the comment it owns, on every later push. */
4
+ export declare const MARKER = "<!-- graft-blast-radius -->";
5
+ export interface ReviewDeps {
6
+ token: (installationId: number) => Promise<string>;
7
+ fetch: Fetch;
8
+ api?: string;
9
+ /** Somewhere to put the viewer page; returns the URL to link. Optional: the
10
+ * comment is useful on its own, and a page that fails to publish must not cost
11
+ * the review. */
12
+ publish?: (job: ReviewJob, html: string) => Promise<string | null>;
13
+ log?: (msg: string) => void;
14
+ }
15
+ export interface ReviewResult {
16
+ /** `null` when the diff had nothing indexed to say. */
17
+ commentUrl: string | null;
18
+ areas: number;
19
+ affected: number;
20
+ viewerUrl: string | null;
21
+ }
22
+ export declare function reviewPullRequest(job: ReviewJob, deps: ReviewDeps): Promise<ReviewResult>;
23
+ //# sourceMappingURL=review.d.ts.map
@@ -0,0 +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,CAuC/F"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * One pull request, reviewed.
3
+ *
4
+ * Checkout → graph → blast radius → comment, with the page handed to whatever
5
+ * the caller uses to publish it. Everything runs in-process against the same
6
+ * functions the CLI calls, which is what keeps the App's comment and `graft
7
+ * blast` on a laptop from drifting apart — and means the PR's own package.json
8
+ * is never installed or run.
9
+ */
10
+ import { blastRadiusIn } from "../blast/blast.js";
11
+ import { changedFiles } from "../blast/diff.js";
12
+ import { markdownReport } from "../blast/render.js";
13
+ import { blastVizGraph } from "../blast/viz.js";
14
+ import { buildGraph } from "../graph/build.js";
15
+ import { contextDirFor } from "../context/node-file.js";
16
+ import { loadGraphCached } from "../graph/load.js";
17
+ import { checkoutPullRequest, redact } from "./checkout.js";
18
+ /** Hidden marker: how the App finds the comment it owns, on every later push. */
19
+ export const MARKER = "<!-- graft-blast-radius -->";
20
+ const DEPTH = 2;
21
+ export async function reviewPullRequest(job, deps) {
22
+ const log = deps.log ?? (() => { });
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 });
25
+ try {
26
+ log(`${job.owner}/${job.repo}#${job.number}: building`);
27
+ await buildGraph(checkout.dir);
28
+ const contextDir = contextDirFor(checkout.dir);
29
+ const graph = loadGraphCached(contextDir);
30
+ if (!graph)
31
+ throw new Error("build produced no graph");
32
+ const diff = changedFiles(checkout.dir, checkout.base);
33
+ if (!diff)
34
+ throw new Error(`no diff against ${job.baseRef}`);
35
+ const report = blastRadiusIn(graph, contextDir, diff.files, diff.basis, DEPTH);
36
+ // NOTE: once #180 lands, pass `{ root: checkout.dir }` so the collapsed list
37
+ // quotes the reaching line here too. It is additive, and the App works without it.
38
+ let body = `${MARKER}\n${markdownReport(report)}`;
39
+ let viewerUrl = null;
40
+ if (deps.publish) {
41
+ const html = await exportPage(report, contextDir, checkout.dir, job);
42
+ viewerUrl = html ? await deps.publish(job, html) : null;
43
+ if (viewerUrl) {
44
+ body += `\n[**Open the interactive graph →**](${viewerUrl}) — click an area to see the code that changed, and the line that reaches it.\n`;
45
+ }
46
+ }
47
+ const commentUrl = await upsertComment(job, body, token, deps);
48
+ return { commentUrl, areas: report.areas.length, affected: report.modules.length, viewerUrl };
49
+ }
50
+ catch (err) {
51
+ // The token is in git's error text on a permissions failure, and this string
52
+ // is about to be logged.
53
+ throw new Error(redact(err instanceof Error ? err.message : String(err), token));
54
+ }
55
+ finally {
56
+ checkout.cleanup();
57
+ }
58
+ }
59
+ /** The self-contained viewer page for this radius, or null if it cannot be made. */
60
+ async function exportPage(report, contextDir, root, job) {
61
+ const { exportViz } = await import("../viz/export.js");
62
+ const { fileURLToPath } = await import("node:url");
63
+ const { mkdtempSync, readFileSync, rmSync } = await import("node:fs");
64
+ const { tmpdir } = await import("node:os");
65
+ const { join } = await import("node:path");
66
+ const out = mkdtempSync(join(tmpdir(), "graft-page-"));
67
+ try {
68
+ const res = exportViz({
69
+ contextDir,
70
+ viewerDir: fileURLToPath(new URL("../viewer/", import.meta.url)),
71
+ outDir: out,
72
+ repoName: job.repo,
73
+ subtitle: `PR #${job.number}`,
74
+ contextGraph: blastVizGraph(report, { root }),
75
+ // Context alone: the other tabs are about the repository, and this page is
76
+ // about one pull request.
77
+ tabs: ["context"],
78
+ });
79
+ return readFileSync(res.file, "utf8");
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ finally {
85
+ rmSync(out, { recursive: true, force: true });
86
+ }
87
+ }
88
+ /**
89
+ * One comment per pull request, edited in place.
90
+ *
91
+ * A new comment on every push turns a busy PR into a wall of stale diagrams, so
92
+ * the marker is searched for first. Listing is paginated because the comment the
93
+ * App owns is not necessarily on page one of a long discussion.
94
+ */
95
+ async function upsertComment(job, body, token, deps) {
96
+ const api = (deps.api ?? "https://api.github.com").replace(/\/$/, "");
97
+ const base = `${api}/repos/${job.owner}/${job.repo}/issues/${job.number}/comments`;
98
+ const headers = {
99
+ authorization: `token ${token}`,
100
+ accept: "application/vnd.github+json",
101
+ "content-type": "application/json",
102
+ "user-agent": "graft-app",
103
+ };
104
+ const existing = await findComment(base, headers, deps.fetch);
105
+ const res = await deps.fetch(existing ? `${api}/repos/${job.owner}/${job.repo}/issues/comments/${existing}` : base, {
106
+ method: existing ? "PATCH" : "POST",
107
+ headers,
108
+ body: JSON.stringify({ body }),
109
+ });
110
+ const text = await res.text();
111
+ if (!res.ok)
112
+ throw new Error(`comment failed: ${res.status} ${text.slice(0, 200)}`);
113
+ return JSON.parse(text).html_url ?? null;
114
+ }
115
+ async function findComment(base, headers, fetchImpl) {
116
+ for (let page = 1; page <= 10; page += 1) {
117
+ const res = await fetchImpl(`${base}?per_page=100&page=${page}`, { headers });
118
+ if (!res.ok)
119
+ return null;
120
+ const items = JSON.parse(await res.text());
121
+ const mine = items.find((c) => c.body?.startsWith(MARKER));
122
+ if (mine)
123
+ return mine.id;
124
+ if (items.length < 100)
125
+ return null;
126
+ }
127
+ return null;
128
+ }
129
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/app/review.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The HTTP surface: one webhook in, one page out.
3
+ *
4
+ * Deliberately `node:http` and nothing else. This process handles a private key,
5
+ * a webhook secret and other people's source code, so every dependency is one
6
+ * more thing to trust; the whole server is a hundred lines and needs no
7
+ * framework.
8
+ *
9
+ * GitHub retries a delivery it considers failed and gives up after ten seconds,
10
+ * while a review takes tens of them — so the handler validates, queues, and
11
+ * answers 202. Everything real happens on the queue.
12
+ */
13
+ import { type Server } from "node:http";
14
+ import { type ReviewJob } from "./events.js";
15
+ import { type AppCredentials, type Fetch } from "./identity.js";
16
+ import { PageStore } from "./pages.js";
17
+ import { WorkQueue } from "./queue.js";
18
+ import { reviewPullRequest } from "./review.js";
19
+ /** Seams for tests: nothing here has a default that touches the network. */
20
+ export interface AppSeams {
21
+ fetch?: Fetch;
22
+ /** Swapped out to assert what the queue was handed, without a clone. */
23
+ review?: typeof reviewPullRequest;
24
+ now?: () => number;
25
+ }
26
+ export interface AppConfig extends AppCredentials {
27
+ webhookSecret: string;
28
+ /** Public origin, for the links put in comments, e.g. https://graft.example.com */
29
+ publicUrl: string;
30
+ port?: number;
31
+ concurrency?: number;
32
+ api?: string;
33
+ log?: (msg: string) => void;
34
+ }
35
+ export declare function createApp(config: AppConfig, seams?: AppSeams): {
36
+ server: Server;
37
+ queue: WorkQueue<ReviewJob>;
38
+ pages: PageStore;
39
+ };
40
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The HTTP surface: one webhook in, one page out.
3
+ *
4
+ * Deliberately `node:http` and nothing else. This process handles a private key,
5
+ * a webhook secret and other people's source code, so every dependency is one
6
+ * more thing to trust; the whole server is a hundred lines and needs no
7
+ * framework.
8
+ *
9
+ * GitHub retries a delivery it considers failed and gives up after ten seconds,
10
+ * while a review takes tens of them — so the handler validates, queues, and
11
+ * answers 202. Everything real happens on the queue.
12
+ */
13
+ import { createServer } from "node:http";
14
+ import { jobKey, reviewJobFor } from "./events.js";
15
+ import { InstallationTokens, verifySignature } from "./identity.js";
16
+ import { PageStore } from "./pages.js";
17
+ import { WorkQueue } from "./queue.js";
18
+ import { reviewPullRequest } from "./review.js";
19
+ /** A webhook body larger than this is not a pull_request event we can use. */
20
+ const MAX_BODY_BYTES = 2 * 1024 * 1024;
21
+ export function createApp(config, seams = {}) {
22
+ const log = config.log ?? ((msg) => console.log(msg));
23
+ const fetchImpl = seams.fetch ?? globalThis.fetch;
24
+ const review = seams.review ?? reviewPullRequest;
25
+ const tokens = new InstallationTokens(config, fetchImpl, seams.now ?? Date.now, config.api);
26
+ const pages = new PageStore({ secret: config.webhookSecret });
27
+ const origin = config.publicUrl.replace(/\/$/, "");
28
+ const queue = new WorkQueue(async (job) => {
29
+ const started = Date.now();
30
+ const res = await review(job, {
31
+ token: (id) => tokens.get(id),
32
+ fetch: fetchImpl,
33
+ api: config.api,
34
+ publish: async (j, html) => {
35
+ const { id, token } = pages.put(j.owner, j.repo, j.number, html);
36
+ return `${origin}/p/${id}?t=${token}`;
37
+ },
38
+ log,
39
+ });
40
+ log(`${jobKey(job)}: ${res.areas} changed → ${res.affected} affected in ${Date.now() - started}ms`);
41
+ }, {
42
+ concurrency: config.concurrency,
43
+ onError: (err, key) => {
44
+ // A token GitHub rejected is worth forgetting: the next delivery for that
45
+ // installation should mint a fresh one rather than fail the same way.
46
+ log(`${key}: FAILED ${err instanceof Error ? err.message : String(err)}`);
47
+ },
48
+ });
49
+ const server = createServer((req, res) => {
50
+ void handle(req, res).catch((err) => {
51
+ log(`request failed: ${err instanceof Error ? err.message : String(err)}`);
52
+ if (!res.headersSent)
53
+ send(res, 500, "text/plain", "internal error");
54
+ });
55
+ });
56
+ async function handle(req, res) {
57
+ const url = new URL(req.url ?? "/", origin);
58
+ if (req.method === "GET" && url.pathname === "/healthz") {
59
+ return send(res, 200, "application/json", JSON.stringify({ ok: true, queued: queue.size, pages: pages.size }));
60
+ }
61
+ if (req.method === "GET" && url.pathname.startsWith("/p/")) {
62
+ const html = pages.get(url.pathname.slice(3), url.searchParams.get("t") ?? undefined);
63
+ // A bad token and a missing page are the same answer on purpose: probing
64
+ // for which pull requests exist is not something this should help with.
65
+ if (!html)
66
+ return send(res, 404, "text/plain", "not found");
67
+ return send(res, 200, "text/html; charset=utf-8", html, { "cache-control": "private, max-age=600" });
68
+ }
69
+ if (req.method === "POST" && url.pathname === "/webhook") {
70
+ const body = await readBody(req);
71
+ if (body === null)
72
+ return send(res, 413, "text/plain", "payload too large");
73
+ if (!verifySignature(config.webhookSecret, body, header(req, "x-hub-signature-256"))) {
74
+ return send(res, 401, "text/plain", "bad signature");
75
+ }
76
+ const event = header(req, "x-github-event") ?? "";
77
+ let payload;
78
+ try {
79
+ payload = JSON.parse(body);
80
+ }
81
+ catch {
82
+ return send(res, 400, "text/plain", "bad json");
83
+ }
84
+ const decided = reviewJobFor(event, payload);
85
+ if ("skip" in decided)
86
+ return send(res, 204, "text/plain", "");
87
+ queue.push(jobKey(decided.job), decided.job);
88
+ log(`${jobKey(decided.job)}: queued${decided.job.fromFork ? " (fork)" : ""}`);
89
+ return send(res, 202, "application/json", JSON.stringify({ queued: true }));
90
+ }
91
+ return send(res, 404, "text/plain", "not found");
92
+ }
93
+ if (config.port !== undefined)
94
+ server.listen(config.port, () => log(`graft app listening on :${config.port}`));
95
+ return { server, queue, pages };
96
+ }
97
+ const header = (req, name) => {
98
+ const v = req.headers[name];
99
+ return Array.isArray(v) ? v[0] : v;
100
+ };
101
+ function send(res, status, type, body, extra = {}) {
102
+ res.writeHead(status, { "content-type": type, ...extra });
103
+ res.end(body);
104
+ }
105
+ /** The raw body — needed verbatim, because the signature covers these bytes. */
106
+ async function readBody(req) {
107
+ const chunks = [];
108
+ let size = 0;
109
+ for await (const chunk of req) {
110
+ size += chunk.length;
111
+ if (size > MAX_BODY_BYTES)
112
+ return null;
113
+ chunks.push(chunk);
114
+ }
115
+ return Buffer.concat(chunks).toString("utf8");
116
+ }
117
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/app/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,YAAY,EAA0D,MAAM,WAAW,CAAC;AACjG,OAAO,EAAE,MAAM,EAAE,YAAY,EAAkB,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAmC,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;AAEhD,8EAA8E;AAC9E,MAAM,cAAc,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAoBvC,MAAM,UAAU,SAAS,CACvB,MAAiB,EACjB,QAAkB,EAAE;IAEpB,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9D,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,IAAK,UAAU,CAAC,KAA0B,CAAC;IACxE,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,iBAAiB,CAAC;IACjD,MAAM,MAAM,GAAG,IAAI,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5F,MAAM,KAAK,GAAG,IAAI,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAEnD,MAAM,KAAK,GAAG,IAAI,SAAS,CACzB,KAAK,EAAE,GAAG,EAAE,EAAE;QACZ,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,GAAG,EAAE;YAC5B,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7B,KAAK,EAAE,SAAS;YAChB,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;gBACzB,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;gBACjE,OAAO,GAAG,MAAM,MAAM,EAAE,MAAM,KAAK,EAAE,CAAC;YACxC,CAAC;YACD,GAAG;SACJ,CAAC,CAAC;QACH,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,KAAK,cAAc,GAAG,CAAC,QAAQ,gBAAgB,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,IAAI,CAAC,CAAC;IACtG,CAAC,EACD;QACE,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACpB,0EAA0E;YAC1E,sEAAsE;YACtE,GAAG,CAAC,GAAG,GAAG,YAAY,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5E,CAAC;KACF,CACF,CAAC;IAEF,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACvC,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YAClC,GAAG,CAAC,mBAAmB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC3E,IAAI,CAAC,GAAG,CAAC,WAAW;gBAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,gBAAgB,CAAC,CAAC;QACvE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,KAAK,UAAU,MAAM,CAAC,GAAoB,EAAE,GAAmB;QAC7D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,MAAM,CAAC,CAAC;QAE5C,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;YACxD,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,kBAAkB,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACjH,CAAC;QAED,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3D,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,CAAC;YACtF,yEAAyE;YACzE,wEAAwE;YACxE,IAAI,CAAC,IAAI;gBAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC;YAC5D,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,0BAA0B,EAAE,IAAI,EAAE,EAAE,eAAe,EAAE,sBAAsB,EAAE,CAAC,CAAC;QACvG,CAAC;QAED,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;YACzD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,IAAI,KAAK,IAAI;gBAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,mBAAmB,CAAC,CAAC;YAC5E,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,aAAa,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,EAAE,CAAC;gBACrF,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,eAAe,CAAC,CAAC;YACvD,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,EAAE,gBAAgB,CAAC,IAAI,EAAE,CAAC;YAClD,IAAI,OAAgB,CAAC;YACrB,IAAI,CAAC;gBACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC7B,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;YAClD,CAAC;YAED,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YAC7C,IAAI,MAAM,IAAI,OAAO;gBAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,EAAE,CAAC,CAAC;YAC/D,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;YAC7C,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC9E,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,kBAAkB,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC9E,CAAC;QAED,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC;IACnD,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,2BAA2B,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAC/G,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAClC,CAAC;AAED,MAAM,MAAM,GAAG,CAAC,GAAoB,EAAE,IAAY,EAAsB,EAAE;IACxE,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC,CAAC;AAEF,SAAS,IAAI,CAAC,GAAmB,EAAE,MAAc,EAAE,IAAY,EAAE,IAAY,EAAE,QAAgC,EAAE;IAC/G,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;IAC1D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,gFAAgF;AAChF,KAAK,UAAU,QAAQ,CAAC,GAAoB;IAC1C,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QAC9B,IAAI,IAAK,KAAgB,CAAC,MAAM,CAAC;QACjC,IAAI,IAAI,GAAG,cAAc;YAAE,OAAO,IAAI,CAAC;QACvC,MAAM,CAAC,IAAI,CAAC,KAAe,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC"}
package/dist/ask/ask.d.ts CHANGED
@@ -18,11 +18,39 @@ export interface AskHit {
18
18
  * the `[scope/] ` label in formatAsk. Absent on single-scope repos. */
19
19
  scope?: string;
20
20
  }
21
+ /** Internal file-aware ranking group. The first hit is the one document that
22
+ * participates in a higher-level ranker; the remaining hits are projected only
23
+ * after every ranked group has emitted its leader. */
24
+ export interface AskRankingGroup {
25
+ key: string;
26
+ hits: AskHit[];
27
+ /** Exact baseline-scored members of this group, in baseline order. Populated
28
+ * only in opt-in metadata so a workspace top lock never reuses a pooled
29
+ * leader score for a secondary span. */
30
+ baselineHits?: AskHit[];
31
+ coverage: number;
32
+ coverageStrong: number;
33
+ }
34
+ /** Internal metadata used by workspace federation to fuse file documents
35
+ * before projecting symbol spans. It is opt-in and never appears in normal
36
+ * CLI/API results. */
37
+ export interface AskRankingMetadata {
38
+ groups: AskRankingGroup[];
39
+ baseline: Array<{
40
+ group: string;
41
+ hit: AskHit;
42
+ }>;
43
+ baselineCoverage?: number;
44
+ baselineCoverageStrong?: number;
45
+ }
21
46
  export interface AskResult {
22
47
  query: string;
23
48
  mode: "structural" | "lexical" | "empty";
24
49
  /** For structural mode: the symbol whose neighbours we walked. */
25
50
  subject?: string;
51
+ /** Authoritative result order. Lexical results preserve each hit's raw
52
+ * relevance score, but select distinct-file leaders before sibling spans, so
53
+ * callers must not re-sort this list by `score`. */
26
54
  hits: AskHit[];
27
55
  note?: string;
28
56
  /** Token-saving estimate, set only in `--source` (retriever) mode: the whole
@@ -57,6 +85,8 @@ export interface AskResult {
57
85
  bestId: string;
58
86
  }[];
59
87
  };
88
+ /** @internal Opt-in latent file groups plus the exact pre-file-ranking list. */
89
+ ranking?: AskRankingMetadata;
60
90
  }
61
91
  /** Score a document's token counts against the query counts (name field
62
92
  * pre-weighted by caller). Each shared term is weighted by its inverse document
@@ -113,6 +143,18 @@ export interface AskOptions {
113
143
  * existing multi-scope machinery degrades to its single-scope passthrough
114
144
  * with no scope labels. A prefix matching nothing indexed throws. */
115
145
  in?: string;
146
+ /** @internal Defer file-first projection to a downstream authoritative
147
+ * ranking stage (currently workspace federation). Direct callers should
148
+ * leave this unset. */
149
+ fileFirst?: boolean;
150
+ /** @internal Bounded file scoring control. */
151
+ fileComplement?: boolean;
152
+ /** @internal Exact baseline top-hit lock. Requires bounded file
153
+ * scoring and composes it with the file-first frontier. */
154
+ fileTopLock?: boolean;
155
+ /** @internal Return latent file queues and the exact baseline list so a
156
+ * workspace parent can fuse files before projecting spans. */
157
+ includeRankingMetadata?: boolean;
116
158
  }
117
159
  /** Answer a query from the graft/ graph at `dir`. Deterministic, $0. */
118
160
  export declare function ask(dir: string, query: string, opts?: AskOptions): AskResult;
@@ -1 +1 @@
1
- {"version":3,"file":"ask.d.ts","sourceRoot":"","sources":["../../src/ask/ask.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ask.d.ts","sourceRoot":"","sources":["../../src/ask/ask.ts"],"names":[],"mappings":"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"}