co-maintainer 0.4.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/dist/main.d.ts +2 -0
  4. package/dist/main.js +9 -0
  5. package/dist/mod.d.ts +4 -0
  6. package/dist/mod.js +2 -0
  7. package/dist/package.json +54 -0
  8. package/dist/src/ai/batch.d.ts +16 -0
  9. package/dist/src/ai/batch.js +117 -0
  10. package/dist/src/ai/fake.d.ts +7 -0
  11. package/dist/src/ai/fake.js +40 -0
  12. package/dist/src/ai/hetzner.d.ts +16 -0
  13. package/dist/src/ai/hetzner.js +69 -0
  14. package/dist/src/ai/mermaid.d.ts +27 -0
  15. package/dist/src/ai/mermaid.js +457 -0
  16. package/dist/src/ai/mermaid_loop.d.ts +12 -0
  17. package/dist/src/ai/mermaid_loop.js +100 -0
  18. package/dist/src/ai/openrouter.d.ts +9 -0
  19. package/dist/src/ai/openrouter.js +29 -0
  20. package/dist/src/ai/provider.d.ts +4 -0
  21. package/dist/src/ai/provider.js +63 -0
  22. package/dist/src/cli/args.d.ts +5 -0
  23. package/dist/src/cli/args.js +252 -0
  24. package/dist/src/cli/commands/probe.d.ts +2 -0
  25. package/dist/src/cli/commands/probe.js +122 -0
  26. package/dist/src/cli/commands/review.d.ts +3 -0
  27. package/dist/src/cli/commands/review.js +84 -0
  28. package/dist/src/cli/commands/serve.d.ts +18 -0
  29. package/dist/src/cli/commands/serve.js +183 -0
  30. package/dist/src/cli/commands/set.d.ts +7 -0
  31. package/dist/src/cli/commands/set.js +171 -0
  32. package/dist/src/cli/main.d.ts +1 -0
  33. package/dist/src/cli/main.js +27 -0
  34. package/dist/src/cli/prompt.d.ts +4 -0
  35. package/dist/src/cli/prompt.js +26 -0
  36. package/dist/src/cli/review_args.d.ts +28 -0
  37. package/dist/src/cli/review_args.js +84 -0
  38. package/dist/src/cli/review_output.d.ts +2 -0
  39. package/dist/src/cli/review_output.js +10 -0
  40. package/dist/src/cli/review_result.d.ts +81 -0
  41. package/dist/src/cli/review_result.js +281 -0
  42. package/dist/src/config.d.ts +108 -0
  43. package/dist/src/config.js +138 -0
  44. package/dist/src/git/default_branch.d.ts +5 -0
  45. package/dist/src/git/default_branch.js +22 -0
  46. package/dist/src/github/app.d.ts +43 -0
  47. package/dist/src/github/app.js +120 -0
  48. package/dist/src/github/client.d.ts +17 -0
  49. package/dist/src/github/client.js +66 -0
  50. package/dist/src/github/collect.d.ts +7 -0
  51. package/dist/src/github/collect.js +368 -0
  52. package/dist/src/github/gh.d.ts +8 -0
  53. package/dist/src/github/gh.js +63 -0
  54. package/dist/src/github/jwt.d.ts +12 -0
  55. package/dist/src/github/jwt.js +84 -0
  56. package/dist/src/github/oauth.d.ts +9 -0
  57. package/dist/src/github/oauth.js +51 -0
  58. package/dist/src/github/pat.d.ts +10 -0
  59. package/dist/src/github/pat.js +60 -0
  60. package/dist/src/knowledge/facts.d.ts +3 -0
  61. package/dist/src/knowledge/facts.js +325 -0
  62. package/dist/src/knowledge/guide.d.ts +6 -0
  63. package/dist/src/knowledge/guide.js +82 -0
  64. package/dist/src/knowledge/probe.d.ts +12 -0
  65. package/dist/src/knowledge/probe.js +144 -0
  66. package/dist/src/knowledge/sections.d.ts +2 -0
  67. package/dist/src/knowledge/sections.js +13 -0
  68. package/dist/src/knowledge/skill.d.ts +10 -0
  69. package/dist/src/knowledge/skill.js +106 -0
  70. package/dist/src/knowledge/synthesis.d.ts +7 -0
  71. package/dist/src/knowledge/synthesis.js +315 -0
  72. package/dist/src/knowledge/types.d.ts +49 -0
  73. package/dist/src/knowledge/types.js +1 -0
  74. package/dist/src/knowledge/validate.d.ts +6 -0
  75. package/dist/src/knowledge/validate.js +134 -0
  76. package/dist/src/local/carry_over_store.d.ts +18 -0
  77. package/dist/src/local/carry_over_store.js +40 -0
  78. package/dist/src/local/codegraph_exclude.d.ts +3 -0
  79. package/dist/src/local/codegraph_exclude.js +26 -0
  80. package/dist/src/local/codegraph_prepare.d.ts +17 -0
  81. package/dist/src/local/codegraph_prepare.js +51 -0
  82. package/dist/src/local/git_ops.d.ts +12 -0
  83. package/dist/src/local/git_ops.js +98 -0
  84. package/dist/src/local/git_parse.d.ts +27 -0
  85. package/dist/src/local/git_parse.js +131 -0
  86. package/dist/src/local/git_revision.d.ts +15 -0
  87. package/dist/src/local/git_revision.js +141 -0
  88. package/dist/src/local/git_untracked.d.ts +21 -0
  89. package/dist/src/local/git_untracked.js +79 -0
  90. package/dist/src/local/review_local.d.ts +4 -0
  91. package/dist/src/local/review_local.js +273 -0
  92. package/dist/src/local/review_lock.d.ts +7 -0
  93. package/dist/src/local/review_lock.js +45 -0
  94. package/dist/src/pr/checkout.d.ts +25 -0
  95. package/dist/src/pr/checkout.js +136 -0
  96. package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
  97. package/dist/src/pr/codegraph_tool_args.js +34 -0
  98. package/dist/src/pr/codegraph_tools.d.ts +15 -0
  99. package/dist/src/pr/codegraph_tools.js +408 -0
  100. package/dist/src/pr/diff_summary.d.ts +6 -0
  101. package/dist/src/pr/diff_summary.js +52 -0
  102. package/dist/src/pr/findings.d.ts +14 -0
  103. package/dist/src/pr/findings.js +94 -0
  104. package/dist/src/pr/github_revision.d.ts +4 -0
  105. package/dist/src/pr/github_revision.js +39 -0
  106. package/dist/src/pr/hunks.d.ts +18 -0
  107. package/dist/src/pr/hunks.js +73 -0
  108. package/dist/src/pr/reviewer.d.ts +40 -0
  109. package/dist/src/pr/reviewer.js +582 -0
  110. package/dist/src/pr/rounds.d.ts +17 -0
  111. package/dist/src/pr/rounds.js +43 -0
  112. package/dist/src/pr/scope.d.ts +35 -0
  113. package/dist/src/pr/scope.js +84 -0
  114. package/dist/src/pr/snapshot.d.ts +11 -0
  115. package/dist/src/pr/snapshot.js +1 -0
  116. package/dist/src/pr/suggestion.d.ts +17 -0
  117. package/dist/src/pr/suggestion.js +70 -0
  118. package/dist/src/remote/client.d.ts +4 -0
  119. package/dist/src/remote/client.js +199 -0
  120. package/dist/src/remote/client_tools.d.ts +13 -0
  121. package/dist/src/remote/client_tools.js +30 -0
  122. package/dist/src/remote/fixtures.d.ts +1 -0
  123. package/dist/src/remote/fixtures.js +30 -0
  124. package/dist/src/remote/revision_from_submit.d.ts +2 -0
  125. package/dist/src/remote/revision_from_submit.js +26 -0
  126. package/dist/src/remote/schema.d.ts +14 -0
  127. package/dist/src/remote/schema.js +30 -0
  128. package/dist/src/remote/server/body.d.ts +18 -0
  129. package/dist/src/remote/server/body.js +73 -0
  130. package/dist/src/remote/server/routes.d.ts +1 -0
  131. package/dist/src/remote/server/routes.js +150 -0
  132. package/dist/src/remote/server/sessions.d.ts +16 -0
  133. package/dist/src/remote/server/sessions.js +48 -0
  134. package/dist/src/remote/server/sync.d.ts +3 -0
  135. package/dist/src/remote/server/sync.js +90 -0
  136. package/dist/src/remote/tool_bridge.d.ts +15 -0
  137. package/dist/src/remote/tool_bridge.js +84 -0
  138. package/dist/src/remote/validate.d.ts +8 -0
  139. package/dist/src/remote/validate.js +248 -0
  140. package/dist/src/review/carry_over.d.ts +63 -0
  141. package/dist/src/review/carry_over.js +372 -0
  142. package/dist/src/review/engine.d.ts +13 -0
  143. package/dist/src/review/engine.js +4 -0
  144. package/dist/src/review/guides.d.ts +12 -0
  145. package/dist/src/review/guides.js +44 -0
  146. package/dist/src/review/revision.d.ts +26 -0
  147. package/dist/src/review/revision.js +57 -0
  148. package/dist/src/server/api/activity.d.ts +1 -0
  149. package/dist/src/server/api/activity.js +12 -0
  150. package/dist/src/server/api/analytics.d.ts +1 -0
  151. package/dist/src/server/api/analytics.js +9 -0
  152. package/dist/src/server/api/installations.d.ts +4 -0
  153. package/dist/src/server/api/installations.js +24 -0
  154. package/dist/src/server/api/jobs.d.ts +1 -0
  155. package/dist/src/server/api/jobs.js +86 -0
  156. package/dist/src/server/api/json_body.d.ts +1 -0
  157. package/dist/src/server/api/json_body.js +14 -0
  158. package/dist/src/server/api/remote_tokens.d.ts +1 -0
  159. package/dist/src/server/api/remote_tokens.js +71 -0
  160. package/dist/src/server/api/repos.d.ts +4 -0
  161. package/dist/src/server/api/repos.js +136 -0
  162. package/dist/src/server/api/settings.d.ts +1 -0
  163. package/dist/src/server/api/settings.js +210 -0
  164. package/dist/src/server/app.d.ts +27 -0
  165. package/dist/src/server/app.js +147 -0
  166. package/dist/src/server/assets.d.ts +3 -0
  167. package/dist/src/server/assets.js +21 -0
  168. package/dist/src/server/auth.d.ts +44 -0
  169. package/dist/src/server/auth.js +188 -0
  170. package/dist/src/server/errors.d.ts +8 -0
  171. package/dist/src/server/errors.js +10 -0
  172. package/dist/src/server/http.d.ts +8 -0
  173. package/dist/src/server/http.js +117 -0
  174. package/dist/src/server/logo.d.ts +1 -0
  175. package/dist/src/server/logo.js +2 -0
  176. package/dist/src/server/pages/activity.d.ts +4 -0
  177. package/dist/src/server/pages/activity.js +200 -0
  178. package/dist/src/server/pages/add_repo.d.ts +9 -0
  179. package/dist/src/server/pages/add_repo.js +56 -0
  180. package/dist/src/server/pages/analytics.d.ts +2 -0
  181. package/dist/src/server/pages/analytics.js +193 -0
  182. package/dist/src/server/pages/client.d.ts +2 -0
  183. package/dist/src/server/pages/client.js +2 -0
  184. package/dist/src/server/pages/home.d.ts +16 -0
  185. package/dist/src/server/pages/home.js +87 -0
  186. package/dist/src/server/pages/knowledge.d.ts +2 -0
  187. package/dist/src/server/pages/knowledge.js +64 -0
  188. package/dist/src/server/pages/layout.d.ts +32 -0
  189. package/dist/src/server/pages/layout.js +230 -0
  190. package/dist/src/server/pages/pr.d.ts +2 -0
  191. package/dist/src/server/pages/pr.js +109 -0
  192. package/dist/src/server/pages/repo.d.ts +2 -0
  193. package/dist/src/server/pages/repo.js +92 -0
  194. package/dist/src/server/pages/repo_prs.d.ts +2 -0
  195. package/dist/src/server/pages/repo_prs.js +75 -0
  196. package/dist/src/server/pages/repo_remote.d.ts +2 -0
  197. package/dist/src/server/pages/repo_remote.js +38 -0
  198. package/dist/src/server/pages/repo_settings.d.ts +3 -0
  199. package/dist/src/server/pages/repo_settings.js +127 -0
  200. package/dist/src/server/pages/router.d.ts +17 -0
  201. package/dist/src/server/pages/router.js +312 -0
  202. package/dist/src/server/pages/settings.d.ts +2 -0
  203. package/dist/src/server/pages/settings.js +337 -0
  204. package/dist/src/server/pages/setup.d.ts +5 -0
  205. package/dist/src/server/pages/setup.js +38 -0
  206. package/dist/src/server/pages/styles.d.ts +2 -0
  207. package/dist/src/server/pages/styles.js +2 -0
  208. package/dist/src/server/webhook/index.d.ts +1 -0
  209. package/dist/src/server/webhook/index.js +64 -0
  210. package/dist/src/server/webhook/signature.d.ts +7 -0
  211. package/dist/src/server/webhook/signature.js +41 -0
  212. package/dist/src/services/credentials.d.ts +19 -0
  213. package/dist/src/services/credentials.js +146 -0
  214. package/dist/src/services/dashboard.d.ts +165 -0
  215. package/dist/src/services/dashboard.js +200 -0
  216. package/dist/src/services/drift.d.ts +13 -0
  217. package/dist/src/services/drift.js +76 -0
  218. package/dist/src/services/jobs.d.ts +34 -0
  219. package/dist/src/services/jobs.js +222 -0
  220. package/dist/src/services/remote_review.d.ts +10 -0
  221. package/dist/src/services/remote_review.js +274 -0
  222. package/dist/src/services/remote_review_abort.d.ts +3 -0
  223. package/dist/src/services/remote_review_abort.js +11 -0
  224. package/dist/src/services/remote_token_jobs.d.ts +1 -0
  225. package/dist/src/services/remote_token_jobs.js +20 -0
  226. package/dist/src/services/remote_tokens.d.ts +13 -0
  227. package/dist/src/services/remote_tokens.js +57 -0
  228. package/dist/src/services/replies.d.ts +7 -0
  229. package/dist/src/services/replies.js +248 -0
  230. package/dist/src/services/review.d.ts +35 -0
  231. package/dist/src/services/review.js +671 -0
  232. package/dist/src/services/setup.d.ts +29 -0
  233. package/dist/src/services/setup.js +364 -0
  234. package/dist/src/services/webhook.d.ts +22 -0
  235. package/dist/src/services/webhook.js +401 -0
  236. package/dist/src/store/app_db.d.ts +10 -0
  237. package/dist/src/store/app_db.js +85 -0
  238. package/dist/src/store/cache_db.d.ts +4 -0
  239. package/dist/src/store/cache_db.js +63 -0
  240. package/dist/src/store/deliveries.d.ts +12 -0
  241. package/dist/src/store/deliveries.js +23 -0
  242. package/dist/src/store/drift.d.ts +4 -0
  243. package/dist/src/store/drift.js +22 -0
  244. package/dist/src/store/findings.d.ts +31 -0
  245. package/dist/src/store/findings.js +64 -0
  246. package/dist/src/store/installations.d.ts +9 -0
  247. package/dist/src/store/installations.js +35 -0
  248. package/dist/src/store/job_logs.d.ts +5 -0
  249. package/dist/src/store/job_logs.js +17 -0
  250. package/dist/src/store/jobs.d.ts +27 -0
  251. package/dist/src/store/jobs.js +86 -0
  252. package/dist/src/store/migrations.d.ts +5 -0
  253. package/dist/src/store/migrations.js +258 -0
  254. package/dist/src/store/remote_review_inputs.d.ts +11 -0
  255. package/dist/src/store/remote_review_inputs.js +25 -0
  256. package/dist/src/store/remote_tokens.d.ts +10 -0
  257. package/dist/src/store/remote_tokens.js +45 -0
  258. package/dist/src/store/replies.d.ts +24 -0
  259. package/dist/src/store/replies.js +79 -0
  260. package/dist/src/store/repos.d.ts +11 -0
  261. package/dist/src/store/repos.js +59 -0
  262. package/dist/src/store/reviews.d.ts +86 -0
  263. package/dist/src/store/reviews.js +220 -0
  264. package/dist/src/store/rows.d.ts +179 -0
  265. package/dist/src/store/rows.js +4 -0
  266. package/dist/src/store/sessions.d.ts +8 -0
  267. package/dist/src/store/sessions.js +34 -0
  268. package/dist/src/store/skill_state.d.ts +3 -0
  269. package/dist/src/store/skill_state.js +24 -0
  270. package/dist/src/store/sqlite.d.ts +39 -0
  271. package/dist/src/store/sqlite.js +77 -0
  272. package/dist/src/store/subjects.d.ts +15 -0
  273. package/dist/src/store/subjects.js +78 -0
  274. package/dist/src/tools/codegraph.d.ts +58 -0
  275. package/dist/src/tools/codegraph.js +165 -0
  276. package/dist/src/tools/codegraph_exec.d.ts +13 -0
  277. package/dist/src/tools/codegraph_exec.js +66 -0
  278. package/dist/src/types.d.ts +89 -0
  279. package/dist/src/types.js +1 -0
  280. package/dist/src/util/atomic.d.ts +4 -0
  281. package/dist/src/util/atomic.js +78 -0
  282. package/dist/src/util/keyed_lock.d.ts +3 -0
  283. package/dist/src/util/keyed_lock.js +33 -0
  284. package/dist/src/util/log.d.ts +11 -0
  285. package/dist/src/util/log.js +42 -0
  286. package/dist/src/util/redact.d.ts +5 -0
  287. package/dist/src/util/redact.js +30 -0
  288. package/dist/src/util/runtime.d.ts +90 -0
  289. package/dist/src/util/runtime.js +206 -0
  290. package/dist/src/util/time.d.ts +9 -0
  291. package/dist/src/util/time.js +36 -0
  292. package/dist/src/version.d.ts +1 -0
  293. package/dist/src/version.js +5 -0
  294. package/package.json +54 -0
