@coldtea/abide 0.0.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 (370) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -0
  3. package/dist/abide-hook.d.ts +2 -0
  4. package/dist/abide-hook.d.ts.map +1 -0
  5. package/dist/abide-hook.js +23 -0
  6. package/dist/abide-hook.js.map +1 -0
  7. package/dist/bin.d.ts +3 -0
  8. package/dist/bin.d.ts.map +1 -0
  9. package/dist/bin.js +64 -0
  10. package/dist/bin.js.map +1 -0
  11. package/dist/commands/audit.d.ts +3 -0
  12. package/dist/commands/audit.d.ts.map +1 -0
  13. package/dist/commands/audit.js +79 -0
  14. package/dist/commands/audit.js.map +1 -0
  15. package/dist/commands/bench.d.ts +3 -0
  16. package/dist/commands/bench.d.ts.map +1 -0
  17. package/dist/commands/bench.js +206 -0
  18. package/dist/commands/bench.js.map +1 -0
  19. package/dist/commands/calibrate.d.ts +3 -0
  20. package/dist/commands/calibrate.d.ts.map +1 -0
  21. package/dist/commands/calibrate.js +159 -0
  22. package/dist/commands/calibrate.js.map +1 -0
  23. package/dist/commands/check.d.ts +3 -0
  24. package/dist/commands/check.d.ts.map +1 -0
  25. package/dist/commands/check.js +106 -0
  26. package/dist/commands/check.js.map +1 -0
  27. package/dist/commands/compile.d.ts +3 -0
  28. package/dist/commands/compile.d.ts.map +1 -0
  29. package/dist/commands/compile.js +116 -0
  30. package/dist/commands/compile.js.map +1 -0
  31. package/dist/commands/init.d.ts +4 -0
  32. package/dist/commands/init.d.ts.map +1 -0
  33. package/dist/commands/init.js +93 -0
  34. package/dist/commands/init.js.map +1 -0
  35. package/dist/commands/login.d.ts +3 -0
  36. package/dist/commands/login.d.ts.map +1 -0
  37. package/dist/commands/login.js +58 -0
  38. package/dist/commands/login.js.map +1 -0
  39. package/dist/commands/replay.d.ts +4 -0
  40. package/dist/commands/replay.d.ts.map +1 -0
  41. package/dist/commands/replay.js +118 -0
  42. package/dist/commands/replay.js.map +1 -0
  43. package/dist/commands/report.d.ts +5 -0
  44. package/dist/commands/report.d.ts.map +1 -0
  45. package/dist/commands/report.js +78 -0
  46. package/dist/commands/report.js.map +1 -0
  47. package/dist/commands/rubric.d.ts +3 -0
  48. package/dist/commands/rubric.d.ts.map +1 -0
  49. package/dist/commands/rubric.js +40 -0
  50. package/dist/commands/rubric.js.map +1 -0
  51. package/dist/commands/uninstall.d.ts +2 -0
  52. package/dist/commands/uninstall.d.ts.map +1 -0
  53. package/dist/commands/uninstall.js +31 -0
  54. package/dist/commands/uninstall.js.map +1 -0
  55. package/dist/hooks/postToolUse.d.ts +3 -0
  56. package/dist/hooks/postToolUse.d.ts.map +1 -0
  57. package/dist/hooks/postToolUse.js +145 -0
  58. package/dist/hooks/postToolUse.js.map +1 -0
  59. package/dist/hooks/sessionStart.d.ts +11 -0
  60. package/dist/hooks/sessionStart.d.ts.map +1 -0
  61. package/dist/hooks/sessionStart.js +83 -0
  62. package/dist/hooks/sessionStart.js.map +1 -0
  63. package/dist/hooks/stop.d.ts +26 -0
  64. package/dist/hooks/stop.d.ts.map +1 -0
  65. package/dist/hooks/stop.js +227 -0
  66. package/dist/hooks/stop.js.map +1 -0
  67. package/dist/hooks/turnStart.d.ts +8 -0
  68. package/dist/hooks/turnStart.d.ts.map +1 -0
  69. package/dist/hooks/turnStart.js +40 -0
  70. package/dist/hooks/turnStart.js.map +1 -0
  71. package/dist/lib/applyPatch.d.ts +22 -0
  72. package/dist/lib/applyPatch.d.ts.map +1 -0
  73. package/dist/lib/applyPatch.js +63 -0
  74. package/dist/lib/applyPatch.js.map +1 -0
  75. package/dist/lib/audit.d.ts +40 -0
  76. package/dist/lib/audit.d.ts.map +1 -0
  77. package/dist/lib/audit.js +215 -0
  78. package/dist/lib/audit.js.map +1 -0
  79. package/dist/lib/band.d.ts +20 -0
  80. package/dist/lib/band.d.ts.map +1 -0
  81. package/dist/lib/band.js +40 -0
  82. package/dist/lib/band.js.map +1 -0
  83. package/dist/lib/calibration.d.ts +13 -0
  84. package/dist/lib/calibration.d.ts.map +1 -0
  85. package/dist/lib/calibration.js +25 -0
  86. package/dist/lib/calibration.js.map +1 -0
  87. package/dist/lib/checkRunner.d.ts +34 -0
  88. package/dist/lib/checkRunner.d.ts.map +1 -0
  89. package/dist/lib/checkRunner.js +76 -0
  90. package/dist/lib/checkRunner.js.map +1 -0
  91. package/dist/lib/claude.d.ts +4 -0
  92. package/dist/lib/claude.d.ts.map +1 -0
  93. package/dist/lib/claude.js +28 -0
  94. package/dist/lib/claude.js.map +1 -0
  95. package/dist/lib/compilePrompt.d.ts +22 -0
  96. package/dist/lib/compilePrompt.d.ts.map +1 -0
  97. package/dist/lib/compilePrompt.js +33 -0
  98. package/dist/lib/compilePrompt.js.map +1 -0
  99. package/dist/lib/constants.d.ts +33 -0
  100. package/dist/lib/constants.d.ts.map +1 -0
  101. package/dist/lib/constants.js +33 -0
  102. package/dist/lib/constants.js.map +1 -0
  103. package/dist/lib/coverage.d.ts +7 -0
  104. package/dist/lib/coverage.d.ts.map +1 -0
  105. package/dist/lib/coverage.js +17 -0
  106. package/dist/lib/coverage.js.map +1 -0
  107. package/dist/lib/credentials.d.ts +27 -0
  108. package/dist/lib/credentials.d.ts.map +1 -0
  109. package/dist/lib/credentials.js +89 -0
  110. package/dist/lib/credentials.js.map +1 -0
  111. package/dist/lib/diff.d.ts +30 -0
  112. package/dist/lib/diff.d.ts.map +1 -0
  113. package/dist/lib/diff.js +139 -0
  114. package/dist/lib/diff.js.map +1 -0
  115. package/dist/lib/events.d.ts +5 -0
  116. package/dist/lib/events.d.ts.map +1 -0
  117. package/dist/lib/events.js +38 -0
  118. package/dist/lib/events.js.map +1 -0
  119. package/dist/lib/git.d.ts +37 -0
  120. package/dist/lib/git.d.ts.map +1 -0
  121. package/dist/lib/git.js +163 -0
  122. package/dist/lib/git.js.map +1 -0
  123. package/dist/lib/hookRunner.d.ts +10 -0
  124. package/dist/lib/hookRunner.d.ts.map +1 -0
  125. package/dist/lib/hookRunner.js +52 -0
  126. package/dist/lib/hookRunner.js.map +1 -0
  127. package/dist/lib/hosts.d.ts +20 -0
  128. package/dist/lib/hosts.d.ts.map +1 -0
  129. package/dist/lib/hosts.js +101 -0
  130. package/dist/lib/hosts.js.map +1 -0
  131. package/dist/lib/jev.d.ts +24 -0
  132. package/dist/lib/jev.d.ts.map +1 -0
  133. package/dist/lib/jev.js +169 -0
  134. package/dist/lib/jev.js.map +1 -0
  135. package/dist/lib/lintConfig.d.ts +3 -0
  136. package/dist/lib/lintConfig.d.ts.map +1 -0
  137. package/dist/lib/lintConfig.js +55 -0
  138. package/dist/lib/lintConfig.js.map +1 -0
  139. package/dist/lib/loadRules.d.ts +12 -0
  140. package/dist/lib/loadRules.d.ts.map +1 -0
  141. package/dist/lib/loadRules.js +31 -0
  142. package/dist/lib/loadRules.js.map +1 -0
  143. package/dist/lib/opencodePlugin.d.ts +10 -0
  144. package/dist/lib/opencodePlugin.d.ts.map +1 -0
  145. package/dist/lib/opencodePlugin.js +36 -0
  146. package/dist/lib/opencodePlugin.js.map +1 -0
  147. package/dist/lib/output.d.ts +6 -0
  148. package/dist/lib/output.d.ts.map +1 -0
  149. package/dist/lib/output.js +35 -0
  150. package/dist/lib/output.js.map +1 -0
  151. package/dist/lib/packageRoot.d.ts +12 -0
  152. package/dist/lib/packageRoot.d.ts.map +1 -0
  153. package/dist/lib/packageRoot.js +50 -0
  154. package/dist/lib/packageRoot.js.map +1 -0
  155. package/dist/lib/paths.d.ts +24 -0
  156. package/dist/lib/paths.d.ts.map +1 -0
  157. package/dist/lib/paths.js +75 -0
  158. package/dist/lib/paths.js.map +1 -0
  159. package/dist/lib/reason.d.ts +11 -0
  160. package/dist/lib/reason.d.ts.map +1 -0
  161. package/dist/lib/reason.js +31 -0
  162. package/dist/lib/reason.js.map +1 -0
  163. package/dist/lib/regularFile.d.ts +26 -0
  164. package/dist/lib/regularFile.d.ts.map +1 -0
  165. package/dist/lib/regularFile.js +77 -0
  166. package/dist/lib/regularFile.js.map +1 -0
  167. package/dist/lib/replay.d.ts +74 -0
  168. package/dist/lib/replay.d.ts.map +1 -0
  169. package/dist/lib/replay.js +250 -0
  170. package/dist/lib/replay.js.map +1 -0
  171. package/dist/lib/replayCodex.d.ts +6 -0
  172. package/dist/lib/replayCodex.d.ts.map +1 -0
  173. package/dist/lib/replayCodex.js +132 -0
  174. package/dist/lib/replayCodex.js.map +1 -0
  175. package/dist/lib/replayOpencode.d.ts +15 -0
  176. package/dist/lib/replayOpencode.d.ts.map +1 -0
  177. package/dist/lib/replayOpencode.js +154 -0
  178. package/dist/lib/replayOpencode.js.map +1 -0
  179. package/dist/lib/rubricFile.d.ts +31 -0
  180. package/dist/lib/rubricFile.d.ts.map +1 -0
  181. package/dist/lib/rubricFile.js +75 -0
  182. package/dist/lib/rubricFile.js.map +1 -0
  183. package/dist/lib/scope.d.ts +4 -0
  184. package/dist/lib/scope.d.ts.map +1 -0
  185. package/dist/lib/scope.js +14 -0
  186. package/dist/lib/scope.js.map +1 -0
  187. package/dist/lib/session.d.ts +48 -0
  188. package/dist/lib/session.d.ts.map +1 -0
  189. package/dist/lib/session.js +182 -0
  190. package/dist/lib/session.js.map +1 -0
  191. package/dist/lib/settings.d.ts +26 -0
  192. package/dist/lib/settings.d.ts.map +1 -0
  193. package/dist/lib/settings.js +104 -0
  194. package/dist/lib/settings.js.map +1 -0
  195. package/dist/lib/sources.d.ts +28 -0
  196. package/dist/lib/sources.d.ts.map +1 -0
  197. package/dist/lib/sources.js +137 -0
  198. package/dist/lib/sources.js.map +1 -0
  199. package/dist/lib/stdin.d.ts +3 -0
  200. package/dist/lib/stdin.d.ts.map +1 -0
  201. package/dist/lib/stdin.js +24 -0
  202. package/dist/lib/stdin.js.map +1 -0
  203. package/dist/lib/transcript.d.ts +3 -0
  204. package/dist/lib/transcript.d.ts.map +1 -0
  205. package/dist/lib/transcript.js +57 -0
  206. package/dist/lib/transcript.js.map +1 -0
  207. package/dist/lib/ui.d.ts +8 -0
  208. package/dist/lib/ui.d.ts.map +1 -0
  209. package/dist/lib/ui.js +34 -0
  210. package/dist/lib/ui.js.map +1 -0
  211. package/dist/ui/components/Buckets.d.ts +6 -0
  212. package/dist/ui/components/Buckets.d.ts.map +1 -0
  213. package/dist/ui/components/Buckets.js +11 -0
  214. package/dist/ui/components/Buckets.js.map +1 -0
  215. package/dist/ui/components/Callout.d.ts +11 -0
  216. package/dist/ui/components/Callout.d.ts.map +1 -0
  217. package/dist/ui/components/Callout.js +10 -0
  218. package/dist/ui/components/Callout.js.map +1 -0
  219. package/dist/ui/components/Checklist.d.ts +10 -0
  220. package/dist/ui/components/Checklist.d.ts.map +1 -0
  221. package/dist/ui/components/Checklist.js +8 -0
  222. package/dist/ui/components/Checklist.js.map +1 -0
  223. package/dist/ui/components/Columns.d.ts +28 -0
  224. package/dist/ui/components/Columns.d.ts.map +1 -0
  225. package/dist/ui/components/Columns.js +20 -0
  226. package/dist/ui/components/Columns.js.map +1 -0
  227. package/dist/ui/components/Header.d.ts +9 -0
  228. package/dist/ui/components/Header.d.ts.map +1 -0
  229. package/dist/ui/components/Header.js +7 -0
  230. package/dist/ui/components/Header.js.map +1 -0
  231. package/dist/ui/components/Live.d.ts +16 -0
  232. package/dist/ui/components/Live.d.ts.map +1 -0
  233. package/dist/ui/components/Live.js +40 -0
  234. package/dist/ui/components/Live.js.map +1 -0
  235. package/dist/ui/components/RuleTable.d.ts +15 -0
  236. package/dist/ui/components/RuleTable.d.ts.map +1 -0
  237. package/dist/ui/components/RuleTable.js +81 -0
  238. package/dist/ui/components/RuleTable.js.map +1 -0
  239. package/dist/ui/components/Section.d.ts +10 -0
  240. package/dist/ui/components/Section.d.ts.map +1 -0
  241. package/dist/ui/components/Section.js +8 -0
  242. package/dist/ui/components/Section.js.map +1 -0
  243. package/dist/ui/components/Verdicts.d.ts +9 -0
  244. package/dist/ui/components/Verdicts.d.ts.map +1 -0
  245. package/dist/ui/components/Verdicts.js +12 -0
  246. package/dist/ui/components/Verdicts.js.map +1 -0
  247. package/dist/ui/render.d.ts +20 -0
  248. package/dist/ui/render.d.ts.map +1 -0
  249. package/dist/ui/render.js +32 -0
  250. package/dist/ui/render.js.map +1 -0
  251. package/dist/ui/theme.d.ts +37 -0
  252. package/dist/ui/theme.d.ts.map +1 -0
  253. package/dist/ui/theme.js +63 -0
  254. package/dist/ui/theme.js.map +1 -0
  255. package/dist/ui/views/AuditView.d.ts +18 -0
  256. package/dist/ui/views/AuditView.d.ts.map +1 -0
  257. package/dist/ui/views/AuditView.js +48 -0
  258. package/dist/ui/views/AuditView.js.map +1 -0
  259. package/dist/ui/views/BenchView.d.ts +33 -0
  260. package/dist/ui/views/BenchView.d.ts.map +1 -0
  261. package/dist/ui/views/BenchView.js +19 -0
  262. package/dist/ui/views/BenchView.js.map +1 -0
  263. package/dist/ui/views/CalibrateView.d.ts +26 -0
  264. package/dist/ui/views/CalibrateView.d.ts.map +1 -0
  265. package/dist/ui/views/CalibrateView.js +42 -0
  266. package/dist/ui/views/CalibrateView.js.map +1 -0
  267. package/dist/ui/views/CheckView.d.ts +19 -0
  268. package/dist/ui/views/CheckView.d.ts.map +1 -0
  269. package/dist/ui/views/CheckView.js +15 -0
  270. package/dist/ui/views/CheckView.js.map +1 -0
  271. package/dist/ui/views/ErrorView.d.ts +4 -0
  272. package/dist/ui/views/ErrorView.d.ts.map +1 -0
  273. package/dist/ui/views/ErrorView.js +26 -0
  274. package/dist/ui/views/ErrorView.js.map +1 -0
  275. package/dist/ui/views/HelpView.d.ts +2 -0
  276. package/dist/ui/views/HelpView.d.ts.map +1 -0
  277. package/dist/ui/views/HelpView.js +25 -0
  278. package/dist/ui/views/HelpView.js.map +1 -0
  279. package/dist/ui/views/InitView.d.ts +27 -0
  280. package/dist/ui/views/InitView.d.ts.map +1 -0
  281. package/dist/ui/views/InitView.js +24 -0
  282. package/dist/ui/views/InitView.js.map +1 -0
  283. package/dist/ui/views/ReplayView.d.ts +16 -0
  284. package/dist/ui/views/ReplayView.d.ts.map +1 -0
  285. package/dist/ui/views/ReplayView.js +44 -0
  286. package/dist/ui/views/ReplayView.js.map +1 -0
  287. package/dist/ui/views/ReportView.d.ts +14 -0
  288. package/dist/ui/views/ReportView.d.ts.map +1 -0
  289. package/dist/ui/views/ReportView.js +86 -0
  290. package/dist/ui/views/ReportView.js.map +1 -0
  291. package/dist/ui/views/RubricView.d.ts +18 -0
  292. package/dist/ui/views/RubricView.d.ts.map +1 -0
  293. package/dist/ui/views/RubricView.js +18 -0
  294. package/dist/ui/views/RubricView.js.map +1 -0
  295. package/opencode/abide.mjs +246 -0
  296. package/package.json +50 -0
  297. package/skills/abide-compile/SKILL.md +173 -0
  298. package/src/abide-hook.ts +23 -0
  299. package/src/bin.ts +64 -0
  300. package/src/commands/audit.ts +97 -0
  301. package/src/commands/bench.ts +226 -0
  302. package/src/commands/calibrate.ts +165 -0
  303. package/src/commands/check.ts +114 -0
  304. package/src/commands/compile.ts +127 -0
  305. package/src/commands/init.ts +109 -0
  306. package/src/commands/login.ts +61 -0
  307. package/src/commands/replay.ts +148 -0
  308. package/src/commands/report.ts +83 -0
  309. package/src/commands/rubric.ts +45 -0
  310. package/src/commands/uninstall.ts +33 -0
  311. package/src/hooks/postToolUse.ts +171 -0
  312. package/src/hooks/sessionStart.ts +100 -0
  313. package/src/hooks/stop.ts +275 -0
  314. package/src/hooks/turnStart.ts +36 -0
  315. package/src/lib/applyPatch.ts +75 -0
  316. package/src/lib/audit.ts +265 -0
  317. package/src/lib/band.ts +49 -0
  318. package/src/lib/calibration.ts +35 -0
  319. package/src/lib/checkRunner.ts +124 -0
  320. package/src/lib/claude.ts +35 -0
  321. package/src/lib/compilePrompt.ts +67 -0
  322. package/src/lib/constants.ts +39 -0
  323. package/src/lib/coverage.ts +21 -0
  324. package/src/lib/credentials.ts +104 -0
  325. package/src/lib/diff.ts +209 -0
  326. package/src/lib/events.ts +34 -0
  327. package/src/lib/git.ts +206 -0
  328. package/src/lib/hookRunner.ts +55 -0
  329. package/src/lib/hosts.ts +112 -0
  330. package/src/lib/jev.ts +223 -0
  331. package/src/lib/lintConfig.ts +53 -0
  332. package/src/lib/loadRules.ts +39 -0
  333. package/src/lib/opencodePlugin.ts +38 -0
  334. package/src/lib/output.ts +44 -0
  335. package/src/lib/packageRoot.ts +48 -0
  336. package/src/lib/paths.ts +96 -0
  337. package/src/lib/reason.ts +46 -0
  338. package/src/lib/regularFile.ts +96 -0
  339. package/src/lib/replay.ts +314 -0
  340. package/src/lib/replayCodex.ts +130 -0
  341. package/src/lib/replayOpencode.ts +186 -0
  342. package/src/lib/rubricFile.ts +99 -0
  343. package/src/lib/scope.ts +18 -0
  344. package/src/lib/session.ts +223 -0
  345. package/src/lib/settings.ts +112 -0
  346. package/src/lib/sources.ts +154 -0
  347. package/src/lib/stdin.ts +23 -0
  348. package/src/lib/transcript.ts +49 -0
  349. package/src/lib/ui.ts +38 -0
  350. package/src/ui/components/Buckets.tsx +30 -0
  351. package/src/ui/components/Callout.tsx +26 -0
  352. package/src/ui/components/Checklist.tsx +29 -0
  353. package/src/ui/components/Columns.tsx +62 -0
  354. package/src/ui/components/Header.tsx +35 -0
  355. package/src/ui/components/Live.tsx +73 -0
  356. package/src/ui/components/RuleTable.tsx +94 -0
  357. package/src/ui/components/Section.tsx +24 -0
  358. package/src/ui/components/Verdicts.tsx +40 -0
  359. package/src/ui/render.tsx +51 -0
  360. package/src/ui/theme.ts +65 -0
  361. package/src/ui/views/AuditView.tsx +140 -0
  362. package/src/ui/views/BenchView.tsx +87 -0
  363. package/src/ui/views/CalibrateView.tsx +108 -0
  364. package/src/ui/views/CheckView.tsx +56 -0
  365. package/src/ui/views/ErrorView.tsx +32 -0
  366. package/src/ui/views/HelpView.tsx +51 -0
  367. package/src/ui/views/InitView.tsx +103 -0
  368. package/src/ui/views/ReplayView.tsx +100 -0
  369. package/src/ui/views/ReportView.tsx +225 -0
  370. package/src/ui/views/RubricView.tsx +82 -0
