@nanonets/graft 0.21.0 → 0.22.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 (348) hide show
  1. package/README.md +549 -315
  2. package/dist/app/review-process.d.ts.map +1 -1
  3. package/dist/app/review-process.js +6 -5
  4. package/dist/app/review-process.js.map +1 -1
  5. package/dist/app/sources.js +2 -2
  6. package/dist/app/sources.js.map +1 -1
  7. package/dist/ask/ask.d.ts +12 -6
  8. package/dist/ask/ask.d.ts.map +1 -1
  9. package/dist/ask/ask.js +28 -38
  10. package/dist/ask/ask.js.map +1 -1
  11. package/dist/bin/graft.d.ts +3 -0
  12. package/dist/bin/graft.d.ts.map +1 -0
  13. package/dist/bin/graft.js +8 -0
  14. package/dist/bin/graft.js.map +1 -0
  15. package/dist/bin/trail.d.ts +3 -0
  16. package/dist/bin/trail.d.ts.map +1 -0
  17. package/dist/bin/trail.js +6 -0
  18. package/dist/bin/trail.js.map +1 -0
  19. package/dist/blast/blast-cli.d.ts.map +1 -1
  20. package/dist/blast/blast-cli.js +2 -1
  21. package/dist/blast/blast-cli.js.map +1 -1
  22. package/dist/blast/render.d.ts.map +1 -1
  23. package/dist/blast/render.js +3 -2
  24. package/dist/blast/render.js.map +1 -1
  25. package/dist/brand.d.ts +97 -0
  26. package/dist/brand.d.ts.map +1 -0
  27. package/dist/brand.js +254 -0
  28. package/dist/brand.js.map +1 -0
  29. package/dist/check/local-check.d.ts +104 -0
  30. package/dist/check/local-check.d.ts.map +1 -0
  31. package/dist/check/local-check.js +830 -0
  32. package/dist/check/local-check.js.map +1 -0
  33. package/dist/check/types.d.ts +31 -0
  34. package/dist/check/types.d.ts.map +1 -0
  35. package/dist/check/types.js +2 -0
  36. package/dist/check/types.js.map +1 -0
  37. package/dist/claude/format.d.ts +44 -1
  38. package/dist/claude/format.d.ts.map +1 -1
  39. package/dist/claude/format.js +113 -19
  40. package/dist/claude/format.js.map +1 -1
  41. package/dist/claude/hook-layers.d.ts +8 -0
  42. package/dist/claude/hook-layers.d.ts.map +1 -0
  43. package/dist/claude/hook-layers.js +72 -0
  44. package/dist/claude/hook-layers.js.map +1 -0
  45. package/dist/claude/hooks.d.ts +34 -0
  46. package/dist/claude/hooks.d.ts.map +1 -1
  47. package/dist/claude/hooks.js +220 -12
  48. package/dist/claude/hooks.js.map +1 -1
  49. package/dist/claude/init.d.ts +12 -1
  50. package/dist/claude/init.d.ts.map +1 -1
  51. package/dist/claude/init.js +48 -19
  52. package/dist/claude/init.js.map +1 -1
  53. package/dist/claude/session-metrics.d.ts +1 -1
  54. package/dist/claude/session-metrics.d.ts.map +1 -1
  55. package/dist/claude/session-metrics.js +11 -8
  56. package/dist/claude/session-metrics.js.map +1 -1
  57. package/dist/claude/settings-merge.d.ts +1 -0
  58. package/dist/claude/settings-merge.d.ts.map +1 -1
  59. package/dist/claude/settings-merge.js +21 -18
  60. package/dist/claude/settings-merge.js.map +1 -1
  61. package/dist/claude/shim-template.d.ts +17 -2
  62. package/dist/claude/shim-template.d.ts.map +1 -1
  63. package/dist/claude/shim-template.js +116 -22
  64. package/dist/claude/shim-template.js.map +1 -1
  65. package/dist/claude/skill-template.d.ts +10 -1
  66. package/dist/claude/skill-template.d.ts.map +1 -1
  67. package/dist/claude/skill-template.js +132 -1
  68. package/dist/claude/skill-template.js.map +1 -1
  69. package/dist/claude/state.d.ts +8 -0
  70. package/dist/claude/state.d.ts.map +1 -1
  71. package/dist/claude/state.js.map +1 -1
  72. package/dist/claude/statusline.d.ts.map +1 -1
  73. package/dist/claude/statusline.js +4 -1
  74. package/dist/claude/statusline.js.map +1 -1
  75. package/dist/claude/tally.js +1 -1
  76. package/dist/claude/tally.js.map +1 -1
  77. package/dist/cli-epilogue.d.ts +0 -6
  78. package/dist/cli-epilogue.d.ts.map +1 -1
  79. package/dist/cli-epilogue.js +5 -4
  80. package/dist/cli-epilogue.js.map +1 -1
  81. package/dist/cli-meta.d.ts +8 -0
  82. package/dist/cli-meta.d.ts.map +1 -1
  83. package/dist/cli-meta.js +28 -8
  84. package/dist/cli-meta.js.map +1 -1
  85. package/dist/cli-picker.d.ts.map +1 -1
  86. package/dist/cli-picker.js +11 -7
  87. package/dist/cli-picker.js.map +1 -1
  88. package/dist/cli.d.ts +0 -5
  89. package/dist/cli.d.ts.map +1 -1
  90. package/dist/cli.js +1037 -595
  91. package/dist/cli.js.map +1 -1
  92. package/dist/cloud/diff.d.ts +18 -0
  93. package/dist/cloud/diff.d.ts.map +1 -0
  94. package/dist/cloud/diff.js +58 -0
  95. package/dist/cloud/diff.js.map +1 -0
  96. package/dist/cloud/nudge.d.ts +66 -0
  97. package/dist/cloud/nudge.d.ts.map +1 -0
  98. package/dist/cloud/nudge.js +269 -0
  99. package/dist/cloud/nudge.js.map +1 -0
  100. package/dist/cloud/workspace-sync.d.ts +58 -0
  101. package/dist/cloud/workspace-sync.d.ts.map +1 -0
  102. package/dist/cloud/workspace-sync.js +268 -0
  103. package/dist/cloud/workspace-sync.js.map +1 -0
  104. package/dist/cloud/workspace.d.ts +159 -0
  105. package/dist/cloud/workspace.d.ts.map +1 -0
  106. package/dist/cloud/workspace.js +134 -0
  107. package/dist/cloud/workspace.js.map +1 -0
  108. package/dist/context/check.d.ts +1 -1
  109. package/dist/context/check.d.ts.map +1 -1
  110. package/dist/context/check.js +9 -8
  111. package/dist/context/check.js.map +1 -1
  112. package/dist/context/node-file.js +1 -1
  113. package/dist/context/node-file.js.map +1 -1
  114. package/dist/context/savings.d.ts.map +1 -1
  115. package/dist/context/savings.js +10 -7
  116. package/dist/context/savings.js.map +1 -1
  117. package/dist/engine.d.ts.map +1 -1
  118. package/dist/engine.js +3 -1
  119. package/dist/engine.js.map +1 -1
  120. package/dist/graph/build.d.ts +30 -0
  121. package/dist/graph/build.d.ts.map +1 -1
  122. package/dist/graph/build.js +65 -19
  123. package/dist/graph/build.js.map +1 -1
  124. package/dist/graph/cards.d.ts.map +1 -1
  125. package/dist/graph/cards.js +4 -2
  126. package/dist/graph/cards.js.map +1 -1
  127. package/dist/graph/check.d.ts.map +1 -1
  128. package/dist/graph/check.js +17 -18
  129. package/dist/graph/check.js.map +1 -1
  130. package/dist/graph/container.d.ts.map +1 -1
  131. package/dist/graph/container.js +7 -2
  132. package/dist/graph/container.js.map +1 -1
  133. package/dist/graph/extract-cache.d.ts +4 -0
  134. package/dist/graph/extract-cache.d.ts.map +1 -1
  135. package/dist/graph/extract-cache.js.map +1 -1
  136. package/dist/graph/extract.d.ts +2 -0
  137. package/dist/graph/extract.d.ts.map +1 -1
  138. package/dist/graph/extract.js +5 -32
  139. package/dist/graph/extract.js.map +1 -1
  140. package/dist/graph/generic.d.ts +7 -1
  141. package/dist/graph/generic.d.ts.map +1 -1
  142. package/dist/graph/generic.js +48 -8
  143. package/dist/graph/generic.js.map +1 -1
  144. package/dist/graph/native.d.ts +38 -0
  145. package/dist/graph/native.d.ts.map +1 -0
  146. package/dist/graph/native.js +169 -0
  147. package/dist/graph/native.js.map +1 -0
  148. package/dist/graph/refresh.d.ts.map +1 -1
  149. package/dist/graph/refresh.js +3 -2
  150. package/dist/graph/refresh.js.map +1 -1
  151. package/dist/graph/traverse-cli.d.ts.map +1 -1
  152. package/dist/graph/traverse-cli.js +4 -3
  153. package/dist/graph/traverse-cli.js.map +1 -1
  154. package/dist/graph/workspace-cli.d.ts +2 -0
  155. package/dist/graph/workspace-cli.d.ts.map +1 -1
  156. package/dist/graph/workspace-cli.js +13 -1
  157. package/dist/graph/workspace-cli.js.map +1 -1
  158. package/dist/graph/workspace.d.ts.map +1 -1
  159. package/dist/graph/workspace.js +6 -5
  160. package/dist/graph/workspace.js.map +1 -1
  161. package/dist/hosts/antigravity.d.ts +10 -3
  162. package/dist/hosts/antigravity.d.ts.map +1 -1
  163. package/dist/hosts/antigravity.js +27 -10
  164. package/dist/hosts/antigravity.js.map +1 -1
  165. package/dist/hosts/claude-global.d.ts +27 -4
  166. package/dist/hosts/claude-global.d.ts.map +1 -1
  167. package/dist/hosts/claude-global.js +90 -9
  168. package/dist/hosts/claude-global.js.map +1 -1
  169. package/dist/hosts/codex-hooks.d.ts.map +1 -1
  170. package/dist/hosts/codex-hooks.js +1 -2
  171. package/dist/hosts/codex-hooks.js.map +1 -1
  172. package/dist/hosts/config-write.d.ts.map +1 -1
  173. package/dist/hosts/config-write.js +2 -1
  174. package/dist/hosts/config-write.js.map +1 -1
  175. package/dist/hosts/cursor-hooks.d.ts +5 -2
  176. package/dist/hosts/cursor-hooks.d.ts.map +1 -1
  177. package/dist/hosts/cursor-hooks.js +14 -9
  178. package/dist/hosts/cursor-hooks.js.map +1 -1
  179. package/dist/hosts/init.d.ts +3 -0
  180. package/dist/hosts/init.d.ts.map +1 -1
  181. package/dist/hosts/init.js +32 -10
  182. package/dist/hosts/init.js.map +1 -1
  183. package/dist/hosts/instructions.d.ts +9 -4
  184. package/dist/hosts/instructions.d.ts.map +1 -1
  185. package/dist/hosts/instructions.js +19 -16
  186. package/dist/hosts/instructions.js.map +1 -1
  187. package/dist/hosts/mcp-config.d.ts +19 -4
  188. package/dist/hosts/mcp-config.d.ts.map +1 -1
  189. package/dist/hosts/mcp-config.js +81 -30
  190. package/dist/hosts/mcp-config.js.map +1 -1
  191. package/dist/hosts/plan.d.ts.map +1 -1
  192. package/dist/hosts/plan.js +11 -9
  193. package/dist/hosts/plan.js.map +1 -1
  194. package/dist/hosts/registry.d.ts +25 -2
  195. package/dist/hosts/registry.d.ts.map +1 -1
  196. package/dist/hosts/registry.js +53 -0
  197. package/dist/hosts/registry.js.map +1 -1
  198. package/dist/hosts/retract.d.ts.map +1 -1
  199. package/dist/hosts/retract.js +73 -33
  200. package/dist/hosts/retract.js.map +1 -1
  201. package/dist/hosts/sections.d.ts +17 -1
  202. package/dist/hosts/sections.d.ts.map +1 -1
  203. package/dist/hosts/sections.js +52 -2
  204. package/dist/hosts/sections.js.map +1 -1
  205. package/dist/mcp/instructions.d.ts +1 -25
  206. package/dist/mcp/instructions.d.ts.map +1 -1
  207. package/dist/mcp/instructions.js +6 -4
  208. package/dist/mcp/instructions.js.map +1 -1
  209. package/dist/mcp/server.d.ts.map +1 -1
  210. package/dist/mcp/server.js +7 -3
  211. package/dist/mcp/server.js.map +1 -1
  212. package/dist/mcp/tool-names.d.ts +10 -3
  213. package/dist/mcp/tool-names.d.ts.map +1 -1
  214. package/dist/mcp/tool-names.js +13 -5
  215. package/dist/mcp/tool-names.js.map +1 -1
  216. package/dist/mcp/tools.d.ts.map +1 -1
  217. package/dist/mcp/tools.js +6 -5
  218. package/dist/mcp/tools.js.map +1 -1
  219. package/dist/notes/ask-notes.d.ts +8 -0
  220. package/dist/notes/ask-notes.d.ts.map +1 -0
  221. package/dist/notes/ask-notes.js +18 -0
  222. package/dist/notes/ask-notes.js.map +1 -0
  223. package/dist/notes/branches.d.ts +85 -0
  224. package/dist/notes/branches.d.ts.map +1 -0
  225. package/dist/notes/branches.js +238 -0
  226. package/dist/notes/branches.js.map +1 -0
  227. package/dist/notes/cold-start.d.ts +127 -0
  228. package/dist/notes/cold-start.d.ts.map +1 -0
  229. package/dist/notes/cold-start.js +476 -0
  230. package/dist/notes/cold-start.js.map +1 -0
  231. package/dist/notes/git-facts.d.ts +17 -0
  232. package/dist/notes/git-facts.d.ts.map +1 -0
  233. package/dist/notes/git-facts.js +117 -0
  234. package/dist/notes/git-facts.js.map +1 -0
  235. package/dist/notes/home.d.ts +87 -0
  236. package/dist/notes/home.d.ts.map +1 -0
  237. package/dist/notes/home.js +423 -0
  238. package/dist/notes/home.js.map +1 -0
  239. package/dist/notes/notes.d.ts +178 -0
  240. package/dist/notes/notes.d.ts.map +1 -0
  241. package/dist/notes/notes.js +498 -0
  242. package/dist/notes/notes.js.map +1 -0
  243. package/dist/notes/repo-trail.d.ts +16 -0
  244. package/dist/notes/repo-trail.d.ts.map +1 -0
  245. package/dist/notes/repo-trail.js +74 -0
  246. package/dist/notes/repo-trail.js.map +1 -0
  247. package/dist/notes/session-cost.d.ts +21 -0
  248. package/dist/notes/session-cost.d.ts.map +1 -0
  249. package/dist/notes/session-cost.js +162 -0
  250. package/dist/notes/session-cost.js.map +1 -0
  251. package/dist/search/grep-cli.d.ts.map +1 -1
  252. package/dist/search/grep-cli.js +4 -3
  253. package/dist/search/grep-cli.js.map +1 -1
  254. package/dist/skills/skills.d.ts +118 -0
  255. package/dist/skills/skills.d.ts.map +1 -0
  256. package/dist/skills/skills.js +370 -0
  257. package/dist/skills/skills.js.map +1 -0
  258. package/dist/telemetry/contract.d.ts +2 -14
  259. package/dist/telemetry/contract.d.ts.map +1 -1
  260. package/dist/telemetry/contract.js +39 -24
  261. package/dist/telemetry/contract.js.map +1 -1
  262. package/dist/telemetry/gate.d.ts.map +1 -1
  263. package/dist/telemetry/gate.js +2 -1
  264. package/dist/telemetry/gate.js.map +1 -1
  265. package/dist/telemetry/notice.d.ts +1 -1
  266. package/dist/telemetry/notice.d.ts.map +1 -1
  267. package/dist/telemetry/notice.js +6 -5
  268. package/dist/telemetry/notice.js.map +1 -1
  269. package/dist/telemetry/track.d.ts.map +1 -1
  270. package/dist/telemetry/track.js +5 -0
  271. package/dist/telemetry/track.js.map +1 -1
  272. package/dist/upkeep-run.d.ts +7 -0
  273. package/dist/upkeep-run.d.ts.map +1 -1
  274. package/dist/upkeep-run.js +52 -12
  275. package/dist/upkeep-run.js.map +1 -1
  276. package/dist/upkeep.d.ts +9 -19
  277. package/dist/upkeep.d.ts.map +1 -1
  278. package/dist/upkeep.js +38 -59
  279. package/dist/upkeep.js.map +1 -1
  280. package/dist/util/browser.d.ts +3 -0
  281. package/dist/util/browser.d.ts.map +1 -0
  282. package/dist/util/browser.js +14 -0
  283. package/dist/util/browser.js.map +1 -0
  284. package/dist/util/front-matter.d.ts +19 -0
  285. package/dist/util/front-matter.d.ts.map +1 -0
  286. package/dist/util/front-matter.js +21 -0
  287. package/dist/util/front-matter.js.map +1 -0
  288. package/dist/util/state.d.ts +1 -18
  289. package/dist/util/state.d.ts.map +1 -1
  290. package/dist/util/state.js +4 -2
  291. package/dist/util/state.js.map +1 -1
  292. package/dist/viz/assemble.js +1 -1
  293. package/dist/viz/assemble.js.map +1 -1
  294. package/dist/viz/serve.d.ts.map +1 -1
  295. package/dist/viz/serve.js +4 -3
  296. package/dist/viz/serve.js.map +1 -1
  297. package/package.json +7 -7
  298. package/scripts/pack-trail.mjs +53 -0
  299. package/scripts/postinstall.mjs +17 -2
  300. package/scripts/run-tests.mjs +4 -1
  301. package/dist/brain/attach.d.ts +0 -44
  302. package/dist/brain/attach.d.ts.map +0 -1
  303. package/dist/brain/attach.js +0 -63
  304. package/dist/brain/attach.js.map +0 -1
  305. package/dist/brain/claude-md.d.ts +0 -61
  306. package/dist/brain/claude-md.d.ts.map +0 -1
  307. package/dist/brain/claude-md.js +0 -171
  308. package/dist/brain/claude-md.js.map +0 -1
  309. package/dist/brain/connect.d.ts +0 -59
  310. package/dist/brain/connect.d.ts.map +0 -1
  311. package/dist/brain/connect.js +0 -80
  312. package/dist/brain/connect.js.map +0 -1
  313. package/dist/brain/context-files.d.ts +0 -79
  314. package/dist/brain/context-files.d.ts.map +0 -1
  315. package/dist/brain/context-files.js +0 -245
  316. package/dist/brain/context-files.js.map +0 -1
  317. package/dist/brain/link.d.ts +0 -106
  318. package/dist/brain/link.d.ts.map +0 -1
  319. package/dist/brain/link.js +0 -167
  320. package/dist/brain/link.js.map +0 -1
  321. package/dist/brain/pull.d.ts +0 -17
  322. package/dist/brain/pull.d.ts.map +0 -1
  323. package/dist/brain/pull.js +0 -172
  324. package/dist/brain/pull.js.map +0 -1
  325. package/dist/brain/push.d.ts +0 -155
  326. package/dist/brain/push.d.ts.map +0 -1
  327. package/dist/brain/push.js +0 -304
  328. package/dist/brain/push.js.map +0 -1
  329. package/dist/brain/signup.d.ts +0 -117
  330. package/dist/brain/signup.d.ts.map +0 -1
  331. package/dist/brain/signup.js +0 -276
  332. package/dist/brain/signup.js.map +0 -1
  333. package/dist/brain/upload.d.ts +0 -61
  334. package/dist/brain/upload.d.ts.map +0 -1
  335. package/dist/brain/upload.js +0 -210
  336. package/dist/brain/upload.js.map +0 -1
  337. package/dist/brain/watch.d.ts +0 -209
  338. package/dist/brain/watch.d.ts.map +0 -1
  339. package/dist/brain/watch.js +0 -396
  340. package/dist/brain/watch.js.map +0 -1
  341. package/dist/brain/wire.d.ts +0 -38
  342. package/dist/brain/wire.d.ts.map +0 -1
  343. package/dist/brain/wire.js +0 -133
  344. package/dist/brain/wire.js.map +0 -1
  345. package/dist/legacy-args.d.ts +0 -12
  346. package/dist/legacy-args.d.ts.map +0 -1
  347. package/dist/legacy-args.js +0 -31
  348. package/dist/legacy-args.js.map +0 -1