@@ -0,0 +1,146 @@
1
+ /** Probe GitHub credentials before they are saved. A token that cannot
2
+ * authenticate, or an App missing the permissions in PLAN.md Section 6,
3
+ * is rejected instead of stored and failing later on a review. */
4
+ import { githubFetch, GitHubHttpError } from "../github/client.js";
5
+ import { AppJwtClient } from "../github/app.js";
6
+ import { GhClient } from "../github/gh.js";
7
+ const APP_NEED = {
8
+ metadata: "read",
9
+ contents: "read",
10
+ issues: "write",
11
+ pull_requests: "write",
12
+ checks: "write",
13
+ };
14
+ export function missingAppPermissions(permissions) {
15
+ const got = permissions ?? {};
16
+ const missing = [];
17
+ for (const [name, need] of Object.entries(APP_NEED)) {
18
+ const value = got[name];
19
+ if (need === "write") {
20
+ if (value !== "write")
21
+ missing.push(`${name} write`);
22
+ }
23
+ else if (value !== "read" && value !== "write") {
24
+ missing.push(`${name} read`);
25
+ }
26
+ }
27
+ return missing;
28
+ }
29
+ export async function testGithubAccess(opts) {
30
+ if (opts.auth === "pat")
31
+ return await testPat(opts.githubPat ?? "");
32
+ return await testGh();
33
+ }
34
+ export async function testAppAccess(opts) {
35
+ if (!opts.appId.trim() || !opts.privateKeyPem.trim()) {
36
+ return { ok: false, message: "App ID and private key are required." };
37
+ }
38
+ try {
39
+ const app = new AppJwtClient(opts.appId, opts.privateKeyPem);
40
+ const installations = await app.listInstallations();
41
+ for (const installation of installations) {
42
+ const missing = missingAppPermissions(installation.permissions);
43
+ if (missing.length === 0)
44
+ continue;
45
+ const account = installation.account?.login ?? `installation ${installation.id}`;
46
+ return {
47
+ ok: false,
48
+ message: `The App on ${account} is missing ${missing.join(" and ")}.`,
49
+ };
50
+ }
51
+ return { ok: true, installations: installations.length };
52
+ }
53
+ catch (error) {
54
+ return { ok: false, message: humanGithubError(error) };
55
+ }
56
+ }
57
+ async function testPat(token) {
58
+ if (!token.trim()) {
59
+ return {
60
+ ok: false,
61
+ message: "A personal access token is required for PAT access.",
62
+ };
63
+ }
64
+ const user = await githubGet("user", token);
65
+ if (user.status === 401) {
66
+ return { ok: false, message: "This personal access token was rejected." };
67
+ }
68
+ if (!user.ok) {
69
+ return { ok: false, message: "GitHub rejected this token." };
70
+ }
71
+ const scopes = user.scopes;
72
+ if (scopes.length > 0) {
73
+ const canRead = scopes.includes("repo") || scopes.includes("public_repo");
74
+ if (!canRead) {
75
+ return {
76
+ ok: false,
77
+ message: "This token cannot read repositories. Grant the repo scope.",
78
+ };
79
+ }
80
+ return { ok: true, login: user.login };
81
+ }
82
+ const repos = await githubGet("user/repos?per_page=1", token);
83
+ if (repos.status === 403 || repos.status === 401) {
84
+ return {
85
+ ok: false,
86
+ message: "This token cannot list repositories. Grant repository access.",
87
+ };
88
+ }
89
+ if (!repos.ok) {
90
+ return { ok: false, message: "GitHub rejected this token." };
91
+ }
92
+ return { ok: true, login: user.login };
93
+ }
94
+ async function testGh() {
95
+ try {
96
+ const user = await new GhClient().request("user");
97
+ if (!user.login) {
98
+ return { ok: false, message: "gh CLI did not return a GitHub login." };
99
+ }
100
+ return { ok: true, login: user.login };
101
+ }
102
+ catch {
103
+ return {
104
+ ok: false,
105
+ message: "gh CLI could not reach GitHub. Sign in with gh auth login.",
106
+ };
107
+ }
108
+ }
109
+ async function githubGet(endpoint, token) {
110
+ const response = await githubFetch(`https://api.github.com/${endpoint}`, {
111
+ headers: {
112
+ Accept: "application/vnd.github+json",
113
+ Authorization: `Bearer ${token}`,
114
+ "X-GitHub-Api-Version": "2022-11-28",
115
+ },
116
+ });
117
+ const scopes = (response.headers.get("x-oauth-scopes") ?? "")
118
+ .split(",")
119
+ .map((scope) => scope.trim())
120
+ .filter(Boolean);
121
+ if (!response.ok) {
122
+ return { ok: false, status: response.status, scopes };
123
+ }
124
+ let login;
125
+ try {
126
+ const body = (await response.json());
127
+ login = body.login;
128
+ }
129
+ catch {
130
+ login = undefined;
131
+ }
132
+ return { ok: true, status: response.status, login, scopes };
133
+ }
134
+ function humanGithubError(error) {
135
+ if (error instanceof GitHubHttpError) {
136
+ if (error.status === 401) {
137
+ return "GitHub rejected this App ID or private key.";
138
+ }
139
+ return "GitHub rejected this App.";
140
+ }
141
+ const text = error instanceof Error ? error.message : String(error);
142
+ if (text.toLowerCase().includes("private key")) {
143
+ return "This private key could not be read.";
144
+ }
145
+ return "GitHub rejected this App.";
146
+ }
@@ -0,0 +1,165 @@
1
+ import { getDrift } from "../store/drift.ts";
2
+ import { reviewStats } from "../store/reviews.ts";
3
+ import type { DeliveryRow, JobRow, RepoRow, ReviewRow } from "../store/rows.ts";
4
+ export declare function requireActiveRepo(fullName: string): RepoRow;
5
+ export declare class RepoNotFound extends Error {
6
+ constructor(fullName: string);
7
+ }
8
+ export declare function statsForRange(range: string): {
9
+ days: number;
10
+ since: string;
11
+ totals: import("../store/reviews.ts").ReviewStats;
12
+ previous: import("../store/reviews.ts").ReviewStats;
13
+ change: {
14
+ pullRequests: number | undefined;
15
+ findings: number | undefined;
16
+ cost: number | undefined;
17
+ };
18
+ byRepo: (import("../store/reviews.ts").ReviewStats & {
19
+ repo: string;
20
+ })[];
21
+ byDay: {
22
+ day: string;
23
+ reviews: number;
24
+ findings: number;
25
+ cost: number;
26
+ }[];
27
+ byModel: (import("../store/reviews.ts").ReviewStats & {
28
+ model: string;
29
+ })[];
30
+ byRemoteToken: {
31
+ tokenId: string;
32
+ tokenName: string;
33
+ reviews: number;
34
+ cost: number;
35
+ findings: number;
36
+ }[];
37
+ bySeverity: import("../store/findings.ts").SeverityStats[];
38
+ };
39
+ export type RepoListItem = {
40
+ repo: RepoRow;
41
+ drift: ReturnType<typeof getDrift>;
42
+ stats: ReturnType<typeof reviewStats>;
43
+ latestJob: JobRow | undefined;
44
+ };
45
+ export declare function listReposForHome(): RepoListItem[];
46
+ export declare function repoOverview(fullName: string): {
47
+ repo: RepoRow;
48
+ drift: import("../store/rows.ts").DriftRow | undefined;
49
+ stats: import("../store/reviews.ts").ReviewStats;
50
+ recentPulls: ReviewRow[];
51
+ latestJob: JobRow | undefined;
52
+ };
53
+ export declare function repoRemoteReviews(fullName: string, limit?: number): {
54
+ items: ReviewRow[];
55
+ stats: import("../store/reviews.ts").ReviewStats;
56
+ };
57
+ export declare function repoPulls(fullName: string, page: number, per: number): {
58
+ items: import("../store/reviews.ts").PullSummary[];
59
+ page: number;
60
+ per: number;
61
+ total: number;
62
+ stats: import("../store/reviews.ts").ReviewStats;
63
+ };
64
+ export declare function prDetail(fullName: string, prNumber: number): {
65
+ prNumber: number;
66
+ reviews: {
67
+ findings: {
68
+ repeatOf: string | undefined;
69
+ id: string;
70
+ review_id: string;
71
+ severity: string;
72
+ path: string | null;
73
+ line_from: number | null;
74
+ line_to: number | null;
75
+ title: string;
76
+ body_md: string;
77
+ posted_comment_id: string | null;
78
+ thread_comment_id: string | null;
79
+ first_seen_review_id: string | null;
80
+ anchor_text: string | null;
81
+ state: string;
82
+ carried_from_finding_id: string | null;
83
+ close_reason: string | null;
84
+ }[];
85
+ id: string;
86
+ kind: string;
87
+ subject_id: string | null;
88
+ repo: string;
89
+ pr_number: number | null;
90
+ branch: string | null;
91
+ token_id: string | null;
92
+ token_name: string | null;
93
+ job_id: string;
94
+ head_sha: string | null;
95
+ base_sha: string | null;
96
+ scope: string;
97
+ model: string;
98
+ findings_count: number;
99
+ open_count: number;
100
+ closed_count: number;
101
+ tokens_in: number;
102
+ tokens_out: number;
103
+ cost: number | null;
104
+ duration_ms: number | null;
105
+ posted_review_id: string | null;
106
+ status: string;
107
+ created_at: string;
108
+ round: number;
109
+ trigger: string | null;
110
+ check_run_id: string | null;
111
+ posted_fallback: number;
112
+ guide_built_at: string | null;
113
+ codegraph: string | null;
114
+ }[];
115
+ };
116
+ export declare function repoKnowledge(fullName: string): Promise<{
117
+ repo: RepoRow;
118
+ drift: import("../store/rows.ts").DriftRow | undefined;
119
+ docs: ({
120
+ bytes: number;
121
+ text: string;
122
+ id: "guide";
123
+ file: "PR_REVIEW_GUIDE.md";
124
+ title: "Review guide";
125
+ covers: "Checks learned from past review comments";
126
+ } | {
127
+ bytes: number;
128
+ text: string;
129
+ id: "detailed";
130
+ file: "PR_REVIEW_DETAILED_GUIDE.md";
131
+ title: "Detailed guide";
132
+ covers: "Evidence behind the review checks";
133
+ } | {
134
+ bytes: number;
135
+ text: string;
136
+ id: "codebase";
137
+ file: "CODEBASE.md";
138
+ title: "Codebase notes";
139
+ covers: "Layout, style and test conventions";
140
+ } | {
141
+ bytes: number;
142
+ text: string;
143
+ id: "skill";
144
+ file: "SKILL.md";
145
+ title: "Contribution skill";
146
+ covers: "How to build, test and ship here";
147
+ })[];
148
+ }>;
149
+ export type ActivityItem = {
150
+ kind: "job";
151
+ at: string;
152
+ job: JobRow;
153
+ } | {
154
+ kind: "review";
155
+ at: string;
156
+ review: ReviewRow;
157
+ };
158
+ export declare function activityFeed(page: number, per: number): {
159
+ items: ActivityItem[];
160
+ page: number;
161
+ per: number;
162
+ total: number;
163
+ };
164
+ export declare function runningJobs(): JobRow[];
165
+ export declare function skippedDeliveries(): DeliveryRow[];
@@ -0,0 +1,200 @@
1
+ /** Read models for dashboard pages and the JSON APIs that mirror them. */
2
+ import { reposDir } from "../config.js";
3
+ import { getDrift } from "../store/drift.js";
4
+ import { refreshDrift } from "./drift.js";
5
+ import { listSkipped } from "../store/deliveries.js";
6
+ import { listJobs } from "../store/jobs.js";
7
+ import { getRepo, listActiveRepos } from "../store/repos.js";
8
+ import { listLatestReviewsForRepo, listPullSummaries, listRecentReviews, listReviewsForPr, reviewStats, reviewStatsByDay, reviewStatsByModel, reviewStatsByRemoteToken, listRemoteReviewsForRepo, reviewStatsByRepo, } from "../store/reviews.js";
9
+ import { findingStatsBySeverity, listFindingsForReview, } from "../store/findings.js";
10
+ import { daysAgoIso } from "../util/time.js";
11
+ import { isNotFound, readTextFile } from "../util/runtime.js";
12
+ export function requireActiveRepo(fullName) {
13
+ const row = getRepo(fullName);
14
+ if (!row || row.active !== 1) {
15
+ throw new RepoNotFound(fullName);
16
+ }
17
+ return row;
18
+ }
19
+ export class RepoNotFound extends Error {
20
+ constructor(fullName) {
21
+ super(`no active repository ${fullName}`);
22
+ this.name = "RepoNotFound";
23
+ }
24
+ }
25
+ /** Undefined rather than a made up percentage when the earlier window had
26
+ * nothing to grow from, so the page can leave the trend line out instead of
27
+ * printing an infinite jump. */
28
+ function percentChange(before, after) {
29
+ if (!before)
30
+ return undefined;
31
+ return Math.round(((after - before) / before) * 100);
32
+ }
33
+ /** A day with no reviews still has to occupy a slot, otherwise the bar chart
34
+ * silently closes the gap and a quiet week reads like a busy one. */
35
+ function fillDays(days, rows) {
36
+ const byDay = new Map(rows.map((row) => [row.day, row]));
37
+ const out = [];
38
+ for (let back = days - 1; back >= 0; back--) {
39
+ const day = daysAgoIso(back).slice(0, 10);
40
+ out.push(byDay.get(day) ?? { day, reviews: 0, findings: 0, cost: 0 });
41
+ }
42
+ return out;
43
+ }
44
+ export function statsForRange(range) {
45
+ const days = range === "7d" ? 7 : range === "90d" ? 90 : 30;
46
+ const since = daysAgoIso(days);
47
+ const totals = reviewStats(since);
48
+ const previous = reviewStats(daysAgoIso(days * 2), undefined, since);
49
+ return {
50
+ days,
51
+ since,
52
+ totals,
53
+ previous,
54
+ change: {
55
+ pullRequests: percentChange(previous.pullRequests, totals.pullRequests),
56
+ findings: percentChange(previous.findings, totals.findings),
57
+ cost: percentChange(previous.cost, totals.cost),
58
+ },
59
+ byRepo: reviewStatsByRepo(since),
60
+ byDay: fillDays(days, reviewStatsByDay(since)),
61
+ byModel: reviewStatsByModel(since),
62
+ byRemoteToken: reviewStatsByRemoteToken(since),
63
+ bySeverity: findingStatsBySeverity(since),
64
+ };
65
+ }
66
+ function latestSetupJob(fullName) {
67
+ return listJobs({ repo: fullName }).find((job) => job.type === "init" || job.type === "remake");
68
+ }
69
+ export function listReposForHome() {
70
+ const since = daysAgoIso(30);
71
+ return listActiveRepos().map((repo) => ({
72
+ repo,
73
+ drift: getDrift(repo.full_name),
74
+ stats: reviewStats(since, repo.full_name),
75
+ latestJob: latestSetupJob(repo.full_name),
76
+ }));
77
+ }
78
+ export function repoOverview(fullName) {
79
+ const repo = requireActiveRepo(fullName);
80
+ const since = daysAgoIso(30);
81
+ return {
82
+ repo,
83
+ drift: getDrift(fullName),
84
+ stats: reviewStats(since, fullName),
85
+ recentPulls: listLatestReviewsForRepo(fullName, 8),
86
+ latestJob: latestSetupJob(fullName),
87
+ };
88
+ }
89
+ export function repoRemoteReviews(fullName, limit = 30) {
90
+ requireActiveRepo(fullName);
91
+ const since = daysAgoIso(30);
92
+ return {
93
+ items: listRemoteReviewsForRepo(fullName, limit, 0, since),
94
+ stats: reviewStats(since, fullName),
95
+ };
96
+ }
97
+ export function repoPulls(fullName, page, per) {
98
+ requireActiveRepo(fullName);
99
+ const safePage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
100
+ const safePer = Number.isFinite(per) && per > 0 ? Math.min(per, 50) : 20;
101
+ const { items, total } = listPullSummaries(fullName, safePer, (safePage - 1) * safePer);
102
+ return {
103
+ items,
104
+ page: safePage,
105
+ per: safePer,
106
+ total,
107
+ stats: reviewStats(daysAgoIso(30), fullName),
108
+ };
109
+ }
110
+ export function prDetail(fullName, prNumber) {
111
+ requireActiveRepo(fullName);
112
+ const reviews = listReviewsForPr(fullName, prNumber);
113
+ return {
114
+ prNumber,
115
+ reviews: reviews.map((review) => ({
116
+ ...review,
117
+ findings: listFindingsForReview(review.id).map((finding) => ({
118
+ ...finding,
119
+ repeatOf: finding.first_seen_review_id ?? undefined,
120
+ })),
121
+ })),
122
+ };
123
+ }
124
+ const KNOWLEDGE_DOCS = [
125
+ {
126
+ id: "guide",
127
+ file: "PR_REVIEW_GUIDE.md",
128
+ title: "Review guide",
129
+ covers: "Checks learned from past review comments",
130
+ },
131
+ {
132
+ id: "detailed",
133
+ file: "PR_REVIEW_DETAILED_GUIDE.md",
134
+ title: "Detailed guide",
135
+ covers: "Evidence behind the review checks",
136
+ },
137
+ {
138
+ id: "codebase",
139
+ file: "CODEBASE.md",
140
+ title: "Codebase notes",
141
+ covers: "Layout, style and test conventions",
142
+ },
143
+ {
144
+ id: "skill",
145
+ file: "SKILL.md",
146
+ title: "Contribution skill",
147
+ covers: "How to build, test and ship here",
148
+ },
149
+ ];
150
+ export async function repoKnowledge(fullName) {
151
+ const repo = requireActiveRepo(fullName);
152
+ const { withKnowledgeLock } = await import("../review/guides.js");
153
+ return withKnowledgeLock(fullName, async () => {
154
+ const docs = [];
155
+ for (const doc of KNOWLEDGE_DOCS) {
156
+ const path = `${reposDir()}/${fullName}/${doc.file}`;
157
+ try {
158
+ const text = await readTextFile(path);
159
+ docs.push({
160
+ ...doc,
161
+ bytes: new TextEncoder().encode(text).byteLength,
162
+ text,
163
+ });
164
+ }
165
+ catch (error) {
166
+ if (!isNotFound(error))
167
+ throw error;
168
+ }
169
+ }
170
+ return { repo, drift: await refreshDrift(fullName), docs };
171
+ });
172
+ }
173
+ export function activityFeed(page, per) {
174
+ const safePage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
175
+ const safePer = Number.isFinite(per) && per > 0 ? Math.min(per, 50) : 20;
176
+ const items = [
177
+ ...listJobs().map((job) => ({
178
+ kind: "job",
179
+ at: job.created_at,
180
+ job,
181
+ })),
182
+ ...listRecentReviews(500).map((review) => ({
183
+ kind: "review",
184
+ at: review.created_at,
185
+ review,
186
+ })),
187
+ ].sort((a, b) => b.at.localeCompare(a.at));
188
+ const total = items.length;
189
+ const slice = items.slice((safePage - 1) * safePer, safePage * safePer);
190
+ return { items: slice, page: safePage, per: safePer, total };
191
+ }
192
+ export function runningJobs() {
193
+ return [
194
+ ...listJobs({ status: "running" }),
195
+ ...listJobs({ status: "queued" }),
196
+ ];
197
+ }
198
+ export function skippedDeliveries() {
199
+ return listSkipped();
200
+ }
@@ -0,0 +1,13 @@
1
+ import type { DriftRow } from "../store/rows.ts";
2
+ import type { GitHubClient } from "../types.ts";
3
+ /** GitHub caps a compare response at 300 files, so a very large drift
4
+ * reports 300 and the UI reads "300+". */
5
+ export declare const COMPARE_FILE_CAP = 300;
6
+ /** The fallback path pages through commits, so it stops counting rather than
7
+ * walking a year of history. Pages read this ceiling the same way. */
8
+ export declare const MAX_COMMITS_COUNTED = 500;
9
+ export declare function computeDrift(client: GitHubClient, repo: string, builtAt: string, baseSha: string | null): Promise<DriftRow>;
10
+ /** Returns the stored row, refreshing it first when it is missing or older
11
+ * than the freshness window. A GitHub failure keeps the previous row so a
12
+ * dashboard page still renders. */
13
+ export declare function refreshDrift(fullName: string, client?: GitHubClient): Promise<DriftRow | undefined>;
@@ -0,0 +1,76 @@
1
+ /** How far the repository has moved since the knowledge guide was built.
2
+ * The numbers come from GitHub, so they are recomputed on a timer rather
3
+ * than on every page render. */
4
+ import { getRepo } from "../store/repos.js";
5
+ import { getDrift, upsertDrift } from "../store/drift.js";
6
+ import { clientFor } from "./review.js";
7
+ import { nowIso } from "../util/time.js";
8
+ const FRESH_MS = 15 * 60 * 1000;
9
+ /** GitHub caps a compare response at 300 files, so a very large drift
10
+ * reports 300 and the UI reads "300+". */
11
+ export const COMPARE_FILE_CAP = 300;
12
+ /** The fallback path pages through commits, so it stops counting rather than
13
+ * walking a year of history. Pages read this ceiling the same way. */
14
+ export const MAX_COMMITS_COUNTED = 500;
15
+ function isCommitSha(value) {
16
+ return !!value && /^[0-9a-f]{40}$/.test(value);
17
+ }
18
+ function isFresh(row, nowMs) {
19
+ if (!row)
20
+ return false;
21
+ const at = Date.parse(row.as_of);
22
+ return Number.isFinite(at) && nowMs - at < FRESH_MS;
23
+ }
24
+ async function searchCount(client, query) {
25
+ const result = await client.request(`search/issues?per_page=1&q=${encodeURIComponent(query)}`);
26
+ return result.total_count ?? 0;
27
+ }
28
+ /** Commits and changed files in one call when we know the sha the guide was
29
+ * built from, otherwise a date-bounded commit list with no file count. */
30
+ async function codeDrift(client, repo, baseSha, builtAt, defaultBranch) {
31
+ if (isCommitSha(baseSha)) {
32
+ const compare = await client.request(`repos/${repo}/compare/${baseSha}...${encodeURIComponent(defaultBranch)}`);
33
+ return {
34
+ commits: compare.total_commits ?? 0,
35
+ files: Math.min(compare.files?.length ?? 0, COMPARE_FILE_CAP),
36
+ };
37
+ }
38
+ const commits = await client.pages(`repos/${repo}/commits?sha=${encodeURIComponent(defaultBranch)}&since=${encodeURIComponent(builtAt)}`, MAX_COMMITS_COUNTED);
39
+ return { commits: commits.length, files: 0 };
40
+ }
41
+ export async function computeDrift(client, repo, builtAt, baseSha) {
42
+ const meta = await client.request(`repos/${repo}`);
43
+ const defaultBranch = String(meta.default_branch ?? "main");
44
+ const [opened, touched, code] = await Promise.all([
45
+ searchCount(client, `repo:${repo} type:pr created:>${builtAt}`),
46
+ searchCount(client, `repo:${repo} type:pr updated:>${builtAt} created:<${builtAt}`),
47
+ codeDrift(client, repo, baseSha, builtAt, defaultBranch),
48
+ ]);
49
+ return {
50
+ repo,
51
+ as_of: nowIso(),
52
+ prs_since: opened,
53
+ prs_updated: touched,
54
+ commits_since: code.commits,
55
+ files_changed: code.files,
56
+ };
57
+ }
58
+ /** Returns the stored row, refreshing it first when it is missing or older
59
+ * than the freshness window. A GitHub failure keeps the previous row so a
60
+ * dashboard page still renders. */
61
+ export async function refreshDrift(fullName, client) {
62
+ const stored = getDrift(fullName);
63
+ if (isFresh(stored, Date.now()))
64
+ return stored;
65
+ const repo = getRepo(fullName);
66
+ if (!repo?.knowledge_built_at || repo.installation_id === null)
67
+ return stored;
68
+ try {
69
+ const row = await computeDrift(client ?? clientFor(repo.installation_id), fullName, repo.knowledge_built_at, repo.knowledge_base_sha);
70
+ upsertDrift(row);
71
+ return row;
72
+ }
73
+ catch {
74
+ return stored;
75
+ }
76
+ }
@@ -0,0 +1,34 @@
1
+ import type { JobLogRow, JobRow } from "../store/rows.ts";
2
+ export type LogFn = (level: string, message: string) => void;
3
+ export type JobHandler = {
4
+ run(job: JobRow, log: LogFn, signal: AbortSignal): Promise<void>;
5
+ /** Called instead of `run` for a job found in `status = 'running'` at
6
+ * boot: reconciles against whatever might already have happened rather
7
+ * than blindly repeating side effects. Omit it when `run` is idempotent. */
8
+ reconcile?(job: JobRow, log: LogFn): Promise<"done" | "canceled" | void>;
9
+ };
10
+ export declare function registerHandler(type: string, handler: JobHandler): void;
11
+ type Subscriber = (line: JobLogRow) => void;
12
+ export declare function subscribeToLogs(jobId: string, fn: Subscriber): () => void;
13
+ export declare function getLogsSince(jobId: string, fromSeq?: number): JobLogRow[];
14
+ export declare function enqueue(input: {
15
+ type: string;
16
+ repo: string;
17
+ prNumber?: number;
18
+ args?: unknown;
19
+ deliveryId?: string;
20
+ debounceMs?: number;
21
+ queueKey?: string;
22
+ }): {
23
+ id: string;
24
+ debounced: boolean;
25
+ };
26
+ export declare function claimAndRun(): Promise<boolean>;
27
+ export declare function startWorkerLoop(intervalMs?: number): void;
28
+ export declare function stopWorkerLoop(): Promise<void>;
29
+ /** Cooperative: aborts a running job's `AbortSignal` if this process is
30
+ * running it, or cancels it outright if it is merely queued. Returns false
31
+ * if neither applies (already finished, or running in another process). */
32
+ export declare function cancel(jobId: string, reason?: string): boolean;
33
+ export declare function recoverOrphans(): Promise<number>;
34
+ export {};