@@ -0,0 +1,38 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { packageRoot } from "./packageRoot.js";
5
+
6
+ export const OPENCODE_PLUGIN_MARKER = "abide-opencode-plugin";
7
+
8
+ /**
9
+ * The plugin as shipped in the package. The installed file only points at it,
10
+ * so an upgrade needs no reinstall. OpenCode discovers `plugins/*.js` and
11
+ * `*.ts` only, so the installed file is `.js` whatever the package uses.
12
+ */
13
+ export const pluginSourcePath = (): string => path.join(packageRoot(), "opencode", "abide.mjs");
14
+
15
+ const shim = (source: string): string =>
16
+ [
17
+ `// ${OPENCODE_PLUGIN_MARKER}: written by \`abide init opencode\`; remove with \`abide uninstall opencode\`.`,
18
+ `export { default } from ${JSON.stringify(pathToFileURL(source).href)};`,
19
+ "",
20
+ ].join("\n");
21
+
22
+ export const installOpencodePlugin = (target: string): void => {
23
+ mkdirSync(path.dirname(target), { recursive: true });
24
+ writeFileSync(target, shim(pluginSourcePath()));
25
+ };
26
+
27
+ export const uninstallOpencodePlugin = (target: string): boolean => {
28
+ if (!existsSync(target)) return false;
29
+ let text: string;
30
+ try {
31
+ text = readFileSync(target, "utf8");
32
+ } catch {
33
+ return false;
34
+ }
35
+ if (!text.includes(OPENCODE_PLUGIN_MARKER)) return false;
36
+ rmSync(target);
37
+ return true;
38
+ };
@@ -0,0 +1,44 @@
1
+ import { assertNever, type HookOutput } from "@coldtea/abide-schema";
2
+
3
+ export type HookEventName = "SessionStart" | "UserPromptSubmit" | "PostToolUse" | "Stop";
4
+
5
+ const write = (payload: Record<string, unknown>, done: () => void): void => {
6
+ process.stdout.write(`${JSON.stringify(payload)}\n`, () => done());
7
+ };
8
+
9
+ /** The only path to stdout. Everything else goes to stderr, which the host keeps out of the transcript. */
10
+ export const emit = (output: HookOutput, event: HookEventName, done: () => void): void => {
11
+ switch (output.kind) {
12
+ case "silent":
13
+ done();
14
+ return;
15
+ case "session-context":
16
+ write(
17
+ {
18
+ ...(output.systemMessage === undefined ? {} : { systemMessage: output.systemMessage }),
19
+ hookSpecificOutput: { hookEventName: event, additionalContext: output.additionalContext },
20
+ },
21
+ done,
22
+ );
23
+ return;
24
+ case "block":
25
+ write(
26
+ {
27
+ decision: "block",
28
+ reason: output.reason,
29
+ ...(output.systemMessage === undefined ? {} : { systemMessage: output.systemMessage }),
30
+ },
31
+ done,
32
+ );
33
+ return;
34
+ case "notice":
35
+ write({ systemMessage: output.systemMessage }, done);
36
+ return;
37
+ default:
38
+ return assertNever(output);
39
+ }
40
+ };
41
+
42
+ export const debug = (message: string): void => {
43
+ if (process.env.ABIDE_DEBUG) process.stderr.write(`abide: ${message}\n`);
44
+ };
@@ -0,0 +1,48 @@
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { readRegularText, writeRegularFile } from "./regularFile.js";
5
+
6
+ /** Directory of the installed @coldtea/abide package (the one holding package.json). */
7
+ export const packageRoot = (): string => {
8
+ let dir = path.dirname(fileURLToPath(import.meta.url));
9
+ for (;;) {
10
+ const candidate = path.join(dir, "package.json");
11
+ if (existsSync(candidate)) {
12
+ try {
13
+ const name: unknown = JSON.parse(readFileSync(candidate, "utf8")).name;
14
+ if (name === "@coldtea/abide") return dir;
15
+ } catch {
16
+ // keep walking
17
+ }
18
+ }
19
+ const parent = path.dirname(dir);
20
+ if (parent === dir) return dir;
21
+ dir = parent;
22
+ }
23
+ };
24
+
25
+ export const hookScriptPath = (): string => path.join(packageRoot(), "dist", "abide-hook.js");
26
+ export const binScriptPath = (): string => path.join(packageRoot(), "dist", "bin.js");
27
+ export const compileSkillPath = (): string =>
28
+ path.join(packageRoot(), "skills", "abide-compile", "SKILL.md");
29
+
30
+ /**
31
+ * Copies the packaged skill into the repo so the agent can read it without
32
+ * leaving the project. A FIFO there would hold the hook and a symlink would
33
+ * redirect the copy, so either falls back to the packaged path.
34
+ */
35
+ export const placeCompileSkill = (root: string): string => {
36
+ const packaged = compileSkillPath();
37
+ const target = path.join(root, ".abide", "compile-skill.md");
38
+ const skill = readRegularText(packaged);
39
+ if (skill === undefined) return packaged;
40
+ try {
41
+ mkdirSync(path.dirname(target), { recursive: true });
42
+ } catch {
43
+ return packaged;
44
+ }
45
+ return writeRegularFile(target, skill, { use: "replace", followSymlinks: false })
46
+ ? target
47
+ : packaged;
48
+ };
@@ -0,0 +1,96 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import picomatch from "picomatch";
5
+
6
+ export const homeDir = (): string => process.env.ABIDE_HOME_DIR ?? homedir();
7
+
8
+ export const globalAbideDir = (): string => path.join(homeDir(), ".abide");
9
+ export const globalRubricPath = (): string => path.join(globalAbideDir(), "global.json");
10
+ export const sessionsDir = (): string => path.join(globalAbideDir(), "sessions");
11
+
12
+ export const abideDir = (root: string): string => path.join(root, ".abide");
13
+ export const rubricPath = (root: string): string => path.join(abideDir(root), "rubric.json");
14
+ export const eventsPath = (root: string): string => path.join(abideDir(root), "events.jsonl");
15
+
16
+ const isDir = (p: string): boolean => {
17
+ try {
18
+ return statSync(p).isDirectory();
19
+ } catch {
20
+ return false;
21
+ }
22
+ };
23
+
24
+ const ROOT_MARKERS = [".git", ".abide", "AGENTS.md", "CLAUDE.md"];
25
+
26
+ /** The nearest ancestor that looks like a repository root, else the start directory. */
27
+ export const findRepoRoot = (start: string): string => {
28
+ let dir = path.resolve(start);
29
+ if (!isDir(dir)) dir = path.dirname(dir);
30
+ let fallback: string | undefined;
31
+ for (;;) {
32
+ if (existsSync(path.join(dir, ".git"))) return dir;
33
+ if (fallback === undefined && ROOT_MARKERS.some((m) => existsSync(path.join(dir, m)))) {
34
+ fallback = dir;
35
+ }
36
+ const parent = path.dirname(dir);
37
+ if (parent === dir) break;
38
+ dir = parent;
39
+ }
40
+ return fallback ?? path.resolve(start);
41
+ };
42
+
43
+ export const expandHome = (p: string): string =>
44
+ p === "~" ? homeDir() : p.startsWith("~/") ? path.join(homeDir(), p.slice(2)) : p;
45
+
46
+ /** Absolute location of a rubric source path ("~/x" or repo-relative). */
47
+ export const resolveSourcePath = (root: string, sourcePath: string): string =>
48
+ sourcePath.startsWith("~") ? expandHome(sourcePath) : path.resolve(root, sourcePath);
49
+
50
+ const toPosix = (p: string): string => p.split(path.sep).join("/");
51
+
52
+ /** The rubric's spelling of an absolute path: repo-relative, "~/..." inside home, else absolute. */
53
+ export const toSourcePath = (root: string, absolute: string): string => {
54
+ const rel = path.relative(root, absolute);
55
+ if (
56
+ path.resolve(root) !== homeDir() &&
57
+ rel !== "" &&
58
+ !rel.startsWith("..") &&
59
+ !path.isAbsolute(rel)
60
+ ) {
61
+ return toPosix(rel);
62
+ }
63
+ const fromHome = path.relative(homeDir(), absolute);
64
+ if (fromHome !== "" && !fromHome.startsWith("..") && !path.isAbsolute(fromHome)) {
65
+ return `~/${toPosix(fromHome)}`;
66
+ }
67
+ return toPosix(absolute);
68
+ };
69
+
70
+ /** One spelling for a source path however the agent wrote it. */
71
+ export const canonicalSourcePath = (root: string, sourcePath: string): string =>
72
+ toSourcePath(root, resolveSourcePath(root, sourcePath));
73
+
74
+ export const relativeToRoot = (root: string, absolute: string): string =>
75
+ toPosix(path.relative(root, absolute));
76
+
77
+ /** Files abide owns are never checked; the agent writes the rubric under supervision of the compile skill. */
78
+ export const isAbideOwned = (relativePath: string): boolean =>
79
+ relativePath === ".abide" || relativePath.startsWith(".abide/");
80
+
81
+ /** Basename globs; also fed to git as pathspecs. */
82
+ export const SECRET_FILE_PATTERNS: readonly string[] = [
83
+ ".env",
84
+ ".env.*",
85
+ ".envrc",
86
+ "*.pem",
87
+ "*.key",
88
+ ];
89
+
90
+ const isSecretName = picomatch([...SECRET_FILE_PATTERNS], { dot: true });
91
+
92
+ export const isSecretFile = (relativePath: string): boolean =>
93
+ isSecretName(path.posix.basename(relativePath));
94
+
95
+ export const isExcludedPath = (relativePath: string): boolean =>
96
+ isAbideOwned(relativePath) || isSecretFile(relativePath);
@@ -0,0 +1,46 @@
1
+ import type { Rule, Verdict } from "@coldtea/abide-schema";
2
+
3
+ const where = (rule: Rule): string =>
4
+ rule.source.line === undefined
5
+ ? rule.source.path
6
+ : `${rule.source.path} line ${rule.source.line}`;
7
+
8
+ const quote = (text: string): string => {
9
+ const trimmed = text.trim().replace(/\s+/g, " ");
10
+ return trimmed.length > 220 ? `${trimmed.slice(0, 217)}...` : trimmed;
11
+ };
12
+
13
+ /** One sentence per violation, naming the rule. Never the whole instruction file. */
14
+ export const repairReason = (
15
+ phase: "edit" | "turn",
16
+ violations: readonly { rule: Rule; verdict: Verdict }[],
17
+ files: readonly string[],
18
+ ): string => {
19
+ const lines = violations.map(({ rule, verdict }) => {
20
+ const evidence =
21
+ rule.check.type === "lint" && verdict.answer !== undefined
22
+ ? ` Matched: ${verdict.answer}`
23
+ : verdict.answer !== undefined
24
+ ? ` Judged: ${verdict.answer} (${verdict.probability.toFixed(2)}).`
25
+ : ` (${verdict.probability.toFixed(2)})`;
26
+ return `Rule "${rule.id}" from ${where(rule)}: "${quote(rule.text)}".${evidence}`;
27
+ });
28
+ const subject = phase === "edit" ? "This edit" : "The changes in this turn";
29
+ const target = files.length === 1 ? files[0] : `${files.length} files (${files.join(", ")})`;
30
+ const ask =
31
+ phase === "edit"
32
+ ? `Repair ${target} now, then continue with the task.`
33
+ : `Repair ${target} before you finish. Keep the fix to what the rule asks.`;
34
+ return `Abide: ${subject} appears to break ${lines.length === 1 ? "a rule" : `${lines.length} rules`} from this repository's instructions.\n${lines.map((l) => `- ${l}`).join("\n")}\n${ask}`;
35
+ };
36
+
37
+ export const flagNotice = (
38
+ phase: "edit" | "turn",
39
+ flagged: readonly { rule: Rule; verdict: Verdict }[],
40
+ files: readonly string[],
41
+ ): string => {
42
+ const list = flagged
43
+ .map(({ rule, verdict }) => `${rule.id} ${verdict.probability.toFixed(2)}`)
44
+ .join(", ");
45
+ return `Abide: uncertain about ${list} on ${files.join(", ")} (${phase}). Not sent to the agent. Details in .abide/events.jsonl.`;
46
+ };
@@ -0,0 +1,96 @@
1
+ import {
2
+ closeSync,
3
+ constants,
4
+ fstatSync,
5
+ openSync,
6
+ readSync,
7
+ writeSync,
8
+ type Stats,
9
+ } from "node:fs";
10
+ import { MAX_FILE_READ_BYTES } from "./constants.js";
11
+
12
+ type Use = "read" | "append" | "replace";
13
+
14
+ type OpenOptions = {
15
+ followSymlinks?: boolean;
16
+ use?: Use;
17
+ };
18
+
19
+ type ReadOptions = Omit<OpenOptions, "use"> & { maxBytes?: number };
20
+
21
+ type WriteOptions = Omit<OpenOptions, "use"> & { use: Exclude<Use, "read"> };
22
+
23
+ const FLAGS: Record<Use, number> = {
24
+ read: constants.O_RDONLY,
25
+ append: constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT,
26
+ replace: constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC,
27
+ };
28
+
29
+ /**
30
+ * A FIFO or device would hold a synchronous read or write for as long as the
31
+ * other side likes, past any hook deadline. Opening non-blocking and judging
32
+ * the descriptor leaves no window between the check and the use. The caller
33
+ * closes it.
34
+ */
35
+ export const openRegular = (
36
+ file: string,
37
+ options: OpenOptions = {},
38
+ ): { fd: number; size: number } | undefined => {
39
+ const { followSymlinks = true, use = "read" } = options;
40
+ let fd: number;
41
+ try {
42
+ fd = openSync(
43
+ file,
44
+ FLAGS[use] | constants.O_NONBLOCK | (followSymlinks ? 0 : constants.O_NOFOLLOW),
45
+ );
46
+ } catch {
47
+ return undefined;
48
+ }
49
+ let stat: Stats | undefined;
50
+ try {
51
+ stat = fstatSync(fd);
52
+ } catch {
53
+ stat = undefined;
54
+ }
55
+ if (stat?.isFile()) return { fd, size: stat.size };
56
+ closeSync(fd);
57
+ return undefined;
58
+ };
59
+
60
+ export const readRegularFile = (file: string, options: ReadOptions = {}): Buffer | undefined => {
61
+ const { maxBytes = MAX_FILE_READ_BYTES, ...open } = options;
62
+ const opened = openRegular(file, open);
63
+ if (opened === undefined) return undefined;
64
+ const { fd, size } = opened;
65
+ try {
66
+ if (size > maxBytes) return undefined;
67
+ const buffer = Buffer.alloc(size);
68
+ let read = 0;
69
+ while (read < size) {
70
+ const count = readSync(fd, buffer, read, size - read, read);
71
+ if (count === 0) break;
72
+ read += count;
73
+ }
74
+ return buffer.subarray(0, read);
75
+ } catch {
76
+ return undefined;
77
+ } finally {
78
+ closeSync(fd);
79
+ }
80
+ };
81
+
82
+ export const readRegularText = (file: string, options: ReadOptions = {}): string | undefined =>
83
+ readRegularFile(file, options)?.toString("utf8");
84
+
85
+ export const writeRegularFile = (file: string, text: string, options: WriteOptions): boolean => {
86
+ const opened = openRegular(file, options);
87
+ if (opened === undefined) return false;
88
+ try {
89
+ writeSync(opened.fd, text);
90
+ return true;
91
+ } catch {
92
+ return false;
93
+ } finally {
94
+ closeSync(opened.fd);
95
+ }
96
+ };
@@ -0,0 +1,314 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ postToolUseInputSchema,
5
+ type PostToolUseInput,
6
+ type Rule,
7
+ type Thresholds,
8
+ type Verdict,
9
+ } from "@coldtea/abide-schema";
10
+ import { pool } from "./audit.js";
11
+ import { runCheck } from "./checkRunner.js";
12
+ import { EDIT_CHECK_TIMEOUT_MS, MAX_TASK_CHARS, TURN_CHECK_TIMEOUT_MS } from "./constants.js";
13
+ import { boundState, editsFromPostToolUse } from "./diff.js";
14
+ import type { FileDiff } from "./git.js";
15
+ import { findRepoRoot, isExcludedPath, relativeToRoot } from "./paths.js";
16
+
17
+ /**
18
+ * A past coding session, read back from the host's transcript, so every edit
19
+ * it made can be judged as if abide had been installed at the time. Nothing
20
+ * here touches the working tree: the diffs come from the transcript alone.
21
+ */
22
+ export type ReplayEdit = { turn: number; input: PostToolUseInput };
23
+ export type ReplayTurn = { index: number; prompt: string | undefined; edits: ReplayEdit[] };
24
+ export type ReplaySession = { file: string; cwd: string; turns: ReplayTurn[] };
25
+
26
+ const EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit"]);
27
+
28
+ type ToolUse = { id: string; name: string; input: unknown; turn: number };
29
+
30
+ const asRecord = (value: unknown): Record<string, unknown> | undefined => {
31
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
32
+ const record: Record<string, unknown> = { ...value };
33
+ return record;
34
+ };
35
+
36
+ const asList = (value: unknown): unknown[] => (Array.isArray(value) ? value : []);
37
+
38
+ const promptText = (content: unknown): string | undefined => {
39
+ if (typeof content === "string") return content.trim() === "" ? undefined : content;
40
+ const parts = asList(content).flatMap((p) => {
41
+ const part = asRecord(p);
42
+ return part?.type === "text" && typeof part.text === "string" ? [part.text] : [];
43
+ });
44
+ const hasToolResult = asList(content).some((p) => asRecord(p)?.type === "tool_result");
45
+ if (hasToolResult || parts.length === 0) return undefined;
46
+ return parts.join("\n");
47
+ };
48
+
49
+ export const parseTranscript = (file: string): ReplaySession => {
50
+ const turns: ReplayTurn[] = [];
51
+ const uses = new Map<string, ToolUse>();
52
+ let cwd: string | undefined;
53
+ let turnIndex = 0;
54
+ const current = (): ReplayTurn => {
55
+ const last = turns.at(-1);
56
+ if (last !== undefined) return last;
57
+ const first: ReplayTurn = { index: 0, prompt: undefined, edits: [] };
58
+ turns.push(first);
59
+ return first;
60
+ };
61
+ for (const line of readFileSync(file, "utf8").split("\n")) {
62
+ if (line.trim() === "") continue;
63
+ let entry: Record<string, unknown> | undefined;
64
+ try {
65
+ entry = asRecord(JSON.parse(line));
66
+ } catch {
67
+ // a torn line, still being written
68
+ continue;
69
+ }
70
+ if (entry === undefined) continue;
71
+ if (typeof entry.cwd === "string") cwd ??= entry.cwd;
72
+ const message = asRecord(entry.message);
73
+ if (entry.type === "user" && entry.isMeta !== true) {
74
+ const prompt = promptText(message?.content);
75
+ if (prompt !== undefined) {
76
+ turnIndex += 1;
77
+ turns.push({ index: turnIndex, prompt: prompt.slice(0, MAX_TASK_CHARS), edits: [] });
78
+ }
79
+ const result = asRecord(entry.toolUseResult);
80
+ for (const p of asList(message?.content)) {
81
+ const part = asRecord(p);
82
+ if (part?.type !== "tool_result" || typeof part.tool_use_id !== "string") continue;
83
+ const use = uses.get(part.tool_use_id);
84
+ if (use === undefined) continue;
85
+ uses.delete(part.tool_use_id);
86
+ const parsed = postToolUseInputSchema.safeParse({
87
+ session_id: path.basename(file, ".jsonl"),
88
+ cwd: cwd ?? process.cwd(),
89
+ hook_event_name: "PostToolUse",
90
+ tool_name: use.name,
91
+ tool_use_id: use.id,
92
+ tool_input: use.input,
93
+ ...(result === undefined ? {} : { tool_response: result }),
94
+ });
95
+ if (!parsed.success) continue;
96
+ const turn = turns.find((t) => t.index === use.turn) ?? current();
97
+ turn.edits.push({ turn: turn.index, input: parsed.data });
98
+ }
99
+ }
100
+ if (entry.type === "assistant") {
101
+ for (const p of asList(message?.content)) {
102
+ const part = asRecord(p);
103
+ if (part?.type !== "tool_use" || typeof part.id !== "string") continue;
104
+ if (typeof part.name !== "string" || !EDIT_TOOLS.has(part.name)) continue;
105
+ uses.set(part.id, { id: part.id, name: part.name, input: part.input, turn: turnIndex });
106
+ }
107
+ }
108
+ }
109
+ return { file, cwd: cwd ?? process.cwd(), turns: turns.filter((t) => t.edits.length > 0) };
110
+ };
111
+
112
+ export type ReplayEditResult = {
113
+ session: string;
114
+ turn: number;
115
+ file: string;
116
+ verdicts: Verdict[];
117
+ costUsd: number;
118
+ error?: string;
119
+ diff?: string;
120
+ task?: string;
121
+ };
122
+
123
+ export type ReplayTurnResult = {
124
+ session: string;
125
+ turn: number;
126
+ files: string[];
127
+ verdicts: Verdict[];
128
+ costUsd: number;
129
+ error?: string;
130
+ };
131
+
132
+ export type ReplayResult = { edits: ReplayEditResult[]; turns: ReplayTurnResult[] };
133
+
134
+ export type ReplayProgress = (done: number, total: number, spendUsd: number) => void;
135
+
136
+ const failed = (error: unknown): string => (error instanceof Error ? error.message : String(error));
137
+
138
+ /**
139
+ * Judges every edit of every session as the edit hook would have, then every
140
+ * turn as the stop hook would have, from the edits alone. Shell-made changes
141
+ * are not in a transcript and are not judged.
142
+ */
143
+ export const replaySessions = async (
144
+ sessions: readonly ReplaySession[],
145
+ rules: readonly Rule[],
146
+ thresholds: Thresholds,
147
+ concurrency: number,
148
+ progress: ReplayProgress,
149
+ keepDiffs = false,
150
+ ): Promise<ReplayResult> => {
151
+ type Job = { session: ReplaySession; turn: ReplayTurn; edit: ReplayEdit };
152
+ const jobs: Job[] = sessions.flatMap((session) =>
153
+ session.turns.flatMap((turn) => turn.edits.map((edit) => ({ session, turn, edit }))),
154
+ );
155
+ const edits: ReplayEditResult[] = [];
156
+ const turnDiffs = new Map<
157
+ string,
158
+ { session: ReplaySession; turn: ReplayTurn; files: Map<string, string[]> }
159
+ >();
160
+ let done = 0;
161
+ let spendUsd = 0;
162
+ await pool(jobs, concurrency, async ({ session, turn, edit }) => {
163
+ const root = findRepoRoot(session.cwd);
164
+ const name = path.basename(session.file, ".jsonl");
165
+ for (const hunk of editsFromPostToolUse(edit.input)) {
166
+ const relative = relativeToRoot(root, hunk.filePath);
167
+ if (relative.startsWith("..") || isExcludedPath(relative) || hunk.text === undefined)
168
+ continue;
169
+ const { text } = boundState(hunk.text);
170
+ if (text.trim() === "") continue;
171
+ const key = `${name}:${turn.index}`;
172
+ const bucket = turnDiffs.get(key) ?? { session, turn, files: new Map<string, string[]>() };
173
+ bucket.files.set(relative, [...(bucket.files.get(relative) ?? []), text]);
174
+ turnDiffs.set(key, bucket);
175
+ try {
176
+ const out = await runCheck({
177
+ phase: "edit",
178
+ fileDiffs: [{ file: relative, text }],
179
+ task: turn.prompt,
180
+ rules,
181
+ thresholds,
182
+ timeoutMs: EDIT_CHECK_TIMEOUT_MS,
183
+ retries: 2,
184
+ });
185
+ spendUsd += out.usage.costUsd ?? 0;
186
+ edits.push({
187
+ session: name,
188
+ turn: turn.index,
189
+ file: relative,
190
+ verdicts: out.verdicts,
191
+ costUsd: out.usage.costUsd ?? 0,
192
+ ...(keepDiffs
193
+ ? { diff: text, ...(turn.prompt === undefined ? {} : { task: turn.prompt }) }
194
+ : {}),
195
+ });
196
+ } catch (error) {
197
+ edits.push({
198
+ session: name,
199
+ turn: turn.index,
200
+ file: relative,
201
+ verdicts: [],
202
+ costUsd: 0,
203
+ error: failed(error),
204
+ });
205
+ }
206
+ }
207
+ done += 1;
208
+ progress(done, jobs.length, spendUsd);
209
+ });
210
+
211
+ const turns: ReplayTurnResult[] = [];
212
+ await pool([...turnDiffs.values()], concurrency, async ({ session, turn, files }) => {
213
+ const name = path.basename(session.file, ".jsonl");
214
+ const fileDiffs: FileDiff[] = [...files.entries()].map(([file, texts]) => ({
215
+ file,
216
+ text: boundState(texts.join("\n"), 8_000).text,
217
+ }));
218
+ try {
219
+ const out = await runCheck({
220
+ phase: "turn",
221
+ fileDiffs,
222
+ task: turn.prompt,
223
+ rules,
224
+ thresholds,
225
+ timeoutMs: TURN_CHECK_TIMEOUT_MS,
226
+ retries: 2,
227
+ });
228
+ spendUsd += out.usage.costUsd ?? 0;
229
+ turns.push({
230
+ session: name,
231
+ turn: turn.index,
232
+ files: fileDiffs.map((f) => f.file),
233
+ verdicts: out.verdicts,
234
+ costUsd: out.usage.costUsd ?? 0,
235
+ });
236
+ } catch (error) {
237
+ turns.push({
238
+ session: name,
239
+ turn: turn.index,
240
+ files: fileDiffs.map((f) => f.file),
241
+ verdicts: [],
242
+ costUsd: 0,
243
+ error: failed(error),
244
+ });
245
+ }
246
+ progress(done, jobs.length, spendUsd);
247
+ });
248
+ return { edits, turns };
249
+ };
250
+
251
+ /** Turn ranges the drift is reported over. The first covers the session's opening, where instructions are fresh. */
252
+ export const DRIFT_BUCKETS: readonly { label: string; from: number; to: number }[] = [
253
+ { label: "turns 1 to 5", from: 1, to: 5 },
254
+ { label: "turns 6 to 15", from: 6, to: 15 },
255
+ { label: "turns 16 and later", from: 16, to: Infinity },
256
+ ];
257
+
258
+ export type DriftRow = {
259
+ label: string;
260
+ edits: number;
261
+ broken: number;
262
+ /** Share of judged edits with at least one rule above the act line. */
263
+ rate: number;
264
+ };
265
+
266
+ export const driftByTurn = (edits: readonly ReplayEditResult[]): DriftRow[] =>
267
+ DRIFT_BUCKETS.map((b) => {
268
+ const inBucket = edits.filter(
269
+ (e) => e.error === undefined && e.turn >= b.from && e.turn <= b.to,
270
+ );
271
+ const broken = inBucket.filter((e) => e.verdicts.some((v) => v.band === "act")).length;
272
+ return {
273
+ label: b.label,
274
+ edits: inBucket.length,
275
+ broken,
276
+ rate: inBucket.length === 0 ? 0 : broken / inBucket.length,
277
+ };
278
+ });
279
+
280
+ export type RuleTally = {
281
+ rule: string;
282
+ phase: "edit" | "turn";
283
+ broken: number;
284
+ flagged: number;
285
+ of: number;
286
+ example: string;
287
+ };
288
+
289
+ export const tallyRules = (result: ReplayResult, rules: readonly Rule[]): RuleTally[] => {
290
+ const tallies: RuleTally[] = [];
291
+ for (const rule of rules) {
292
+ if (rule.check.type !== "model") continue;
293
+ const phase = rule.when ?? "edit";
294
+ const items: { file: string; verdicts: Verdict[] }[] =
295
+ phase === "edit"
296
+ ? result.edits.map((e) => ({ file: e.file, verdicts: e.verdicts }))
297
+ : result.turns.map((t) => ({ file: t.files.join(", "), verdicts: t.verdicts }));
298
+ let broken = 0;
299
+ let flagged = 0;
300
+ let of = 0;
301
+ let example = "";
302
+ for (const item of items) {
303
+ const v = item.verdicts.find((x) => x.ruleId === rule.id);
304
+ if (v === undefined) continue;
305
+ of += 1;
306
+ if (v.band === "act") {
307
+ broken += 1;
308
+ if (example === "") example = item.file;
309
+ } else if (v.band === "flag") flagged += 1;
310
+ }
311
+ if (of > 0) tallies.push({ rule: rule.id, phase, broken, flagged, of, example });
312
+ }
313
+ return tallies.sort((a, b) => b.broken - a.broken || b.flagged - a.flagged);
314
+ };