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,144 @@
1
+ function year(value) {
2
+ return new Date(String(value)).getFullYear();
3
+ }
4
+ function percentile(values, percentage) {
5
+ if (!values.length)
6
+ return 0;
7
+ const sorted = [...values].sort((a, b) => a - b);
8
+ return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * percentage) - 1)];
9
+ }
10
+ function isMerge(commit) {
11
+ const message = String(commit.commit?.message ?? "");
12
+ return (message.startsWith("Merge ") ||
13
+ (Array.isArray(commit.parents) && commit.parents.length > 1));
14
+ }
15
+ function isConventional(commit) {
16
+ return /^(feat|fix|docs|refactor|test|build|ci|chore|perf|style)(\(.+\))?!?:\s/i.test(String(commit.commit?.message ?? ""));
17
+ }
18
+ function commitStats(commits) {
19
+ if (!commits.length)
20
+ return { mergeRatio: 0, conventionalRatio: 0 };
21
+ return {
22
+ mergeRatio: commits.filter(isMerge).length / commits.length,
23
+ conventionalRatio: commits.filter(isConventional).length / commits.length,
24
+ };
25
+ }
26
+ function recommendCommitWindow(commits) {
27
+ if (commits.length <= 300)
28
+ return undefined;
29
+ const useful = commits.filter((commit) => !isMerge(commit));
30
+ if (useful.length < 100)
31
+ return undefined;
32
+ const recent = commitStats(commits.slice(0, 100));
33
+ let stableThrough = 100;
34
+ for (let start = 100; start < commits.length; start += 100) {
35
+ const window = commits.slice(start, start + 100);
36
+ const stats = commitStats(window);
37
+ const stable = Math.abs(stats.mergeRatio - recent.mergeRatio) <= 0.15 &&
38
+ Math.abs(stats.conventionalRatio - recent.conventionalRatio) <= 0.15;
39
+ if (!stable)
40
+ break;
41
+ stableThrough = start + window.length;
42
+ }
43
+ return stableThrough < commits.length ? stableThrough : undefined;
44
+ }
45
+ function recommendPrWindow(pulls, latestReleaseAt) {
46
+ if (!pulls.length)
47
+ return undefined;
48
+ const years = new Map();
49
+ for (const pull of pulls) {
50
+ const itemYear = year(pull.updated_at);
51
+ years.set(itemYear, (years.get(itemYear) ?? 0) + 1);
52
+ }
53
+ const oldest = Math.min(...years.keys());
54
+ const current = new Date().getFullYear();
55
+ const span = current - oldest + 1;
56
+ if (pulls.length <= 150 || span <= 3)
57
+ return undefined;
58
+ const recentTwoYears = pulls.filter((pull) => year(pull.updated_at) >= current - 1).length;
59
+ const releaseAgeMonths = latestReleaseAt
60
+ ? (Date.now() - new Date(latestReleaseAt).getTime()) /
61
+ (30.44 * 24 * 60 * 60 * 1_000)
62
+ : Infinity;
63
+ if (recentTwoYears >= 100 &&
64
+ (releaseAgeMonths <= 18 || recentTwoYears / pulls.length >= 0.35)) {
65
+ return 24;
66
+ }
67
+ const target = pulls.length * 0.85;
68
+ let covered = 0;
69
+ let windowYears = 0;
70
+ while (windowYears < span && (covered < target || windowYears < 2)) {
71
+ windowYears++;
72
+ covered += years.get(current - windowYears + 1) ?? 0;
73
+ }
74
+ return windowYears < span ? windowYears * 12 : undefined;
75
+ }
76
+ export function analyzeProbe(meta, pulls, detailSamples, commits) {
77
+ const years = new Map();
78
+ for (const pull of pulls) {
79
+ const itemYear = year(pull.updated_at);
80
+ years.set(itemYear, (years.get(itemYear) ?? 0) + 1);
81
+ }
82
+ const diffs = detailSamples
83
+ .map((detail) => Number(detail.additions ?? 0) + Number(detail.deletions ?? 0))
84
+ .filter((value) => value >= 0);
85
+ const medianDiff = percentile(diffs, 0.5);
86
+ const p90Diff = percentile(diffs, 0.9);
87
+ const maxDiff = Math.max(...diffs, 0);
88
+ const latestReleaseAt = String(meta.latest_release_at ?? "");
89
+ const maxPrMonths = recommendPrWindow(pulls, latestReleaseAt);
90
+ const maxCommits = recommendCommitWindow(commits);
91
+ const commitSignal = commitStats(commits);
92
+ const usefulCommits = commits.filter((commit) => !isMerge(commit)).length;
93
+ const includeCommitHistory = usefulCommits >= 20 && commitSignal.mergeRatio < 0.75;
94
+ const includeChanges = pulls.length > 0 && diffs.length > 0;
95
+ const maxChangeLines = includeChanges && diffs.length >= 10 && p90Diff < maxDiff
96
+ ? Math.max(100, Math.ceil(p90Diff / 100) * 100)
97
+ : undefined;
98
+ const reasons = [];
99
+ if (maxPrMonths) {
100
+ const releaseAgeMonths = latestReleaseAt
101
+ ? (Date.now() - new Date(latestReleaseAt).getTime()) /
102
+ (30.44 * 24 * 60 * 60 * 1_000)
103
+ : Infinity;
104
+ reasons.push(releaseAgeMonths <= 18
105
+ ? `A release from ${latestReleaseAt.slice(0, 10)} is recent; use the last ${maxPrMonths} months to prioritize the current contribution and release process.`
106
+ : `The last ${maxPrMonths} months cover at least 85% of pull requests; older history is a minority.`);
107
+ }
108
+ else if (pulls.length) {
109
+ reasons.push("Pull request volume or history span is small enough to keep all PR months.");
110
+ }
111
+ if (includeCommitHistory) {
112
+ reasons.push(`${usefulCommits} non-merge commits contain enough signal to analyze commit conventions.`);
113
+ }
114
+ else if (commits.length) {
115
+ reasons.push("Commit history is mostly merge/noise commits, so it is not recommended as a source.");
116
+ }
117
+ if (maxChangeLines) {
118
+ reasons.push(`The sampled diff p90 is ${p90Diff} lines; the cap keeps the largest outliers out while retaining about 90% of sampled diffs.`);
119
+ }
120
+ return {
121
+ includePullRequests: pulls.length > 0,
122
+ includePullRequestChanges: includeChanges,
123
+ includeCommitHistory,
124
+ maxPrMonths,
125
+ maxCommits: includeCommitHistory ? maxCommits : undefined,
126
+ maxPullRequestChangeLines: maxChangeLines,
127
+ reasons,
128
+ report: {
129
+ pullRequests: pulls.length,
130
+ pullRequestYears: Object.fromEntries([...years.entries()].sort()),
131
+ sampledPullRequests: detailSamples.length,
132
+ diffLines: {
133
+ median: medianDiff,
134
+ p90: p90Diff,
135
+ maximum: maxDiff,
136
+ },
137
+ commits: commits.length,
138
+ usefulCommits,
139
+ commitSignal,
140
+ defaultBranch: String(meta.default_branch ?? "main"),
141
+ latestReleaseAt: latestReleaseAt || undefined,
142
+ },
143
+ };
144
+ }
@@ -0,0 +1,2 @@
1
+ export declare const sectionTitles: Record<string, string>;
2
+ export declare const sectionKeys: string[];
@@ -0,0 +1,13 @@
1
+ export const sectionTitles = {
2
+ identity: "Repository",
3
+ devloop: "Development and debugging",
4
+ ship: "Shipping",
5
+ layout: "Code layout",
6
+ style: "Code style",
7
+ tests: "Tests",
8
+ "title-body": "Pull request title and body",
9
+ labels: "Labels",
10
+ "review-bar": "Review bar",
11
+ process: "Pull request process",
12
+ };
13
+ export const sectionKeys = Object.keys(sectionTitles);
@@ -0,0 +1,10 @@
1
+ import type { Fact } from "./types.ts";
2
+ /** Slices an assembled skill's `## Heading` sections back out by section key,
3
+ * from `<!-- section:key -->` markers or (fallback) canonical heading text. */
4
+ export declare function extractSections(markdown: string): Record<string, string>;
5
+ export declare function factSectionHashes(facts: Fact[]): Promise<Record<string, string>>;
6
+ export declare function assembleSkill(repo: string, facts: Fact[], previousMarkdown: string | undefined, previousHashes: Record<string, string>, overrides?: Record<string, string>): Promise<{
7
+ markdown: string;
8
+ hashes: Record<string, string>;
9
+ changed: string[];
10
+ }>;
@@ -0,0 +1,106 @@
1
+ import { sectionKeys, sectionTitles } from "./sections.js";
2
+ function sectionFacts(facts, key) {
3
+ return facts
4
+ .filter((item) => item.sectionKey === key)
5
+ .sort((a, b) => b.weight - a.weight);
6
+ }
7
+ function render(key, facts) {
8
+ const items = sectionFacts(facts, key).slice(0, 6);
9
+ if (!items.length)
10
+ return "";
11
+ return `## ${sectionTitles[key] ?? key}\n\n${items.map((item) => `- ${item.claim}`).join("\n")}`;
12
+ }
13
+ function cleanSection(value) {
14
+ return value.replace(/^\s*```[a-zA-Z0-9_-]*\s*$/gm, "").trim();
15
+ }
16
+ function hasBullets(value) {
17
+ return /^-\s+/m.test(value);
18
+ }
19
+ /** Slices an assembled skill's `## Heading` sections back out by section key,
20
+ * from `<!-- section:key -->` markers or (fallback) canonical heading text. */
21
+ export function extractSections(markdown) {
22
+ const result = {};
23
+ const markerPattern = /<!-- section:([a-z-]+) -->\n([\s\S]*?)\n<!-- \/section:\1 -->/g;
24
+ for (const match of markdown.matchAll(markerPattern)) {
25
+ result[match[1]] = match[2];
26
+ }
27
+ const keysByTitle = Object.fromEntries(Object.entries(sectionTitles).map(([key, title]) => [title, key]));
28
+ const headings = [...markdown.matchAll(/^##\s+(.+)$/gm)];
29
+ for (let index = 0; index < headings.length; index++) {
30
+ const key = keysByTitle[headings[index][1].trim()];
31
+ if (!key || result[key])
32
+ continue;
33
+ const start = headings[index].index ?? 0;
34
+ const end = headings[index + 1]?.index ?? markdown.length;
35
+ result[key] = markdown.slice(start, end).trim();
36
+ }
37
+ return result;
38
+ }
39
+ async function hash(value) {
40
+ const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
41
+ return [...new Uint8Array(bytes)]
42
+ .map((byte) => byte.toString(16).padStart(2, "0"))
43
+ .join("");
44
+ }
45
+ export async function factSectionHashes(facts) {
46
+ const hashes = {};
47
+ for (const key of new Set(facts.map((item) => item.sectionKey))) {
48
+ const relevant = sectionFacts(facts, key);
49
+ hashes[key] = await hash(JSON.stringify(relevant.map((item) => [
50
+ item.id,
51
+ item.claim,
52
+ item.weight,
53
+ item.scope,
54
+ item.confidence,
55
+ item.status,
56
+ ])));
57
+ }
58
+ return hashes;
59
+ }
60
+ export async function assembleSkill(repo, facts, previousMarkdown, previousHashes, overrides = {}) {
61
+ const previous = previousMarkdown ? extractSections(previousMarkdown) : {};
62
+ const sections = {};
63
+ const hashes = {};
64
+ const changed = [];
65
+ const keys = sectionKeys.filter((key) => facts.some((item) => item.sectionKey === key));
66
+ for (const key of keys) {
67
+ const content = render(key, facts);
68
+ if (!content)
69
+ continue;
70
+ const relevant = sectionFacts(facts, key);
71
+ const sectionHash = await hash(JSON.stringify(relevant.map((item) => [
72
+ item.id,
73
+ item.claim,
74
+ item.weight,
75
+ item.scope,
76
+ item.confidence,
77
+ item.status,
78
+ ])));
79
+ hashes[key] = sectionHash;
80
+ const canonicalHeading = `## ${sectionTitles[key] ?? key}`;
81
+ const hasOverride = Object.hasOwn(overrides, key);
82
+ if (!hasOverride &&
83
+ previousMarkdown &&
84
+ previousHashes[key] === sectionHash &&
85
+ previous[key]?.startsWith(canonicalHeading)) {
86
+ sections[key] = cleanSection(previous[key]);
87
+ continue;
88
+ }
89
+ const section = hasOverride ? overrides[key] : content;
90
+ if (!hasBullets(section))
91
+ continue;
92
+ sections[key] = section;
93
+ changed.push(key);
94
+ }
95
+ const header = `---
96
+ name: ${repo.replace("/", "-")}-maintainer
97
+ description: Repository-specific guidance for contributing to ${repo}. Use when writing code, opening or reviewing pull requests, debugging, or releasing this repository.
98
+ ---
99
+
100
+ # ${repo}
101
+
102
+ Generated from repository metadata and selected project history. Prefer current repository files and workflows when this guidance conflicts with a stale example.
103
+ `;
104
+ const body = Object.values(sections).join("\n\n");
105
+ return { markdown: header + body + "\n", hashes, changed };
106
+ }
@@ -0,0 +1,7 @@
1
+ import type { AiProvider, AiResponse, Options } from "../types.ts";
2
+ import type { Fact, Source } from "./types.ts";
3
+ type UsageSink = (job: string, response: AiResponse) => Promise<void>;
4
+ export declare function extractAiFacts(provider: AiProvider, repo: string, source: Source, options: Options, usage?: UsageSink): Promise<Fact[]>;
5
+ export declare function enrichFacts(provider: AiProvider, repo: string, base: Fact[], source: Source, options: Options, usage?: UsageSink): Promise<Fact[]>;
6
+ export declare function synthesizeSections(provider: AiProvider, repo: string, facts: Fact[], _previousMarkdown: string | undefined, ai: Options["ai"], model: string, concurrency: number, usage?: UsageSink, onlySections?: Set<string>): Promise<Record<string, string>>;
7
+ export {};
@@ -0,0 +1,315 @@
1
+ import { AiBatch } from "../ai/batch.js";
2
+ import { sectionTitles } from "./sections.js";
3
+ const allowedSections = new Set([
4
+ "identity",
5
+ "devloop",
6
+ "ship",
7
+ "layout",
8
+ "style",
9
+ "tests",
10
+ "title-body",
11
+ "labels",
12
+ "review-bar",
13
+ "process",
14
+ ]);
15
+ function slug(value) {
16
+ return value.toLowerCase().replaceAll(/\W+/g, "-").replace(/^-|-$/g, "");
17
+ }
18
+ function promptText(value, limit = 6_000) {
19
+ return value.length > limit ? `${value.slice(0, limit)}\n[truncated]` : value;
20
+ }
21
+ function parseJson(text) {
22
+ const cleaned = text
23
+ .trim()
24
+ .replace(/^```(?:json)?\s*/i, "")
25
+ .replace(/\s*```$/, "");
26
+ try {
27
+ return JSON.parse(cleaned);
28
+ }
29
+ catch {
30
+ const start = cleaned.indexOf("[");
31
+ const end = cleaned.lastIndexOf("]");
32
+ if (start < 0)
33
+ throw new Error("AI fact output is not JSON");
34
+ if (end > start)
35
+ return JSON.parse(cleaned.slice(start, end + 1));
36
+ const lastCompleteObject = cleaned.lastIndexOf("}");
37
+ if (lastCompleteObject <= start) {
38
+ throw new Error("AI fact output is truncated");
39
+ }
40
+ return JSON.parse(`${cleaned.slice(start, lastCompleteObject + 1)}]`);
41
+ }
42
+ }
43
+ function factsFromResponse(text, evidence, defaultScope) {
44
+ const parsed = parseJson(text);
45
+ if (!Array.isArray(parsed)) {
46
+ throw new Error("AI fact output must be an array");
47
+ }
48
+ return parsed.flatMap((item) => {
49
+ if (!item || typeof item !== "object")
50
+ return [];
51
+ const value = item;
52
+ const claim = String(value.claim ?? "").trim();
53
+ const sectionKey = String(value.sectionKey ?? "").trim();
54
+ if (!claim || !allowedSections.has(sectionKey))
55
+ return [];
56
+ const scope = value.scope === "current" ||
57
+ value.scope === "repeated-history" ||
58
+ value.scope === "historical-example"
59
+ ? value.scope
60
+ : defaultScope;
61
+ const confidence = value.confidence === "high" ||
62
+ value.confidence === "medium" ||
63
+ value.confidence === "low"
64
+ ? value.confidence
65
+ : "medium";
66
+ return [
67
+ {
68
+ id: `${sectionKey}:${slug(claim)}`,
69
+ sectionKey,
70
+ claim,
71
+ evidence: [evidence],
72
+ weight: 1,
73
+ scope,
74
+ confidence,
75
+ status: "active",
76
+ },
77
+ ];
78
+ });
79
+ }
80
+ function hasUnsupportedIdentifier(claim, source) {
81
+ const evidence = [
82
+ ...source.tree,
83
+ ...Object.keys(source.files),
84
+ ...Object.values(source.files),
85
+ ].join("\n");
86
+ return [...claim.matchAll(/`([A-Za-z_]\w*)`/g)].some(([_, identifier]) => !evidence.includes(identifier));
87
+ }
88
+ function allowsContributionSections(source) {
89
+ return Object.keys(source.files).some((path) => /CONTRIBUTING|PULL_REQUEST_TEMPLATE/i.test(path));
90
+ }
91
+ function hasUnsupportedPath(claim, source) {
92
+ const paths = new Set([...source.tree, ...Object.keys(source.files)]);
93
+ const repositoryName = String(source.repo.full_name ?? "");
94
+ const references = [
95
+ ...[...claim.matchAll(/`([^`\n]+\/[^`\n]+)`/g)].map(([, value]) => value),
96
+ ...[...claim.matchAll(/`([^`\n]+\/)`/g)].map(([, value]) => value),
97
+ ...[
98
+ ...claim.matchAll(/(?:^|[\s("'`])((?!@)[A-Za-z0-9_.-]+\/[A-Za-z0-9_.*?{}<>:+-]+(?:\/[A-Za-z0-9_.*?{}<>:+-]+)*)/g),
99
+ ].map(([, value]) => value),
100
+ ];
101
+ return references.some((reference) => {
102
+ if (reference.startsWith("@") || reference === repositoryName)
103
+ return false;
104
+ const normalized = reference.replace(/^\.\/+/, "").replace(/\/+$/, "");
105
+ const wildcard = normalized.includes("*")
106
+ ? new RegExp(`^${normalized
107
+ .split("*")
108
+ .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
109
+ .join(".*")}$`)
110
+ : undefined;
111
+ return (!paths.has(normalized) &&
112
+ ![...paths].some((path) => wildcard ? wildcard.test(path) : path.startsWith(`${normalized}/`)));
113
+ });
114
+ }
115
+ function pullRequestUnit(pr) {
116
+ return JSON.stringify({
117
+ number: pr.number,
118
+ title: pr.title,
119
+ body: promptText(pr.body),
120
+ labels: pr.labels,
121
+ comments: pr.comments.map((comment) => promptText(comment, 2_000)),
122
+ reviews: pr.reviews.map((review) => promptText(review, 2_000)),
123
+ changedFiles: pr.changedFiles,
124
+ diff: promptText(pr.diff, 8_000),
125
+ });
126
+ }
127
+ function queueFor(provider, repo, ai, model, concurrency) {
128
+ return new AiBatch(provider, repo, Math.max(1, concurrency), `${ai}:v2:${model}`);
129
+ }
130
+ function extractRequest(prompt, maxTokens) {
131
+ return {
132
+ job: "extract_unit",
133
+ prompt,
134
+ maxTokens,
135
+ system: "Extract only repository-specific facts. Never invent files, commands, or process.",
136
+ };
137
+ }
138
+ function cleanSection(text, key) {
139
+ const clean = text.replace(/^\s*```[a-zA-Z0-9_-]*\s*$/gm, "").trim();
140
+ const heading = sectionTitles[key] ?? key;
141
+ return clean.replace(/^##\s+.+$/m, `## ${heading}`);
142
+ }
143
+ function validSection(text, key) {
144
+ const heading = `## ${sectionTitles[key] ?? key}`;
145
+ const bullets = text.match(/^-\s+/gm) ?? [];
146
+ return (text.startsWith(heading) &&
147
+ bullets.length > 0 &&
148
+ bullets.length <= 6 &&
149
+ !text.includes("```") &&
150
+ !/\b(?:TODO|TBD)\b/i.test(text));
151
+ }
152
+ function sectionGoal(key) {
153
+ const goals = {
154
+ layout: "Describe module responsibilities and important dependency or change-impact paths; do not list routine public symbols.",
155
+ tests: "State which verification is required after a behavior change and the dependency/setup that makes it meaningful.",
156
+ devloop: "Give local build, debugging, or change-impact guidance that is not already stated in Shipping.",
157
+ ship: "Give one concise release/CI checklist; merge overlapping workflow, artifact, and release-gate facts.",
158
+ style: "Keep only repository-specific, evidenced conventions. Return only the heading when none are actionable.",
159
+ };
160
+ return goals[key] ?? "Keep only actionable repository-specific guidance.";
161
+ }
162
+ function synthesisFacts(facts, key) {
163
+ return facts
164
+ .filter((item) => item.sectionKey === key)
165
+ .sort((a, b) => (b.status === "active" ? 1 : 0) - (a.status === "active" ? 1 : 0) ||
166
+ ({
167
+ current: 3,
168
+ "repeated-history": 2,
169
+ "historical-example": 1,
170
+ }[b.scope] ?? 0) -
171
+ ({
172
+ current: 3,
173
+ "repeated-history": 2,
174
+ "historical-example": 1,
175
+ }[a.scope] ?? 0) ||
176
+ b.weight - a.weight ||
177
+ b.evidence.length - a.evidence.length ||
178
+ a.claim.localeCompare(b.claim))
179
+ .slice(0, 20);
180
+ }
181
+ export async function extractAiFacts(provider, repo, source, options, usage) {
182
+ const requests = [];
183
+ const evidence = [];
184
+ const scopes = [];
185
+ const prs = options.includePullRequests ? source.pullRequests : [];
186
+ for (const pr of prs) {
187
+ requests.push(extractRequest(`Analyze this pull request and return only a JSON array of atomic observations. Each item must have exactly:
188
+ {"sectionKey":"one allowed section","claim":"one directly evidenced observation","scope":"historical-example","confidence":"medium"}
189
+ Allowed sections: ${[...allowedSections].join(", ")}
190
+ Use evidence from this PR only. Do not generalize this PR into a repository-wide policy.
191
+ Return at most 6 items. Keep each claim under 180 characters. Do not explain the answer.
192
+
193
+ PULL REQUEST:
194
+ ${pullRequestUnit(pr)}`, 1_200));
195
+ evidence.push(`PR #${pr.number}`);
196
+ scopes.push("historical-example");
197
+ }
198
+ if (options.includeCodebase || options.includeHowRepoWorks) {
199
+ const files = Object.entries(source.files)
200
+ .map(([path, content]) => `${path}\n${promptText(content, 4_000)}`)
201
+ .join("\n\n");
202
+ if (files) {
203
+ requests.push(extractRequest(`Analyze these canonical repository files and return only a JSON array of atomic observations.
204
+ Each item must have exactly:
205
+ {"sectionKey":"one allowed section","claim":"one directly evidenced observation","scope":"current","confidence":"high"}
206
+ Allowed sections: ${[...allowedSections].join(", ")}
207
+ Focus on module responsibilities, test-to-code relationships, build/debug constraints,
208
+ release gates, and conventions that affect a code change. Ignore end-user feature lists,
209
+ installation walkthroughs, and generic programming advice. For source files, identify
210
+ what important modules own and how tests exercise them when the files provide evidence.
211
+ Do not infer PR titles, labels, review rules, or contributor policy unless the input
212
+ contains PR or pull-request-template evidence; every claim must be directly supported
213
+ by a provided file. Do not report a current version value; preserve the version
214
+ change or release rule instead.
215
+ Return at most 10 items. Keep each claim under 180 characters. Do not explain the answer.
216
+
217
+ FILES:
218
+ ${files}`, 2_200));
219
+ evidence.push("repository files");
220
+ scopes.push("current");
221
+ }
222
+ }
223
+ const queue = queueFor(provider, repo, options.ai, options.lowModel ?? "", options.aiConcurrent);
224
+ const responses = await queue.run(requests, usage);
225
+ const facts = [];
226
+ for (let index = 0; index < responses.length; index++) {
227
+ const response = responses[index];
228
+ if (!response)
229
+ continue;
230
+ try {
231
+ facts.push(...factsFromResponse(response.text, evidence[index], scopes[index] ?? "historical-example").filter((item) => !hasUnsupportedIdentifier(item.claim, source) &&
232
+ !hasUnsupportedPath(item.claim, source) &&
233
+ (evidence[index] !== "repository files" ||
234
+ allowsContributionSections(source) ||
235
+ !["title-body", "labels", "review-bar", "process"].includes(item.sectionKey))));
236
+ }
237
+ catch (error) {
238
+ console.log(`[ai] invalid ${evidence[index]} output skipped: ${String(error)}`);
239
+ }
240
+ if (index % 10 === 0 || index === responses.length - 1) {
241
+ console.log(`[ai] extract units ${index + 1}/${responses.length}`);
242
+ }
243
+ }
244
+ return facts;
245
+ }
246
+ function mergeFacts(base, extra) {
247
+ const merged = new Map(base.map((item) => [item.id, { ...item, evidence: [...item.evidence] }]));
248
+ for (const item of extra) {
249
+ const existing = merged.get(item.id);
250
+ if (!existing)
251
+ merged.set(item.id, item);
252
+ else {
253
+ existing.weight += item.weight;
254
+ existing.evidence = [
255
+ ...new Set([...existing.evidence, ...item.evidence]),
256
+ ];
257
+ }
258
+ }
259
+ return [...merged.values()];
260
+ }
261
+ export async function enrichFacts(provider, repo, base, source, options, usage) {
262
+ return mergeFacts(base, await extractAiFacts(provider, repo, source, options, usage));
263
+ }
264
+ export async function synthesizeSections(provider, repo, facts, _previousMarkdown, ai, model, concurrency, usage, onlySections) {
265
+ const requests = [];
266
+ const keys = [];
267
+ for (const key of [...new Set(facts.map((item) => item.sectionKey))]) {
268
+ if (onlySections && !onlySections.has(key))
269
+ continue;
270
+ const relevant = synthesisFacts(facts, key);
271
+ keys.push(key);
272
+ requests.push({
273
+ job: "synth_section",
274
+ maxTokens: 1_800,
275
+ reasoningEffort: "high",
276
+ system: `You synthesize contribution guidance for an unfamiliar open-source repository.
277
+ Help developers make correct implementation, testing, review, debugging, and release
278
+ decisions. Current repository evidence outranks history. A single historical
279
+ observation is not a repository-wide rule. If sources conflict and current evidence
280
+ does not resolve the conflict, omit the claim. Never invent paths, commands,
281
+ conventions, or policies. Do not summarize README prose or list incidental details.`,
282
+ prompt: `Write only the Markdown for the "${key}" section of a repository Agent Skill.
283
+ Use only the facts below. Keep rules that change implementation, testing, review,
284
+ debugging, or release decisions. Drop end-user README instructions, feature lists,
285
+ file inventories, duplicate commands, and generic programming advice. Preserve useful
286
+ existing guidance, remove contradictions, deduplicate overlapping facts, and do not
287
+ invent anything. Include the correct human-readable section heading. Use inline
288
+ backticks instead of fenced code blocks. Do not create examples, placeholder
289
+ identifiers, or policies that are not present in the facts. Prefer at most
290
+ 6 high-value bullets; keep a concrete example only when it changes an agent decision.
291
+ Do not preserve a current version value that can become stale. In Shipping,
292
+ consolidate CI and release facts instead of repeating the same workflow rule.
293
+ Facts are raw candidates, not all mandatory output. A fact supported by only one PR
294
+ is an example, not a repository-wide rule; omit it unless canonical files or repeated
295
+ evidence support it. Omit conflicting claims rather than guessing. Return only the
296
+ heading when this section has no durable guidance. Do not preserve bot-specific
297
+ automation commands, reviewer identities, or one-off historical requests as
298
+ repository-wide process guidance.
299
+
300
+ SECTION GOAL:
301
+ ${sectionGoal(key)}
302
+
303
+ FACTS:
304
+ ${JSON.stringify(relevant)}`,
305
+ });
306
+ }
307
+ const queue = queueFor(provider, repo, ai, model, ai === "hetzner" ? 1 : concurrency);
308
+ const responses = await queue.run(requests, usage);
309
+ const overrides = {};
310
+ responses.forEach((response, index) => {
311
+ const section = response ? cleanSection(response.text, keys[index]) : "";
312
+ overrides[keys[index]] = validSection(section, keys[index]) ? section : "";
313
+ });
314
+ return overrides;
315
+ }
@@ -0,0 +1,49 @@
1
+ import type { Json, Options } from "../types.ts";
2
+ export type PullRequest = {
3
+ number: number;
4
+ title: string;
5
+ body: string;
6
+ state: string;
7
+ merged: boolean;
8
+ updatedAt: string;
9
+ headSha: string;
10
+ labels: string[];
11
+ additions: number;
12
+ deletions: number;
13
+ comments: string[];
14
+ reviews: string[];
15
+ changedFiles: string[];
16
+ diff: string;
17
+ };
18
+ export type Source = {
19
+ repo: Json;
20
+ tree: string[];
21
+ treeSha: Record<string, string>;
22
+ files: Record<string, string>;
23
+ pullRequests: PullRequest[];
24
+ commits: Json[];
25
+ };
26
+ export type Fact = {
27
+ id: string;
28
+ sectionKey: string;
29
+ claim: string;
30
+ evidence: string[];
31
+ weight: number;
32
+ scope: "current" | "repeated-history" | "historical-example";
33
+ confidence: "high" | "medium" | "low";
34
+ status: "active" | "contradicted" | "stale";
35
+ };
36
+ export type State = {
37
+ version: 1;
38
+ repo: string;
39
+ options: Omit<Options, "command" | "aiToken" | "githubPat">;
40
+ source: Source;
41
+ facts: Fact[];
42
+ sectionHashes: Record<string, string>;
43
+ scanDone: {
44
+ pullRequests: number;
45
+ commits: number;
46
+ updatedAt: string;
47
+ };
48
+ updatedAt: string;
49
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { Source } from "./types.ts";
2
+ export type ValidationResult = {
3
+ valid: boolean;
4
+ errors: string[];
5
+ };
6
+ export declare function validateSkill(markdown: string, outputDirectory: string, source?: Source): Promise<ValidationResult>;