@@ -0,0 +1,830 @@
1
+ /**
2
+ * `trail check` without a server: the deterministic half of Trail's check,
3
+ * ported from the Go service (trail_check.go, trail_check_rules.go and
4
+ * trail_text.go in assign's backend), so a diff can be held up against the
5
+ * learnings in the repo's `.trail/` (teammates' pushed branches included),
6
+ * the person's own, and the repo's skills, all on this machine. ("Notes"
7
+ * below are learnings: the type kept its old name.)
8
+ *
9
+ * Knowledge is picked the way the server picked it: notes that touched a
10
+ * changed file or its folder, then notes and skills that share the diff's
11
+ * words. Then, in each change block (a run of removed and added lines), the
12
+ * rules look for three things:
13
+ *
14
+ * - a value changed against a recorded number: a line changes `retries` from
15
+ * 3 to 5 and a note on this code says "three retries" (conflict), or warned
16
+ * about or ruled out 5 (conflict), or decided on 5 (follows);
17
+ * - an approach a note's "Tried and ruled out" section names, brought back by
18
+ * the added lines (conflict);
19
+ * - a decision or takeaway whose words the added lines carry (follows).
20
+ *
21
+ * It errs towards silence: a conflict fails the CLI, so one is reported only
22
+ * on evidence in the diff itself, quoted from the source.
23
+ *
24
+ * Approved brain rules exist only on the server, so this check has none.
25
+ */
26
+ import { posix } from "node:path";
27
+ const MAX_CANDIDATES = 25;
28
+ const MAX_SKILLS = 6;
29
+ /** Knowledge matched on words, not on files, must share at least this many terms with the diff. */
30
+ const MIN_TERM_HITS = 2;
31
+ const DIFF_TERMS = 24;
32
+ const SUMMARY_WORDS = 12;
33
+ const QUOTE_WORDS = 40;
34
+ const MAX_FINDINGS = 20;
35
+ const MAX_FILES = 500;
36
+ /** Notes read before ranking, as the server's search reads them. */
37
+ const NOTE_POOL = 100;
38
+ // Ranking weights: touching a changed file outweighs any wording, a shared
39
+ // folder is a weaker hint, recency only breaks near-ties.
40
+ const SCORE_FILE_EXACT = 1.0;
41
+ const SCORE_FILE_DIR = 0.4;
42
+ const SCORE_RECENCY = 0.2;
43
+ const RECENCY_DAYS = 30;
44
+ /**
45
+ * Check a diff against the notes and skills on this machine, with the rules
46
+ * Trail's server falls back to when it has no model. Same shape as the
47
+ * server's answer: `files` is the changed-file count, `notes` and `skills`
48
+ * how many the check was given (the server reports how many it has indexed),
49
+ * and the findings, conflicts first.
50
+ */
51
+ export function checkLocally(input) {
52
+ const now = input.now ?? new Date();
53
+ const diffFiles = parseTrailDiff(input.diff);
54
+ const files = trailCheckFiles(input.files, diffFiles).slice(0, MAX_FILES);
55
+ const out = { files: files.length, notes: input.notes.length, skills: input.skills.length, findings: [] };
56
+ if (files.length === 0)
57
+ return out;
58
+ const terms = trailDiffTerms(diffFiles.flatMap((f) => addedText(f)), DIFF_TERMS);
59
+ const { files: fileList, dirs: dirList } = filesAndDirs(files);
60
+ const notes = notePool(input.notes, terms, fileList, dirList);
61
+ const skills = input.skills.map((skill) => ({
62
+ skill,
63
+ textScore: localTextScore(termHits(`${skill.name}\n${candidateText({ kind: "skill", skill })}`, terms)),
64
+ }));
65
+ const candidates = selectTrailCheckCandidates(files, terms, notes, skills, now);
66
+ if (candidates.length === 0)
67
+ return out;
68
+ out.findings = judgeTrailCheck(candidates, diffFiles);
69
+ return out;
70
+ }
71
+ /* -------------------------------------------------------------------------- */
72
+ /* text */
73
+ /* -------------------------------------------------------------------------- */
74
+ /**
75
+ * Words that say nothing about what a change is about: English filler plus
76
+ * the keywords and names every diff is full of.
77
+ */
78
+ const STOPWORDS = new Set(`a about above after again against all also am an and any are as at be because been before
79
+ being below between both but by can could did do does doing done down during each else
80
+ few for from further get got had has have having he her here hers him his how i if in
81
+ into is it its itself just let like make made me more most my no nor not now of off on
82
+ once only or other our out over own same she should so some such than that the their
83
+ them then there these they this those through to too under until up use used using very
84
+ was we were what when where which while who whom why will with would you your yours
85
+ new old add adds added fix fixes fixed change changes changed update updates updated
86
+ way thing things work works working need needs want try tried still back see look
87
+ func function return returns var const let type struct interface package import def
88
+ class self this nil null none true false err error errors string int bool byte float
89
+ ctx context fmt log if else for range switch case default break continue go defer
90
+ public private static void async await export from require module`.split(/\s+/).filter(Boolean));
91
+ const LOWER = /\p{Ll}/u;
92
+ const UPPER = /\p{Lu}/u;
93
+ /** `NormalizeBox` → [Normalize, Box], `max_retries` → [max, retries], `HTTPServer` → [HTTP, Server]. */
94
+ export function splitIdentifier(word) {
95
+ const parts = [];
96
+ for (const chunk of word.split("_")) {
97
+ if (!chunk)
98
+ continue;
99
+ const runes = Array.from(chunk);
100
+ let start = 0;
101
+ for (let i = 1; i < runes.length; i++) {
102
+ const prev = runes[i - 1];
103
+ const cur = runes[i];
104
+ const lowerToUpper = LOWER.test(prev) && UPPER.test(cur);
105
+ // "HTTPServer" splits before the last capital of the run.
106
+ const acronymEnd = UPPER.test(prev) && UPPER.test(cur) && i + 1 < runes.length && LOWER.test(runes[i + 1]);
107
+ if (lowerToUpper || acronymEnd) {
108
+ parts.push(runes.slice(start, i).join(""));
109
+ start = i;
110
+ }
111
+ }
112
+ parts.push(runes.slice(start).join(""));
113
+ }
114
+ return parts;
115
+ }
116
+ /** Runs of letters, digits and underscores. */
117
+ function rawWords(text) {
118
+ return text.split(/[^\p{L}\p{Nd}_]+/u).filter(Boolean);
119
+ }
120
+ function keepTerm(t) {
121
+ return t.length >= 3 && t.length <= 40 && !STOPWORDS.has(t) && !/^\d+$/.test(t);
122
+ }
123
+ /**
124
+ * The terms of a piece of text, lower-cased, deduplicated, in first-seen
125
+ * order. Identifiers give their parts and, for camelCase, the whole word too.
126
+ * At most `max` (0: no cap).
127
+ */
128
+ export function trailTerms(text, max = 0) {
129
+ const seen = new Set();
130
+ const out = [];
131
+ const add = (raw) => {
132
+ const t = raw.toLowerCase();
133
+ if (!/^[a-z0-9]+$/.test(t) || !keepTerm(t) || seen.has(t))
134
+ return;
135
+ seen.add(t);
136
+ out.push(t);
137
+ };
138
+ for (const w of rawWords(text)) {
139
+ const parts = splitIdentifier(w);
140
+ if (parts.length > 1 && !w.includes("_"))
141
+ add(w);
142
+ for (const p of parts)
143
+ add(p);
144
+ if (max > 0 && out.length >= max)
145
+ return out.slice(0, max);
146
+ }
147
+ return out;
148
+ }
149
+ /**
150
+ * Folds the plural and simple verb endings that make two people's words for
151
+ * the same thing differ (`retries`/`retry`). Much weaker than a stemmer on
152
+ * purpose: it only has to stop overlap detection missing the obvious.
153
+ */
154
+ export function normTerm(term) {
155
+ const t = term.toLowerCase();
156
+ if (t.length > 4 && t.endsWith("ies"))
157
+ return `${t.slice(0, -3)}y`;
158
+ if (t.length > 5 && t.endsWith("ing"))
159
+ return t.slice(0, -3);
160
+ if (t.length > 3 && t.endsWith("s") && !t.endsWith("ss"))
161
+ return t.slice(0, -1);
162
+ return t;
163
+ }
164
+ function normTermSet(text) {
165
+ return new Set(trailTerms(text).map(normTerm));
166
+ }
167
+ /** `./a.go` → `a.go`. */
168
+ function cleanRepoPath(p) {
169
+ const t = p.trim();
170
+ return t.startsWith("./") ? t.slice(2) : t;
171
+ }
172
+ /** The file part of a touch: `internal/ocr/bbox.go#NormalizeBox` → `internal/ocr/bbox.go`. */
173
+ function touchFile(touch) {
174
+ const t = touch.trim();
175
+ const hash = t.indexOf("#");
176
+ return cleanRepoPath(hash >= 0 ? t.slice(0, hash) : t);
177
+ }
178
+ /** Go's path.Dir. */
179
+ function pathDir(p) {
180
+ const dir = p.slice(0, p.lastIndexOf("/") + 1);
181
+ if (!dir)
182
+ return ".";
183
+ const n = posix.normalize(dir);
184
+ return n.length > 1 ? n.replace(/\/+$/, "") || "/" : n;
185
+ }
186
+ /** A file's folder, or "" at the root: sharing the root says nothing about two files. */
187
+ function repoDir(file) {
188
+ const d = pathDir(file);
189
+ return d === "." || d === "/" ? "" : d;
190
+ }
191
+ /** The cleaned, deduplicated files and their non-root folders, both sorted. */
192
+ function filesAndDirs(list) {
193
+ const files = new Set();
194
+ const dirs = new Set();
195
+ for (const raw of list) {
196
+ const f = touchFile(raw);
197
+ if (!f)
198
+ continue;
199
+ files.add(f);
200
+ const d = repoDir(f);
201
+ if (d)
202
+ dirs.add(d);
203
+ }
204
+ return { files: [...files].sort(), dirs: [...dirs].sort() };
205
+ }
206
+ function words(s) {
207
+ return s.split(/\s+/).filter(Boolean);
208
+ }
209
+ function capWords(s, n) {
210
+ return words(s).slice(0, n).join(" ");
211
+ }
212
+ /** Case and whitespace folded, so a quote copied across a line break still matches its source. */
213
+ function normQuote(s) {
214
+ return words(s).join(" ").toLowerCase();
215
+ }
216
+ /** Every line the diff adds to the file. */
217
+ export function addedText(f) {
218
+ return f.lines.filter((l) => l.op === "+").map((l) => l.text);
219
+ }
220
+ /** Whether a new-file line number lies inside one of the file's hunks. */
221
+ export function inHunk(f, line) {
222
+ return f.hunks.some((h) => line >= h.start && line < h.start + h.len);
223
+ }
224
+ function stripDiffPrefix(raw) {
225
+ let p = raw.trim();
226
+ const tab = p.indexOf("\t"); // "+++ b/x.go\t2026-10-07 ..."
227
+ if (tab >= 0)
228
+ p = p.slice(0, tab);
229
+ if (p === "/dev/null")
230
+ return "";
231
+ if (p.startsWith("a/") || p.startsWith("b/"))
232
+ p = p.slice(2);
233
+ return cleanRepoPath(p);
234
+ }
235
+ function atoi(s) {
236
+ return /^[+-]?\d+$/.test(s) ? Number(s) : null;
237
+ }
238
+ /** `@@ -a,b +c,d @@` → the new start and the old and new line counts. */
239
+ function parseHunkHeader(line) {
240
+ const fields = words(line);
241
+ if (fields.length < 3 || fields[0] !== "@@")
242
+ return null;
243
+ if (!fields[1].startsWith("-") || !fields[2].startsWith("+"))
244
+ return null;
245
+ const span = (s) => {
246
+ const body = s.slice(1);
247
+ const comma = body.indexOf(",");
248
+ const start = atoi(comma >= 0 ? body.slice(0, comma) : body);
249
+ const count = comma >= 0 ? atoi(body.slice(comma + 1)) : 1;
250
+ return start === null || count === null ? null : [start, count];
251
+ };
252
+ const old = span(fields[1]);
253
+ const neu = span(fields[2]);
254
+ if (!old || !neu)
255
+ return null;
256
+ return { newStart: neu[0], oldLen: old[1], newLen: neu[1] };
257
+ }
258
+ /**
259
+ * A unified diff (git's or plain) as files. Hunk bodies are read by their
260
+ * declared line counts, so an added line that itself starts with `+++` is not
261
+ * taken for a file header. A deleted file keeps its old path.
262
+ */
263
+ export function parseTrailDiff(diff) {
264
+ const files = [];
265
+ let cur = null;
266
+ let oldPath = "";
267
+ let oldLeft = 0;
268
+ let newLeft = 0;
269
+ let newLine = 0;
270
+ for (const raw of diff.split("\n")) {
271
+ let line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
272
+ if (cur && (oldLeft > 0 || newLeft > 0)) {
273
+ if (line === "")
274
+ line = " "; // some tools strip the blank context line's leading space
275
+ const op = line[0];
276
+ if (op === " ") {
277
+ cur.lines.push({ new: newLine, op: " ", text: line.slice(1) });
278
+ newLine++;
279
+ oldLeft--;
280
+ newLeft--;
281
+ continue;
282
+ }
283
+ if (op === "+") {
284
+ cur.lines.push({ new: newLine, op: "+", text: line.slice(1) });
285
+ newLine++;
286
+ newLeft--;
287
+ continue;
288
+ }
289
+ if (op === "-") {
290
+ cur.lines.push({ new: 0, op: "-", text: line.slice(1) });
291
+ oldLeft--;
292
+ continue;
293
+ }
294
+ if (op === "\\")
295
+ continue; // ""
296
+ oldLeft = 0;
297
+ newLeft = 0;
298
+ }
299
+ if (line.startsWith("diff --git ")) {
300
+ oldPath = "";
301
+ cur = null;
302
+ }
303
+ else if (line.startsWith("--- ")) {
304
+ oldPath = stripDiffPrefix(line.slice(4));
305
+ }
306
+ else if (line.startsWith("+++ ")) {
307
+ const p = stripDiffPrefix(line.slice(4)) || oldPath;
308
+ if (!p) {
309
+ cur = null;
310
+ continue;
311
+ }
312
+ cur = { path: p, lines: [], hunks: [] };
313
+ files.push(cur);
314
+ }
315
+ else if (line.startsWith("@@")) {
316
+ if (!cur)
317
+ continue;
318
+ const h = parseHunkHeader(line);
319
+ if (!h)
320
+ continue;
321
+ cur.hunks.push({ start: h.newStart, len: h.newLen });
322
+ oldLeft = h.oldLen;
323
+ newLeft = h.newLen;
324
+ newLine = h.newStart;
325
+ }
326
+ }
327
+ return files;
328
+ }
329
+ /** The words and identifiers the diff adds: identifiers (camelCase, snake_case, with digits) first, then by how often they appear. */
330
+ export function trailDiffTerms(added, max) {
331
+ const count = new Map();
332
+ const ident = new Set();
333
+ const first = new Map();
334
+ for (const line of added) {
335
+ for (const w of rawWords(line)) {
336
+ const isIdent = splitIdentifier(w).length > 1 || /[0-9]/.test(w);
337
+ for (const t of trailTerms(w)) {
338
+ if (!first.has(t))
339
+ first.set(t, first.size);
340
+ count.set(t, (count.get(t) ?? 0) + 1);
341
+ if (isIdent)
342
+ ident.add(t);
343
+ }
344
+ }
345
+ }
346
+ const terms = [...count.keys()].sort((a, b) => {
347
+ if (ident.has(a) !== ident.has(b))
348
+ return ident.has(a) ? -1 : 1;
349
+ if (count.get(a) !== count.get(b))
350
+ return count.get(b) - count.get(a);
351
+ return first.get(a) - first.get(b);
352
+ });
353
+ return max > 0 && terms.length > max ? terms.slice(0, max) : terms;
354
+ }
355
+ /** The changed files: what the caller listed, then every file the diff names, deduplicated in that order. */
356
+ export function trailCheckFiles(listed, diffFiles) {
357
+ const seen = new Set();
358
+ const out = [];
359
+ for (const raw of [...listed, ...diffFiles.map((f) => f.path)]) {
360
+ const f = cleanRepoPath(raw);
361
+ if (f && !seen.has(f)) {
362
+ seen.add(f);
363
+ out.push(f);
364
+ }
365
+ }
366
+ return out;
367
+ }
368
+ /** What a candidate says, and what a quote must come from. */
369
+ function candidateText(c) {
370
+ if (c.kind === "note")
371
+ return `${c.note.title}\n${c.note.body}`;
372
+ let text = `${c.skill.description}\n${c.skill.body}`;
373
+ for (const t of c.skill.takeaways)
374
+ if (t.foldedIn === undefined)
375
+ text += `\n${t.text}`;
376
+ return text;
377
+ }
378
+ function label(c) {
379
+ return c.kind === "note" ? c.note.title : `skill ${c.skill.name}`;
380
+ }
381
+ /** How many of the terms the text carries, each counted once. */
382
+ function termHits(text, terms) {
383
+ const set = normTermSet(text);
384
+ const seen = new Set();
385
+ for (const t of terms) {
386
+ const k = normTerm(t);
387
+ if (set.has(k))
388
+ seen.add(k);
389
+ }
390
+ return seen.size;
391
+ }
392
+ /**
393
+ * Stands in for Postgres' ts_rank, which only the server has: more shared
394
+ * terms rank higher, and it stays under 0.8 so wording never outranks a note
395
+ * that touched a changed file.
396
+ */
397
+ function localTextScore(hits) {
398
+ return hits / (hits + 8);
399
+ }
400
+ /** Whether any touch is one of `files` (exact) or lies in one of `dirs`. */
401
+ function touchMatch(touches, files, dirs) {
402
+ const fileSet = new Set(files);
403
+ const dirSet = new Set(dirs);
404
+ let exact = false;
405
+ let dir = false;
406
+ for (const t of touches) {
407
+ const f = touchFile(t);
408
+ if (fileSet.has(f))
409
+ exact = true;
410
+ const d = repoDir(f);
411
+ if (d && dirSet.has(d))
412
+ dir = true;
413
+ }
414
+ return { exact, dir };
415
+ }
416
+ /** A note's YYYY-MM-DD date as UTC midnight, or null when it has none. */
417
+ function noteDay(date) {
418
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date))
419
+ return null;
420
+ const d = new Date(`${date}T00:00:00Z`);
421
+ return Number.isNaN(d.getTime()) || d.toISOString().slice(0, 10) !== date ? null : d;
422
+ }
423
+ function noteScore(note, textScore, exact, dir, now) {
424
+ let score = textScore;
425
+ if (exact)
426
+ score += SCORE_FILE_EXACT;
427
+ else if (dir)
428
+ score += SCORE_FILE_DIR;
429
+ const day = noteDay(note.date);
430
+ if (day) {
431
+ const age = Math.max(0, (now.getTime() - day.getTime()) / 86_400_000);
432
+ score += SCORE_RECENCY * Math.exp(-age / RECENCY_DAYS);
433
+ }
434
+ return score;
435
+ }
436
+ function noteSearchText(n) {
437
+ return `${n.title}\n${n.body}\n${n.touches.join(" ")}`;
438
+ }
439
+ /**
440
+ * What the server's note search would hand the selection: notes that share a
441
+ * word with the diff or touch a changed file or folder, best text match
442
+ * first, newest next, at most 100.
443
+ */
444
+ function notePool(notes, terms, files, dirs) {
445
+ const pool = [];
446
+ for (const note of notes) {
447
+ const hits = termHits(noteSearchText(note), terms);
448
+ const m = touchMatch(note.touches, files, dirs);
449
+ if (hits > 0 || m.exact || m.dir)
450
+ pool.push({ note, textScore: localTextScore(hits) });
451
+ }
452
+ pool.sort((a, b) => {
453
+ if (a.textScore !== b.textScore)
454
+ return b.textScore - a.textScore;
455
+ const ad = noteDay(a.note.date)?.getTime() ?? -Infinity;
456
+ const bd = noteDay(b.note.date)?.getTime() ?? -Infinity;
457
+ if (ad !== bd)
458
+ return bd - ad;
459
+ return a.note.path < b.note.path ? -1 : a.note.path > b.note.path ? 1 : 0;
460
+ });
461
+ return pool.slice(0, NOTE_POOL);
462
+ }
463
+ /**
464
+ * At most 25 pieces of knowledge for the changed files: skills that name a
465
+ * changed path or share the diff's words (at most 6), then notes that touched
466
+ * these files or their folders, or share the diff's words, best first. Notes
467
+ * come first in the returned order. Exported for tests.
468
+ */
469
+ export function selectTrailCheckCandidates(files, diffTerms, notes, skills, now) {
470
+ const { files: fileList, dirs: dirList } = filesAndDirs(files);
471
+ const sp = [];
472
+ for (const { skill, textScore } of skills) {
473
+ const text = candidateText({ kind: "skill", skill });
474
+ const mention = fileList.some((f) => f !== "" && text.includes(f));
475
+ if (!mention && (textScore <= 0 || termHits(`${skill.name}\n${text}`, diffTerms) < MIN_TERM_HITS))
476
+ continue;
477
+ sp.push({ skill, mention, score: textScore });
478
+ }
479
+ sp.sort((a, b) => (a.mention !== b.mention ? (a.mention ? -1 : 1) : b.score - a.score));
480
+ sp.splice(MAX_SKILLS);
481
+ const np = [];
482
+ for (const { note, textScore } of notes) {
483
+ const { exact, dir } = touchMatch(note.touches, fileList, dirList);
484
+ if (!exact && !dir && termHits(noteSearchText(note), diffTerms) < MIN_TERM_HITS)
485
+ continue;
486
+ np.push({ note, score: noteScore(note, textScore, exact, dir, now) });
487
+ }
488
+ np.sort((a, b) => b.score - a.score);
489
+ np.splice(MAX_CANDIDATES - sp.length);
490
+ return [
491
+ ...np.map((p) => ({ kind: "note", note: p.note })),
492
+ ...sp.map((p) => ({ kind: "skill", skill: p.skill })),
493
+ ];
494
+ }
495
+ /** A finding's source, built only from the candidate. The quote is kept only if it really is in the candidate's text. */
496
+ function candidateSource(c, quote) {
497
+ const src = { kind: c.kind };
498
+ const q = quote.trim();
499
+ if (q && normQuote(candidateText(c)).includes(normQuote(q)))
500
+ src.quote = words(q).join(" ");
501
+ if (c.kind === "note") {
502
+ if (c.note.title)
503
+ src.title = c.note.title;
504
+ if (c.note.author)
505
+ src.author = c.note.author;
506
+ if (noteDay(c.note.date))
507
+ src.date = c.note.date;
508
+ if (c.note.path)
509
+ src.path = c.note.path;
510
+ }
511
+ else {
512
+ if (c.skill.name)
513
+ src.name = c.skill.name;
514
+ if (c.skill.version)
515
+ src.version = c.skill.version;
516
+ }
517
+ return src;
518
+ }
519
+ function noteSection(heading) {
520
+ const h = heading.toLowerCase();
521
+ const has = (...xs) => xs.some((x) => h.includes(x));
522
+ if (has("ruled out", "tried", "rejected", "dead end", "didn't work", "did not work"))
523
+ return "ruledOut";
524
+ if (has("watch out", "gotcha", "careful", "pitfall", "warning"))
525
+ return "watch";
526
+ if (has("decided", "decision"))
527
+ return "decided";
528
+ return "other";
529
+ }
530
+ const LIST_MARKER = /^(?:[-*+•>]+|\d+[.)])[\t\n\f\r ]+/;
531
+ /**
532
+ * A line of prose as sentences, without bullet or numbering markers. Each is
533
+ * a verbatim run of the line, so it can be quoted back as written. Exported
534
+ * for tests.
535
+ */
536
+ export function trailSentences(raw) {
537
+ let line = raw.trim();
538
+ for (let m = LIST_MARKER.exec(line); m; m = LIST_MARKER.exec(line))
539
+ line = line.slice(m[0].length).trim();
540
+ if (!line)
541
+ return [];
542
+ const out = [];
543
+ let start = 0;
544
+ for (let i = 0; i < line.length - 1; i++) {
545
+ const c = line[i];
546
+ if ((c === "." || c === "!" || c === "?") && line[i + 1] === " " && i + 2 < line.length && /[A-Z0-9]/.test(line[i + 2])) {
547
+ const s = line.slice(start, i + 1).trim();
548
+ if (s)
549
+ out.push(s);
550
+ start = i + 2;
551
+ }
552
+ }
553
+ const s = line.slice(start).trim();
554
+ if (s)
555
+ out.push(s);
556
+ return out;
557
+ }
558
+ /** Marks a sentence of a skill's body as an instruction worth holding a diff against; the rest is explanation. */
559
+ const DIRECTIVE = /\b(never|always|don't|do not|must|avoid|only|instead)\b/i;
560
+ /** A candidate's knowledge, sentence by sentence. */
561
+ function statementsOf(c) {
562
+ const out = [];
563
+ if (c.kind === "note") {
564
+ let kind = "other";
565
+ for (const line of c.note.body.split("\n")) {
566
+ const t = line.trim();
567
+ if (t.startsWith("#")) {
568
+ kind = noteSection(t.replace(/^[# ]+/, ""));
569
+ continue;
570
+ }
571
+ for (const s of trailSentences(t))
572
+ out.push({ kind, text: s });
573
+ }
574
+ return out;
575
+ }
576
+ for (const t of c.skill.takeaways) {
577
+ if (t.foldedIn !== undefined)
578
+ continue;
579
+ for (const line of t.text.split("\n"))
580
+ for (const s of trailSentences(line))
581
+ out.push({ kind: "takeaway", text: s });
582
+ }
583
+ for (const line of c.skill.body.split("\n")) {
584
+ if (line.trim().startsWith("#"))
585
+ continue;
586
+ for (const s of trailSentences(line))
587
+ if (DIRECTIVE.test(s))
588
+ out.push({ kind: "takeaway", text: s });
589
+ }
590
+ return out;
591
+ }
592
+ /** Whose knowledge a candidate is, for a finding's summary. */
593
+ function owner(c) {
594
+ if (c.kind === "skill")
595
+ return `skill ${c.skill.name}`;
596
+ const a = c.note.author.trim();
597
+ return a ? `${a}'s learning` : "a team learning";
598
+ }
599
+ /**
600
+ * Whether the candidate bears on a changed file: a note that touched the file
601
+ * or its folder, a skill that names either, or either sharing two terms with
602
+ * what the diff adds to it.
603
+ */
604
+ function relevantTo(c, f, addedTerms) {
605
+ const dir = repoDir(f.path);
606
+ if (c.kind === "note") {
607
+ const m = touchMatch(c.note.touches, [f.path], dir ? [dir] : []);
608
+ if (m.exact || m.dir)
609
+ return true;
610
+ }
611
+ else {
612
+ const text = candidateText(c);
613
+ if (text.includes(f.path) || (dir && text.includes(`${dir}/`)))
614
+ return true;
615
+ }
616
+ return termHits(`${label(c)}\n${candidateText(c)}`, addedTerms) >= MIN_TERM_HITS;
617
+ }
618
+ function changeBlocks(f) {
619
+ const out = [];
620
+ let open = null;
621
+ for (const l of f.lines) {
622
+ if (l.op === " ") {
623
+ open = null;
624
+ continue;
625
+ }
626
+ if (!open) {
627
+ open = { removed: [], added: [] };
628
+ out.push(open);
629
+ }
630
+ if (l.op === "-")
631
+ open.removed.push(l.text);
632
+ else
633
+ open.added.push(l);
634
+ }
635
+ return out;
636
+ }
637
+ const NUMBER = /\d+(?:\.\d+)?/g;
638
+ const IDENTIFIER = /[A-Za-z_][A-Za-z0-9_]*/g;
639
+ const WORD_NUMBERS = new Map(Object.entries({
640
+ zero: 0, one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8,
641
+ nine: 9, ten: 10, eleven: 11, twelve: 12, fifteen: 15, twenty: 20, thirty: 30,
642
+ once: 1, twice: 2, first: 1, second: 2, third: 3, fourth: 4, fifth: 5, sixth: 6,
643
+ seventh: 7, eighth: 8, ninth: 9, tenth: 10, single: 1, double: 2, triple: 3,
644
+ }));
645
+ /** Name parts that say nothing about which value a line sets: a sentence sharing only "max" with `maxRetries` is not about retries. */
646
+ const GENERIC_NAME_PARTS = new Set(["max", "min", "num", "cnt", "count", "default", "val", "value", "tmp", "total", "limit", "new", "old"]);
647
+ /** Words that frame a "tried and ruled out" sentence rather than name the approach: the code that brings it back never has them. */
648
+ const FRAMING_WORDS = new Set([
649
+ "tried", "ruled", "instead", "because", "around", "didn", "doesn", "isn", "wasn", "worked", "approach", "turned",
650
+ "ended", "turns", "slow", "slower", "broke", "breaks",
651
+ ]);
652
+ /** Every number a sentence states, in digits (`60s`, `4th`) or words (`three`, `fourth`). Exported for tests. */
653
+ export function trailNumbers(text) {
654
+ const out = new Set();
655
+ for (const m of text.match(NUMBER) ?? [])
656
+ out.add(Number(m));
657
+ for (const w of rawWords(text.toLowerCase())) {
658
+ const v = WORD_NUMBERS.get(w);
659
+ if (v !== undefined)
660
+ out.add(v);
661
+ }
662
+ return out;
663
+ }
664
+ /**
665
+ * The value a decision records is the one its head states, before the
666
+ * reason: "Keep defaultRetryMax = 4: ten retries can hold one call for an
667
+ * hour" decided 4, not 10. A head with no number falls back to the sentence.
668
+ */
669
+ function decidedNumbers(text) {
670
+ const head = trailNumbers(approachHead(text));
671
+ return head.size ? head : trailNumbers(text);
672
+ }
673
+ /** The numbers a code line carries outside its identifiers (`retries2` gives no 2), in order. Exported for tests. */
674
+ export function trailLineNumbers(line) {
675
+ return line.replace(IDENTIFIER, " ").match(NUMBER) ?? [];
676
+ }
677
+ /** The first of `a` that `b` does not carry. */
678
+ function firstMissing(a, b) {
679
+ const have = new Set(b);
680
+ return a.find((x) => !have.has(x)) ?? "";
681
+ }
682
+ /** Pairs each added line with a removed line of the same block that shares an identifier and differs from it in a number. */
683
+ function valueChanges(b) {
684
+ const out = [];
685
+ for (const a of b.added) {
686
+ const aIds = new Set(a.text.match(IDENTIFIER) ?? []);
687
+ const aNums = trailLineNumbers(a.text);
688
+ for (const r of b.removed) {
689
+ const name = (r.match(IDENTIFIER) ?? []).find((id) => aIds.has(id) && nameTerms(id).size > 0);
690
+ if (!name)
691
+ continue;
692
+ const rNums = trailLineNumbers(r);
693
+ const from = firstMissing(rNums, aNums);
694
+ const to = firstMissing(aNums, rNums);
695
+ if (!from || !to)
696
+ continue;
697
+ out.push({ line: a.new, name, from: Number(from), to: Number(to), fromText: from, toText: to });
698
+ break;
699
+ }
700
+ }
701
+ return out;
702
+ }
703
+ /** The normalised parts of an identifier that name a thing: `maxRetries` → {maxretry, retry}. */
704
+ function nameTerms(name) {
705
+ return new Set(trailTerms(name)
706
+ .filter((t) => !GENERIC_NAME_PARTS.has(t))
707
+ .map(normTerm));
708
+ }
709
+ /** Whether a sentence is about the identifier. */
710
+ function mentionsName(sentence, name) {
711
+ const set = normTermSet(sentence);
712
+ return [...nameTerms(name)].some((t) => set.has(t));
713
+ }
714
+ /** A sentence's terms without framing words, normalised. */
715
+ function distinctTerms(text) {
716
+ return [...new Set(trailTerms(text).filter((t) => !FRAMING_WORDS.has(t)).map(normTerm))];
717
+ }
718
+ /**
719
+ * The part of a sentence that names the approach, before the reason it was
720
+ * dropped: "A global mutex around the cache: p99 tripled" → "A global mutex
721
+ * around the cache". The reason's words are not in the code that brings it back.
722
+ */
723
+ function approachHead(sentence) {
724
+ let cut = sentence.length;
725
+ // ASCII-only lowering keeps the indexes in step with the sentence.
726
+ const lower = sentence.replace(/[A-Z]/g, (ch) => ch.toLowerCase());
727
+ for (const sep of [":", ";", " — ", " – ", " - ", " because ", " but ", ", which ", " (", " since "]) {
728
+ const i = lower.indexOf(sep);
729
+ if (i > 0 && i < cut)
730
+ cut = i;
731
+ }
732
+ return sentence.slice(0, cut);
733
+ }
734
+ /** The statement's terms the added lines carry, when enough do to say the code is about the same thing: two of a short statement, three of a longer one. */
735
+ function wordsMatch(statement, added) {
736
+ const shared = statement.filter((t) => added.has(t));
737
+ return { shared, ok: shared.length >= (statement.length <= 4 ? 2 : 3) };
738
+ }
739
+ /** The first added line carrying one of the terms, 0 for none. */
740
+ function firstLineWith(lines, terms) {
741
+ for (const l of lines) {
742
+ const set = normTermSet(l.text);
743
+ if (terms.some((t) => set.has(t)))
744
+ return l.new;
745
+ }
746
+ return 0;
747
+ }
748
+ /**
749
+ * Holds the diff against the candidates with the rules above. One finding per
750
+ * (file, candidate), a conflict winning over a follow; conflicts first, then
751
+ * by file and line; at most 20. Exported for tests.
752
+ */
753
+ export function judgeTrailCheck(candidates, diffFiles) {
754
+ const best = new Map();
755
+ const put = (file, ci, f) => {
756
+ const k = `${file}\0${ci}`;
757
+ const cur = best.get(k);
758
+ if (!cur || (cur.verdict !== "conflict" && f.verdict === "conflict"))
759
+ best.set(k, f);
760
+ };
761
+ const statements = candidates.map(statementsOf);
762
+ for (const f of diffFiles) {
763
+ const addedTerms = trailDiffTerms(addedText(f), DIFF_TERMS);
764
+ const blocks = changeBlocks(f);
765
+ candidates.forEach((c, ci) => {
766
+ const stmts = statements[ci];
767
+ if (stmts.length === 0 || !relevantTo(c, f, addedTerms))
768
+ return;
769
+ const finding = (verdict, summary, line, quote) => {
770
+ const out = {
771
+ file: f.path,
772
+ verdict,
773
+ summary: capWords(summary, SUMMARY_WORDS),
774
+ source: candidateSource(c, capWords(quote, QUOTE_WORDS)),
775
+ };
776
+ if (verdict === "conflict" && line > 0)
777
+ out.line = line;
778
+ return out;
779
+ };
780
+ for (const b of blocks) {
781
+ // A value changed against a number the team recorded.
782
+ for (const vc of valueChanges(b)) {
783
+ for (const st of stmts) {
784
+ if (st.kind === "other" || !mentionsName(st.text, vc.name))
785
+ continue;
786
+ if (st.kind === "watch" || st.kind === "ruledOut") {
787
+ if (trailNumbers(st.text).has(vc.to)) {
788
+ const what = st.kind === "ruledOut" ? "ruled out" : "warned about";
789
+ put(f.path, ci, finding("conflict", `sets ${vc.name} to ${vc.toText}, which ${owner(c)} ${what}`, vc.line, st.text));
790
+ }
791
+ continue;
792
+ }
793
+ const nums = decidedNumbers(st.text);
794
+ if (nums.has(vc.from) && !nums.has(vc.to)) {
795
+ put(f.path, ci, finding("conflict", `changes ${vc.name} from ${vc.fromText} to ${vc.toText}`, vc.line, st.text));
796
+ }
797
+ else if (nums.has(vc.to)) {
798
+ put(f.path, ci, finding("follows", `sets ${vc.name} to ${vc.toText}, as ${owner(c)} says`, vc.line, st.text));
799
+ }
800
+ }
801
+ }
802
+ const added = normTermSet(b.added.map((l) => `${l.text}\n`).join(""));
803
+ for (const st of stmts) {
804
+ if (st.kind === "ruledOut") {
805
+ // An approach the team ruled out, back in the added lines.
806
+ const m = wordsMatch(distinctTerms(approachHead(st.text)), added);
807
+ if (m.ok) {
808
+ put(f.path, ci, finding("conflict", `brings back ${m.shared.slice(0, 3).join(" ")}, which ${owner(c)} ruled out`, firstLineWith(b.added, m.shared), st.text));
809
+ }
810
+ }
811
+ else if (st.kind === "decided" || st.kind === "takeaway") {
812
+ if (wordsMatch(distinctTerms(st.text), added).ok) {
813
+ put(f.path, ci, finding("follows", `follows ${owner(c)}: ${st.text.replace(/\.+$/, "")}`, 0, st.text));
814
+ }
815
+ }
816
+ }
817
+ }
818
+ });
819
+ }
820
+ const out = [...best.values()];
821
+ out.sort((a, b) => {
822
+ if ((a.verdict === "conflict") !== (b.verdict === "conflict"))
823
+ return a.verdict === "conflict" ? -1 : 1;
824
+ if (a.file !== b.file)
825
+ return a.file < b.file ? -1 : 1;
826
+ return (a.line ?? 0) - (b.line ?? 0);
827
+ });
828
+ return out.slice(0, MAX_FINDINGS);
829
+ }
830
+ //# sourceMappingURL=local-check.js.map