plumbbob 0.9.3 → 0.10.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 (320) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +92 -89
  3. package/dist/cli-core.js +25 -8
  4. package/dist/cli.js +1 -1
  5. package/dist/lib/agents.js +26 -26
  6. package/dist/lib/buildlog.js +11 -11
  7. package/dist/lib/buildlogsync.js +8 -8
  8. package/dist/lib/check.js +55 -22
  9. package/dist/lib/commitbody.js +72 -0
  10. package/dist/lib/commitmsg.js +7 -7
  11. package/dist/lib/git.js +44 -15
  12. package/dist/lib/intent.js +21 -20
  13. package/dist/lib/latch.js +19 -19
  14. package/dist/lib/orient.js +36 -17
  15. package/dist/lib/reentry.js +70 -0
  16. package/dist/lib/settings.js +16 -16
  17. package/dist/lib/sidecar.js +54 -54
  18. package/dist/lib/templates.js +1 -1
  19. package/dist/verbs/agent.js +27 -27
  20. package/dist/verbs/build.js +9 -9
  21. package/dist/verbs/check.js +6 -6
  22. package/dist/verbs/checkpoint.js +48 -47
  23. package/dist/verbs/doctor.js +31 -30
  24. package/dist/verbs/finish.js +28 -41
  25. package/dist/verbs/handoff.js +9 -8
  26. package/dist/verbs/init.js +9 -9
  27. package/dist/verbs/park.js +3 -3
  28. package/dist/verbs/recover.js +297 -0
  29. package/dist/verbs/revert.js +13 -13
  30. package/dist/verbs/spike.js +25 -19
  31. package/dist/verbs/start.js +49 -19
  32. package/dist/verbs/status.js +8 -8
  33. package/dist/verbs/turn.js +16 -16
  34. package/dist/verbs/use.js +4 -4
  35. package/hooks/post-edit.sh +4 -4
  36. package/hooks/pre-bash-commit.sh +3 -3
  37. package/node_modules/checkride/.claude-plugin/plugin.json +11 -0
  38. package/node_modules/checkride/CHANGELOG.md +1404 -0
  39. package/node_modules/checkride/LICENSE +201 -21
  40. package/node_modules/checkride/README.md +197 -38
  41. package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.d.ts +15 -0
  42. package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.d.ts.map +1 -0
  43. package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.js +17 -0
  44. package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.js.map +1 -0
  45. package/node_modules/checkride/dist/adapters.d.ts +30 -10
  46. package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
  47. package/node_modules/checkride/dist/adapters.js +62 -23
  48. package/node_modules/checkride/dist/adapters.js.map +1 -1
  49. package/node_modules/checkride/dist/agent-setup/claude.d.ts +88 -0
  50. package/node_modules/checkride/dist/agent-setup/claude.d.ts.map +1 -0
  51. package/node_modules/checkride/dist/agent-setup/claude.js +305 -0
  52. package/node_modules/checkride/dist/agent-setup/claude.js.map +1 -0
  53. package/node_modules/checkride/dist/agent-setup/command.d.ts +43 -0
  54. package/node_modules/checkride/dist/agent-setup/command.d.ts.map +1 -0
  55. package/node_modules/checkride/dist/agent-setup/command.js +77 -0
  56. package/node_modules/checkride/dist/agent-setup/command.js.map +1 -0
  57. package/node_modules/checkride/dist/agent-setup/cursor.d.ts +69 -0
  58. package/node_modules/checkride/dist/agent-setup/cursor.d.ts.map +1 -0
  59. package/node_modules/checkride/dist/agent-setup/cursor.js +247 -0
  60. package/node_modules/checkride/dist/agent-setup/cursor.js.map +1 -0
  61. package/node_modules/checkride/dist/agent-setup/files.d.ts +50 -0
  62. package/node_modules/checkride/dist/agent-setup/files.d.ts.map +1 -0
  63. package/node_modules/checkride/dist/agent-setup/files.js +80 -0
  64. package/node_modules/checkride/dist/agent-setup/files.js.map +1 -0
  65. package/node_modules/checkride/dist/agent-setup/harness.d.ts +101 -0
  66. package/node_modules/checkride/dist/agent-setup/harness.d.ts.map +1 -0
  67. package/node_modules/checkride/dist/agent-setup/harness.js +93 -0
  68. package/node_modules/checkride/dist/agent-setup/harness.js.map +1 -0
  69. package/node_modules/checkride/dist/agent-setup/hook.d.ts +45 -52
  70. package/node_modules/checkride/dist/agent-setup/hook.d.ts.map +1 -1
  71. package/node_modules/checkride/dist/agent-setup/hook.js +64 -95
  72. package/node_modules/checkride/dist/agent-setup/hook.js.map +1 -1
  73. package/node_modules/checkride/dist/agent-setup/index.d.ts +10 -4
  74. package/node_modules/checkride/dist/agent-setup/index.d.ts.map +1 -1
  75. package/node_modules/checkride/dist/agent-setup/index.js +9 -4
  76. package/node_modules/checkride/dist/agent-setup/index.js.map +1 -1
  77. package/node_modules/checkride/dist/agent-setup/scripts.d.ts +81 -0
  78. package/node_modules/checkride/dist/agent-setup/scripts.d.ts.map +1 -0
  79. package/node_modules/checkride/dist/agent-setup/scripts.js +604 -0
  80. package/node_modules/checkride/dist/agent-setup/scripts.js.map +1 -0
  81. package/node_modules/checkride/dist/agent-setup/skills.d.ts +31 -0
  82. package/node_modules/checkride/dist/agent-setup/skills.d.ts.map +1 -0
  83. package/node_modules/checkride/dist/agent-setup/skills.js +56 -0
  84. package/node_modules/checkride/dist/agent-setup/skills.js.map +1 -0
  85. package/node_modules/checkride/dist/artifacts/configured.d.ts +30 -0
  86. package/node_modules/checkride/dist/artifacts/configured.d.ts.map +1 -0
  87. package/node_modules/checkride/dist/artifacts/configured.js +45 -0
  88. package/node_modules/checkride/dist/artifacts/configured.js.map +1 -0
  89. package/node_modules/checkride/dist/artifacts/format.d.ts +33 -0
  90. package/node_modules/checkride/dist/artifacts/format.d.ts.map +1 -0
  91. package/node_modules/checkride/dist/artifacts/format.js +56 -0
  92. package/node_modules/checkride/dist/artifacts/format.js.map +1 -0
  93. package/node_modules/checkride/dist/artifacts/freshness.d.ts +32 -0
  94. package/node_modules/checkride/dist/artifacts/freshness.d.ts.map +1 -0
  95. package/node_modules/checkride/dist/artifacts/freshness.js +37 -0
  96. package/node_modules/checkride/dist/artifacts/freshness.js.map +1 -0
  97. package/node_modules/checkride/dist/artifacts/index.d.ts +22 -0
  98. package/node_modules/checkride/dist/artifacts/index.d.ts.map +1 -0
  99. package/node_modules/checkride/dist/artifacts/index.js +19 -0
  100. package/node_modules/checkride/dist/artifacts/index.js.map +1 -0
  101. package/node_modules/checkride/dist/artifacts/json.d.ts +31 -0
  102. package/node_modules/checkride/dist/artifacts/json.d.ts.map +1 -0
  103. package/node_modules/checkride/dist/artifacts/json.js +47 -0
  104. package/node_modules/checkride/dist/artifacts/json.js.map +1 -0
  105. package/node_modules/checkride/dist/artifacts/raw.d.ts +60 -0
  106. package/node_modules/checkride/dist/artifacts/raw.d.ts.map +1 -0
  107. package/node_modules/checkride/dist/artifacts/raw.js +91 -0
  108. package/node_modules/checkride/dist/artifacts/raw.js.map +1 -0
  109. package/node_modules/checkride/dist/artifacts/summary.d.ts +69 -0
  110. package/node_modules/checkride/dist/artifacts/summary.d.ts.map +1 -0
  111. package/node_modules/checkride/dist/artifacts/summary.js +108 -0
  112. package/node_modules/checkride/dist/artifacts/summary.js.map +1 -0
  113. package/node_modules/checkride/dist/atomic.d.ts +16 -1
  114. package/node_modules/checkride/dist/atomic.d.ts.map +1 -1
  115. package/node_modules/checkride/dist/atomic.js +25 -3
  116. package/node_modules/checkride/dist/atomic.js.map +1 -1
  117. package/node_modules/checkride/dist/baseline/fallow.d.ts +9 -5
  118. package/node_modules/checkride/dist/baseline/fallow.d.ts.map +1 -1
  119. package/node_modules/checkride/dist/baseline/fallow.js +163 -60
  120. package/node_modules/checkride/dist/baseline/fallow.js.map +1 -1
  121. package/node_modules/checkride/dist/baseline/fingerprint.d.ts +20 -11
  122. package/node_modules/checkride/dist/baseline/fingerprint.d.ts.map +1 -1
  123. package/node_modules/checkride/dist/baseline/fingerprint.js +73 -23
  124. package/node_modules/checkride/dist/baseline/fingerprint.js.map +1 -1
  125. package/node_modules/checkride/dist/baseline/history.d.ts +98 -0
  126. package/node_modules/checkride/dist/baseline/history.d.ts.map +1 -0
  127. package/node_modules/checkride/dist/baseline/history.js +232 -0
  128. package/node_modules/checkride/dist/baseline/history.js.map +1 -0
  129. package/node_modules/checkride/dist/baseline/index.d.ts +7 -5
  130. package/node_modules/checkride/dist/baseline/index.d.ts.map +1 -1
  131. package/node_modules/checkride/dist/baseline/index.js +5 -4
  132. package/node_modules/checkride/dist/baseline/index.js.map +1 -1
  133. package/node_modules/checkride/dist/baseline/store.d.ts +41 -10
  134. package/node_modules/checkride/dist/baseline/store.d.ts.map +1 -1
  135. package/node_modules/checkride/dist/baseline/store.js +65 -24
  136. package/node_modules/checkride/dist/baseline/store.js.map +1 -1
  137. package/node_modules/checkride/dist/baseline-command.d.ts +5 -5
  138. package/node_modules/checkride/dist/baseline-command.js +7 -7
  139. package/node_modules/checkride/dist/baseline-command.js.map +1 -1
  140. package/node_modules/checkride/dist/cli.d.ts +21 -4
  141. package/node_modules/checkride/dist/cli.d.ts.map +1 -1
  142. package/node_modules/checkride/dist/cli.js +350 -36
  143. package/node_modules/checkride/dist/cli.js.map +1 -1
  144. package/node_modules/checkride/dist/config.d.ts +114 -9
  145. package/node_modules/checkride/dist/config.d.ts.map +1 -1
  146. package/node_modules/checkride/dist/config.js +142 -23
  147. package/node_modules/checkride/dist/config.js.map +1 -1
  148. package/node_modules/checkride/dist/digest/digest.d.ts +5 -5
  149. package/node_modules/checkride/dist/digest/digest.js +6 -6
  150. package/node_modules/checkride/dist/digest/index.d.ts +2 -2
  151. package/node_modules/checkride/dist/digest/index.js +2 -2
  152. package/node_modules/checkride/dist/doctor.d.ts +27 -6
  153. package/node_modules/checkride/dist/doctor.d.ts.map +1 -1
  154. package/node_modules/checkride/dist/doctor.js +231 -27
  155. package/node_modules/checkride/dist/doctor.js.map +1 -1
  156. package/node_modules/checkride/dist/gate.d.ts +180 -0
  157. package/node_modules/checkride/dist/gate.d.ts.map +1 -0
  158. package/node_modules/checkride/dist/gate.js +617 -0
  159. package/node_modules/checkride/dist/gate.js.map +1 -0
  160. package/node_modules/checkride/dist/init.d.ts +79 -12
  161. package/node_modules/checkride/dist/init.d.ts.map +1 -1
  162. package/node_modules/checkride/dist/init.js +385 -81
  163. package/node_modules/checkride/dist/init.js.map +1 -1
  164. package/node_modules/checkride/dist/json.d.ts +21 -0
  165. package/node_modules/checkride/dist/json.d.ts.map +1 -0
  166. package/node_modules/checkride/dist/json.js +21 -0
  167. package/node_modules/checkride/dist/json.js.map +1 -0
  168. package/node_modules/checkride/dist/links.d.ts +31 -5
  169. package/node_modules/checkride/dist/links.d.ts.map +1 -1
  170. package/node_modules/checkride/dist/links.js +141 -32
  171. package/node_modules/checkride/dist/links.js.map +1 -1
  172. package/node_modules/checkride/dist/node-pin.d.ts +129 -0
  173. package/node_modules/checkride/dist/node-pin.d.ts.map +1 -0
  174. package/node_modules/checkride/dist/node-pin.js +211 -0
  175. package/node_modules/checkride/dist/node-pin.js.map +1 -0
  176. package/node_modules/checkride/dist/orchestrator.d.ts +53 -10
  177. package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
  178. package/node_modules/checkride/dist/orchestrator.js +312 -113
  179. package/node_modules/checkride/dist/orchestrator.js.map +1 -1
  180. package/node_modules/checkride/dist/pack.d.ts +8 -9
  181. package/node_modules/checkride/dist/pack.d.ts.map +1 -1
  182. package/node_modules/checkride/dist/pack.js +53 -17
  183. package/node_modules/checkride/dist/pack.js.map +1 -1
  184. package/node_modules/checkride/dist/pm/detect.d.ts +6 -1
  185. package/node_modules/checkride/dist/pm/detect.d.ts.map +1 -1
  186. package/node_modules/checkride/dist/pm/detect.js +6 -1
  187. package/node_modules/checkride/dist/pm/detect.js.map +1 -1
  188. package/node_modules/checkride/dist/pm/index.d.ts +7 -2
  189. package/node_modules/checkride/dist/pm/index.d.ts.map +1 -1
  190. package/node_modules/checkride/dist/pm/index.js +6 -2
  191. package/node_modules/checkride/dist/pm/index.js.map +1 -1
  192. package/node_modules/checkride/dist/pm/launch.d.ts +72 -0
  193. package/node_modules/checkride/dist/pm/launch.d.ts.map +1 -0
  194. package/node_modules/checkride/dist/pm/launch.js +108 -0
  195. package/node_modules/checkride/dist/pm/launch.js.map +1 -0
  196. package/node_modules/checkride/dist/pm/tools.d.ts +73 -0
  197. package/node_modules/checkride/dist/pm/tools.d.ts.map +1 -0
  198. package/node_modules/checkride/dist/pm/tools.js +124 -0
  199. package/node_modules/checkride/dist/pm/tools.js.map +1 -0
  200. package/node_modules/checkride/dist/pm/translate.d.ts +35 -9
  201. package/node_modules/checkride/dist/pm/translate.d.ts.map +1 -1
  202. package/node_modules/checkride/dist/pm/translate.js +78 -18
  203. package/node_modules/checkride/dist/pm/translate.js.map +1 -1
  204. package/node_modules/checkride/dist/proc.d.ts +30 -0
  205. package/node_modules/checkride/dist/proc.d.ts.map +1 -0
  206. package/node_modules/checkride/dist/proc.js +42 -0
  207. package/node_modules/checkride/dist/proc.js.map +1 -0
  208. package/node_modules/checkride/dist/qa/cli.d.ts +20 -0
  209. package/node_modules/checkride/dist/qa/cli.d.ts.map +1 -0
  210. package/node_modules/checkride/dist/qa/cli.js +24 -0
  211. package/node_modules/checkride/dist/qa/cli.js.map +1 -0
  212. package/node_modules/checkride/dist/qa/dead.d.ts +39 -0
  213. package/node_modules/checkride/dist/qa/dead.d.ts.map +1 -0
  214. package/node_modules/checkride/dist/qa/dead.js +88 -0
  215. package/node_modules/checkride/dist/qa/dead.js.map +1 -0
  216. package/node_modules/checkride/dist/qa/dupes.d.ts +45 -0
  217. package/node_modules/checkride/dist/qa/dupes.d.ts.map +1 -0
  218. package/node_modules/checkride/dist/qa/dupes.js +62 -0
  219. package/node_modules/checkride/dist/qa/dupes.js.map +1 -0
  220. package/node_modules/checkride/dist/qa/health.d.ts +85 -0
  221. package/node_modules/checkride/dist/qa/health.d.ts.map +1 -0
  222. package/node_modules/checkride/dist/qa/health.js +109 -0
  223. package/node_modules/checkride/dist/qa/health.js.map +1 -0
  224. package/node_modules/checkride/dist/qa/index.d.ts +23 -0
  225. package/node_modules/checkride/dist/qa/index.d.ts.map +1 -0
  226. package/node_modules/checkride/dist/qa/index.js +22 -0
  227. package/node_modules/checkride/dist/qa/index.js.map +1 -0
  228. package/node_modules/checkride/dist/qa/mutation.d.ts +80 -0
  229. package/node_modules/checkride/dist/qa/mutation.d.ts.map +1 -0
  230. package/node_modules/checkride/dist/qa/mutation.js +195 -0
  231. package/node_modules/checkride/dist/qa/mutation.js.map +1 -0
  232. package/node_modules/checkride/dist/qa/qa.d.ts +71 -0
  233. package/node_modules/checkride/dist/qa/qa.d.ts.map +1 -0
  234. package/node_modules/checkride/dist/qa/qa.js +158 -0
  235. package/node_modules/checkride/dist/qa/qa.js.map +1 -0
  236. package/node_modules/checkride/dist/qa/read.d.ts +40 -0
  237. package/node_modules/checkride/dist/qa/read.d.ts.map +1 -0
  238. package/node_modules/checkride/dist/qa/read.js +47 -0
  239. package/node_modules/checkride/dist/qa/read.js.map +1 -0
  240. package/node_modules/checkride/dist/qa/render.d.ts +26 -0
  241. package/node_modules/checkride/dist/qa/render.d.ts.map +1 -0
  242. package/node_modules/checkride/dist/qa/render.js +310 -0
  243. package/node_modules/checkride/dist/qa/render.js.map +1 -0
  244. package/node_modules/checkride/dist/recover.d.ts +48 -0
  245. package/node_modules/checkride/dist/recover.d.ts.map +1 -0
  246. package/node_modules/checkride/dist/recover.js +220 -0
  247. package/node_modules/checkride/dist/recover.js.map +1 -0
  248. package/node_modules/checkride/dist/security.d.ts +45 -0
  249. package/node_modules/checkride/dist/security.d.ts.map +1 -0
  250. package/node_modules/checkride/dist/security.js +86 -0
  251. package/node_modules/checkride/dist/security.js.map +1 -0
  252. package/node_modules/checkride/dist/smoke.d.ts +6 -6
  253. package/node_modules/checkride/dist/smoke.js +6 -6
  254. package/node_modules/checkride/dist/snippets.d.ts +27 -30
  255. package/node_modules/checkride/dist/snippets.d.ts.map +1 -1
  256. package/node_modules/checkride/dist/snippets.js +26 -30
  257. package/node_modules/checkride/dist/snippets.js.map +1 -1
  258. package/node_modules/checkride/dist/tool-json.d.ts +33 -0
  259. package/node_modules/checkride/dist/tool-json.d.ts.map +1 -0
  260. package/node_modules/checkride/dist/tool-json.js +58 -0
  261. package/node_modules/checkride/dist/tool-json.js.map +1 -0
  262. package/node_modules/checkride/dist/triage/cli.d.ts +21 -0
  263. package/node_modules/checkride/dist/triage/cli.d.ts.map +1 -0
  264. package/node_modules/checkride/dist/triage/cli.js +26 -0
  265. package/node_modules/checkride/dist/triage/cli.js.map +1 -0
  266. package/node_modules/checkride/dist/triage/doctor-fold.d.ts +51 -0
  267. package/node_modules/checkride/dist/triage/doctor-fold.d.ts.map +1 -0
  268. package/node_modules/checkride/dist/triage/doctor-fold.js +88 -0
  269. package/node_modules/checkride/dist/triage/doctor-fold.js.map +1 -0
  270. package/node_modules/checkride/dist/triage/env.d.ts +54 -0
  271. package/node_modules/checkride/dist/triage/env.d.ts.map +1 -0
  272. package/node_modules/checkride/dist/triage/env.js +81 -0
  273. package/node_modules/checkride/dist/triage/env.js.map +1 -0
  274. package/node_modules/checkride/dist/triage/gate.d.ts +61 -0
  275. package/node_modules/checkride/dist/triage/gate.d.ts.map +1 -0
  276. package/node_modules/checkride/dist/triage/gate.js +121 -0
  277. package/node_modules/checkride/dist/triage/gate.js.map +1 -0
  278. package/node_modules/checkride/dist/triage/index.d.ts +22 -0
  279. package/node_modules/checkride/dist/triage/index.d.ts.map +1 -0
  280. package/node_modules/checkride/dist/triage/index.js +20 -0
  281. package/node_modules/checkride/dist/triage/index.js.map +1 -0
  282. package/node_modules/checkride/dist/triage/render.d.ts +19 -0
  283. package/node_modules/checkride/dist/triage/render.d.ts.map +1 -0
  284. package/node_modules/checkride/dist/triage/render.js +419 -0
  285. package/node_modules/checkride/dist/triage/render.js.map +1 -0
  286. package/node_modules/checkride/dist/triage/triage.d.ts +83 -0
  287. package/node_modules/checkride/dist/triage/triage.d.ts.map +1 -0
  288. package/node_modules/checkride/dist/triage/triage.js +131 -0
  289. package/node_modules/checkride/dist/triage/triage.js.map +1 -0
  290. package/node_modules/checkride/package.json +18 -15
  291. package/node_modules/checkride/schema/checkride.config.schema.json +70 -5
  292. package/node_modules/checkride/schema/checkride.summary.schema.json +1 -1
  293. package/node_modules/checkride/skills/check/SKILL.md +265 -0
  294. package/node_modules/checkride/skills/qa/SKILL.md +231 -0
  295. package/node_modules/checkride/templates/shared/cspell.json +1 -0
  296. package/node_modules/checkride/templates/shared/oxlintrc.json +1 -1
  297. package/node_modules/checkride/templates/shared/styles/Repo/Drift.yml +41 -0
  298. package/node_modules/checkride/templates/shared/styles/Repo/Latin.yml +14 -0
  299. package/node_modules/checkride/templates/shared/styles/Repo/LyHyphen.yml +28 -0
  300. package/node_modules/checkride/templates/shared/styles/Repo/Minted.yml +33 -0
  301. package/node_modules/checkride/templates/shared/styles/Repo/ThereIs.yml +16 -0
  302. package/node_modules/checkride/templates/shared/styles/Repo/Weasel.yml +26 -0
  303. package/node_modules/checkride/templates/shared/vale.ini +34 -0
  304. package/package.json +10 -9
  305. package/skills/build/SKILL.md +85 -76
  306. package/skills/doctor/SKILL.md +11 -11
  307. package/skills/finish/SKILL.md +21 -18
  308. package/skills/harvest/SKILL.md +13 -11
  309. package/skills/park/SKILL.md +10 -10
  310. package/skills/plan/SKILL.md +69 -59
  311. package/skills/recover/SKILL.md +28 -0
  312. package/skills/refine/SKILL.md +21 -18
  313. package/skills/revert/SKILL.md +6 -6
  314. package/skills/spike/SKILL.md +9 -9
  315. package/skills/status/SKILL.md +5 -5
  316. package/skills/step/SKILL.md +20 -15
  317. package/skills/verify/SKILL.md +56 -46
  318. package/templates/build-log.md +7 -7
  319. package/templates/intent.md +52 -41
  320. package/templates/spike-report.md +10 -10
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The `.check/summary.json` read — the one place either reader parses it.
3
+ *
4
+ * The summary is checkride's promised index: `schema_version`-ed, additive-only,
5
+ * deterministically ordered and crash-consistent. This module turns it into a
6
+ * five-state result rather than a value-or-throw, because every failure mode is
7
+ * something a reader has to *report* — a missing summary means the gate never
8
+ * wrote one, and a `schema_version` this reader does not understand means stop,
9
+ * not guess.
10
+ *
11
+ * `foreign` and `schema-mismatch` are deliberately separate states even though
12
+ * both end in "this reader cannot use the file". An absent `schema_version` is
13
+ * evidence the file was not written by checkride at all — a repo whose gate is
14
+ * a homegrown script that happens to write `.check/` — and the honest report
15
+ * there is "your gate is not checkride", not "STOP, version boundary". A
16
+ * `schema_version` of 2 is the opposite case: checkride wrote it, and a newer
17
+ * one, so guessing across the boundary is what has to stop.
18
+ *
19
+ * Validation here is deliberately structural, not schematic: the full JSON
20
+ * Schema lives in `schema/checkride.summary.schema.json` and is the engine's
21
+ * job. All this needs is enough narrowing that the fields the readers touch are
22
+ * the types they claim to be, with no cast onto unvalidated bytes.
23
+ *
24
+ * The `../artifacts` barrel is this module's only public surface.
25
+ */
26
+ import type { Summary } from '../orchestrator.js';
27
+ /** The gitignored directory every run writes its artifacts into. */
28
+ export declare const CHECK_DIR = ".check";
29
+ /**
30
+ * The only `schema_version` these readers understand.
31
+ *
32
+ * Deliberately a literal, not the engine's `SCHEMA_VERSION` import: the
33
+ * additive-only guarantee holds *within* a version and promises nothing across
34
+ * one, so a bump has to stop the reader loudly rather than be silently
35
+ * followed. `artifacts.test.ts` asserts this equals the engine's constant, so a
36
+ * future bump turns a test red — a deliberate update, never a quiet change of
37
+ * behavior.
38
+ */
39
+ export declare const SUPPORTED_SCHEMA_VERSION = 1;
40
+ /** The outcome of reading `.check/summary.json`. Every state is reportable. */
41
+ export type SummaryRead = {
42
+ state: 'ok';
43
+ path: string;
44
+ mtimeMs: number;
45
+ summary: Summary;
46
+ }
47
+ /** A JSON object with no `schema_version` at all — not checkride's file. */
48
+ | {
49
+ state: 'foreign';
50
+ path: string;
51
+ mtimeMs: number;
52
+ } | {
53
+ state: 'schema-mismatch';
54
+ path: string;
55
+ mtimeMs: number;
56
+ found: unknown;
57
+ } | {
58
+ state: 'unreadable';
59
+ path: string;
60
+ detail: string;
61
+ } | {
62
+ state: 'missing';
63
+ path: string;
64
+ };
65
+ /** Classify already-read bytes. Split out so tests can drive every state. */
66
+ export declare function parseSummary(raw: string, path: string, mtimeMs: number): SummaryRead;
67
+ /** Read and classify `<cwd>/.check/summary.json`. Never throws. */
68
+ export declare function readSummary(cwd: string): Promise<SummaryRead>;
69
+ //# sourceMappingURL=summary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../../src/artifacts/summary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAgB,MAAM,oBAAoB,CAAC;AAIhE,oEAAoE;AACpE,eAAO,MAAM,SAAS,WAAW,CAAC;AAKlC;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C,+EAA+E;AAC/E,MAAM,MAAM,WAAW,GACnB;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE;AAClE,4EAA4E;GAC1E;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,KAAK,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC3E;IAAE,KAAK,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AA8CvC,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,CAcpF;AAED,mEAAmE;AACnE,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAQnE"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The `.check/summary.json` read — the one place either reader parses it.
3
+ *
4
+ * The summary is checkride's promised index: `schema_version`-ed, additive-only,
5
+ * deterministically ordered and crash-consistent. This module turns it into a
6
+ * five-state result rather than a value-or-throw, because every failure mode is
7
+ * something a reader has to *report* — a missing summary means the gate never
8
+ * wrote one, and a `schema_version` this reader does not understand means stop,
9
+ * not guess.
10
+ *
11
+ * `foreign` and `schema-mismatch` are deliberately separate states even though
12
+ * both end in "this reader cannot use the file". An absent `schema_version` is
13
+ * evidence the file was not written by checkride at all — a repo whose gate is
14
+ * a homegrown script that happens to write `.check/` — and the honest report
15
+ * there is "your gate is not checkride", not "STOP, version boundary". A
16
+ * `schema_version` of 2 is the opposite case: checkride wrote it, and a newer
17
+ * one, so guessing across the boundary is what has to stop.
18
+ *
19
+ * Validation here is deliberately structural, not schematic: the full JSON
20
+ * Schema lives in `schema/checkride.summary.schema.json` and is the engine's
21
+ * job. All this needs is enough narrowing that the fields the readers touch are
22
+ * the types they claim to be, with no cast onto unvalidated bytes.
23
+ *
24
+ * The `../artifacts` barrel is this module's only public surface.
25
+ */
26
+ import { readFile, stat } from 'node:fs/promises';
27
+ import { join } from 'node:path';
28
+ import { isRecord, parseJson } from './json.js';
29
+ /** The gitignored directory every run writes its artifacts into. */
30
+ export const CHECK_DIR = '.check';
31
+ /** The aggregate report, beside the raw artifacts under {@link CHECK_DIR}. */
32
+ const SUMMARY_FILE = 'summary.json';
33
+ /**
34
+ * The only `schema_version` these readers understand.
35
+ *
36
+ * Deliberately a literal, not the engine's `SCHEMA_VERSION` import: the
37
+ * additive-only guarantee holds *within* a version and promises nothing across
38
+ * one, so a bump has to stop the reader loudly rather than be silently
39
+ * followed. `artifacts.test.ts` asserts this equals the engine's constant, so a
40
+ * future bump turns a test red — a deliberate update, never a quiet change of
41
+ * behavior.
42
+ */
43
+ export const SUPPORTED_SCHEMA_VERSION = 1;
44
+ /**
45
+ * Name, adapter and description are present and the right type. Bracket access
46
+ * throughout: these are unvalidated bytes, and reading them as a dynamic record
47
+ * is what they are.
48
+ */
49
+ function hasCheckIdentity(value) {
50
+ return (typeof value['name'] === 'string' &&
51
+ typeof value['description'] === 'string' &&
52
+ (typeof value['adapter'] === 'string' || value['adapter'] === null));
53
+ }
54
+ /** The result fields the readers render are present and the right type. */
55
+ function hasCheckOutcome(value) {
56
+ return (typeof value['ok'] === 'boolean' &&
57
+ typeof value['duration_ms'] === 'number' &&
58
+ (typeof value['exit_code'] === 'number' || value['exit_code'] === null) &&
59
+ (typeof value['output_file'] === 'string' || value['output_file'] === null));
60
+ }
61
+ /**
62
+ * One `checks[]` entry. The optional additive fields (`skipped`, `reason`,
63
+ * `baselined`) are read through `??` at the point of use, so they need no guard.
64
+ */
65
+ function isCheck(value) {
66
+ return isRecord(value) && hasCheckIdentity(value) && hasCheckOutcome(value);
67
+ }
68
+ /** Every required top-level field, and a well-formed `checks[]`. */
69
+ function isSummary(value) {
70
+ const checks = value['checks'];
71
+ return (typeof value['timestamp'] === 'string' &&
72
+ typeof value['ok'] === 'boolean' &&
73
+ typeof value['checks_run'] === 'number' &&
74
+ typeof value['total_duration_ms'] === 'number' &&
75
+ Array.isArray(checks) &&
76
+ checks.every(isCheck));
77
+ }
78
+ /** Classify already-read bytes. Split out so tests can drive every state. */
79
+ export function parseSummary(raw, path, mtimeMs) {
80
+ const value = parseJson(raw);
81
+ if (!isRecord(value))
82
+ return { state: 'unreadable', path, detail: 'not a JSON object' };
83
+ // Provenance first, then version: a summary this reader cannot read is a
84
+ // whose-file-is-this problem or a version problem, not a shape problem, even
85
+ // when its other fields also fail to narrow. Key presence rather than value,
86
+ // so an explicit `"schema_version": null` stays a mismatch — a malformed
87
+ // checkride summary — instead of being read as a stranger's file.
88
+ if (!('schema_version' in value))
89
+ return { state: 'foreign', path, mtimeMs };
90
+ if (value['schema_version'] !== SUPPORTED_SCHEMA_VERSION) {
91
+ return { state: 'schema-mismatch', path, mtimeMs, found: value['schema_version'] };
92
+ }
93
+ if (!isSummary(value))
94
+ return { state: 'unreadable', path, detail: 'missing or mistyped required field(s)' };
95
+ return { state: 'ok', path, mtimeMs, summary: value };
96
+ }
97
+ /** Read and classify `<cwd>/.check/summary.json`. Never throws. */
98
+ export async function readSummary(cwd) {
99
+ const path = join(cwd, CHECK_DIR, SUMMARY_FILE);
100
+ try {
101
+ const [stats, raw] = await Promise.all([stat(path), readFile(path, 'utf8')]);
102
+ return parseSummary(raw, path, stats.mtimeMs);
103
+ }
104
+ catch {
105
+ return { state: 'missing', path };
106
+ }
107
+ }
108
+ //# sourceMappingURL=summary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summary.js","sourceRoot":"","sources":["../../src/artifacts/summary.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,21 @@
8
8
  * version or the new complete version — `rename(2)` is atomic within a
9
9
  * directory — never torn.
10
10
  */
11
- /** Write `data` to `path` atomically: temp sibling first, then rename over. */
11
+ /**
12
+ * Write `data` to `path` atomically: temp sibling, flushed to disk, then
13
+ * renamed over.
14
+ *
15
+ * The `fsync` before the rename is what makes the promise above true rather
16
+ * than nearly true. `rename(2)` is atomic for the *name*, but a write that is
17
+ * still sitting in the page cache when the machine loses power can leave the
18
+ * renamed inode holding nothing — the reader then finds a file that exists and
19
+ * is empty, which is the torn read this exists to prevent, just relocated.
20
+ *
21
+ * The containing directory is deliberately not synced. Without that, the
22
+ * rename itself may not survive a crash — but the reader then sees the
23
+ * *previous complete version*, which is exactly what is promised. Paying a
24
+ * second fsync per artifact to also guarantee which of the two you get is not
25
+ * worth it for a report that the next run rewrites anyway.
26
+ */
12
27
  export declare function writeFileAtomic(path: string, data: string): Promise<void>;
13
28
  //# sourceMappingURL=atomic.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"atomic.d.ts","sourceRoot":"","sources":["../src/atomic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,+EAA+E;AAC/E,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/E"}
1
+ {"version":3,"file":"atomic.d.ts","sourceRoot":"","sources":["../src/atomic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe/E"}
@@ -8,12 +8,34 @@
8
8
  * version or the new complete version — `rename(2)` is atomic within a
9
9
  * directory — never torn.
10
10
  */
11
- import { rename, rm, writeFile } from 'node:fs/promises';
12
- /** Write `data` to `path` atomically: temp sibling first, then rename over. */
11
+ import { open, rename, rm } from 'node:fs/promises';
12
+ /**
13
+ * Write `data` to `path` atomically: temp sibling, flushed to disk, then
14
+ * renamed over.
15
+ *
16
+ * The `fsync` before the rename is what makes the promise above true rather
17
+ * than nearly true. `rename(2)` is atomic for the *name*, but a write that is
18
+ * still sitting in the page cache when the machine loses power can leave the
19
+ * renamed inode holding nothing — the reader then finds a file that exists and
20
+ * is empty, which is the torn read this exists to prevent, just relocated.
21
+ *
22
+ * The containing directory is deliberately not synced. Without that, the
23
+ * rename itself may not survive a crash — but the reader then sees the
24
+ * *previous complete version*, which is exactly what is promised. Paying a
25
+ * second fsync per artifact to also guarantee which of the two you get is not
26
+ * worth it for a report that the next run rewrites anyway.
27
+ */
13
28
  export async function writeFileAtomic(path, data) {
14
29
  const tmp = `${path}.${process.pid}.tmp`;
15
30
  try {
16
- await writeFile(tmp, data);
31
+ const handle = await open(tmp, 'w');
32
+ try {
33
+ await handle.writeFile(data);
34
+ await handle.sync();
35
+ }
36
+ finally {
37
+ await handle.close();
38
+ }
17
39
  await rename(tmp, path);
18
40
  }
19
41
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"atomic.js","sourceRoot":"","sources":["../src/atomic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAEzD,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAY,EAAE,IAAY;IAC9D,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,MAAM,CAAC;IACzC,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC3B,MAAM,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/B,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"atomic.js","sourceRoot":"","sources":["../src/atomic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAY,EAAE,IAAY;IAC9D,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,MAAM,CAAC;IACzC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACpC,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;YAC7B,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACtB,CAAC;gBAAS,CAAC;YACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;QACD,MAAM,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/B,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -22,7 +22,7 @@
22
22
  */
23
23
  import type { Fingerprint } from './fingerprint.js';
24
24
  /**
25
- * Fallow's fingerprint extractor (baseline part 1), for the extractor registry.
25
+ * Fallow's fingerprint extractor, for the extractor registry.
26
26
  * Returns the finding keys for a recognized, readable report and an empty set
27
27
  * for anything else — the "never throw, empty on malformed" contract every
28
28
  * extractor honours. The *gating* verdict (`fallowVerdict`) is where an
@@ -55,10 +55,14 @@ export type FallowVerdict = {
55
55
  * fallow found anything. With a baseline, findings are masked — the slot passes
56
56
  * when every current finding is grandfathered and fails on any new one.
57
57
  *
58
- * A report that parses to *fewer* fingerprints than its issue count has findings
59
- * that can't be individually grandfathered; a baseline must never mask such a
60
- * slot to green, so it stays at the raw findings-based verdict. Correctness over
61
- * precision: better to keep re-surfacing a finding than to silently pass one.
58
+ * A report whose keys don't map one-per-finding onto its issue count — an
59
+ * un-keyable finding, or a key produced for something the count doesn't include
60
+ * — has findings that can't be individually grandfathered; a baseline must never
61
+ * mask such a slot to green, so it stays at the raw findings-based verdict. A key
62
+ * *collision* (two findings sharing a key) is not un-keyable — both are keyed, so
63
+ * the baseline coarsens (grandfathering one grandfathers both) but stays active.
64
+ * Correctness over precision: better to keep re-surfacing a finding than to
65
+ * silently pass one.
62
66
  */
63
67
  export declare function fallowVerdict(raw: string, baselineKeys: readonly string[] | null): FallowVerdict;
64
68
  //# sourceMappingURL=fallow.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fallow.d.ts","sourceRoot":"","sources":["../../src/baseline/fallow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAmMpD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAGvD;AAED,gFAAgF;AAChF,MAAM,MAAM,aAAa,GAAG;IAC1B,2CAA2C;IAC3C,EAAE,EAAE,OAAO,CAAC;IACZ,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gEAAgE;IAChE,QAAQ,EAAE,WAAW,CAAC;IACtB;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB,4EAA4E;IAC5E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,GAAG,aAAa,CAuBhG"}
1
+ {"version":3,"file":"fallow.d.ts","sourceRoot":"","sources":["../../src/baseline/fallow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAgTpD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAGvD;AAED,gFAAgF;AAChF,MAAM,MAAM,aAAa,GAAG;IAC1B,2CAA2C;IAC3C,EAAE,EAAE,OAAO,CAAC;IACZ,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gEAAgE;IAChE,QAAQ,EAAE,WAAW,CAAC;IACtB;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB,4EAA4E;IAC5E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,GAAG,aAAa,CA0BhG"}
@@ -20,12 +20,14 @@
20
20
  * schema 4 with a different layout and is no longer supported). Each analysis
21
21
  * kind has its own layout, so parsing dispatches on the top-level `kind`.
22
22
  */
23
+ import { parseToolJson } from '../tool-json.js';
23
24
  import { applyBaseline } from './store.js';
24
25
  /**
25
- * Minimum fallow JSON schema version checkride understands. fallow 3.5.0 and
26
- * 3.6.0 both emit schema_version 7; 2.x emitted 4 with an incompatible layout.
27
- * A report below this floor — or one whose kind/shape we can't read — is a
28
- * hard failure, never a silent pass.
26
+ * Minimum fallow JSON schema version checkride understands. Every fallow from
27
+ * 3.5.0 through 3.9.1 emits schema_version 7 (verified: dead-code, dupes, and
28
+ * health all report 7, with identical top-level layouts); 2.x emitted 4 with an
29
+ * incompatible layout. A report below this floor — or one whose kind/shape we
30
+ * can't read — is a hard failure, never a silent pass.
29
31
  */
30
32
  const FALLOW_SCHEMA_MIN = 7;
31
33
  /** Narrow to a plain object (arrays and null excluded). */
@@ -37,32 +39,74 @@ function str(value) {
37
39
  return typeof value === 'string' ? value.trim() : '';
38
40
  }
39
41
  /**
40
- * Fingerprint key for one dead-code finding: `dead-code:<category>:<identity>`,
41
- * where identity is a file and/or symbol (never a line or column, so the key
42
- * survives edits that only move the code). Cross-file findings (cycles) key on
43
- * the sorted set of files. Returns `null` when no stable identity can be read —
44
- * the finding still counts toward `issueCount`, it just can't be individually
45
- * grandfathered (which keeps the slot conservatively red, never silently green).
42
+ * Fields naming the declaration that owns a member-scoped finding, in priority
43
+ * order. fallow reports an unused class/enum/store member as `parent_name` +
44
+ * `member_name`, a component binding as `component_name` + `prop_name` /
45
+ * `input_name` / `output_name` / `emit_name` / `event_name`, and a catalog entry
46
+ * as `catalog_name` + `entry_name`. Both halves are needed: the leaf alone is
47
+ * ambiguous (two classes in one file can each have a `render`), and the
48
+ * container alone collapses every member of one declaration onto one key.
46
49
  */
47
- /** Finding fields that carry a single stable symbol identity, in priority order. */
50
+ const CONTAINER_FIELDS = ['parent_name', 'component_name', 'catalog_name'];
51
+ /** Finding fields that carry a stable leaf symbol, in priority order. */
48
52
  const SYMBOL_FIELDS = [
49
53
  'export_name',
50
54
  'name',
51
55
  'type_name',
52
56
  'member',
57
+ 'member_name',
58
+ 'prop_name',
59
+ 'emit_name',
60
+ 'input_name',
61
+ 'output_name',
62
+ 'event_name',
63
+ 'entry_name',
64
+ 'action_name',
65
+ 'key_name',
53
66
  'package_name',
54
67
  'dependency',
55
68
  'specifier',
56
69
  ];
57
- /** The first non-empty symbol field on a finding, or `''` when none is present. */
58
- function symbolIdentity(item) {
59
- for (const field of SYMBOL_FIELDS) {
70
+ /** The first non-empty field of `fields` on a finding, or `''` when none is set. */
71
+ function firstField(item, fields) {
72
+ for (const field of fields) {
60
73
  const value = str(item[field]);
61
74
  if (value)
62
75
  return value;
63
76
  }
64
77
  return '';
65
78
  }
79
+ /**
80
+ * A finding's stable symbol identity: `<container>.<leaf>` when it carries both
81
+ * (`Svc.unusedOne`), otherwise whichever half it has, otherwise `''`. Composing
82
+ * the two is what keeps a member-scoped finding line-free — with neither half
83
+ * recognized such a finding has no symbol at all and falls back to
84
+ * `<path>:<line>:<col>`, a key that moves whenever code above it does, so a
85
+ * grandfathered finding resurfaces as new on the next unrelated edit.
86
+ */
87
+ function symbolIdentity(item) {
88
+ return [firstField(item, CONTAINER_FIELDS), firstField(item, SYMBOL_FIELDS)].filter(Boolean).join('.');
89
+ }
90
+ /**
91
+ * A finding's `line[:col]` position, or `''` when it carries neither. Only used
92
+ * to disambiguate findings that share every stable identity field (see
93
+ * {@link withPosition}); a keyable finding never falls back to this.
94
+ */
95
+ function position(item) {
96
+ return [item['line'], item['col']].filter((n) => typeof n === 'number').join(':');
97
+ }
98
+ /** Append `:<line>[:<col>]` to a key when the finding has a position, else leave it. */
99
+ function withPosition(key, item) {
100
+ const pos = position(item);
101
+ return pos ? `${key}:${pos}` : key;
102
+ }
103
+ /**
104
+ * A function name that is a real, stable identifier (kept line-free so its key
105
+ * survives edits above it). Fallow wraps names it can't resolve to an identity
106
+ * in angle brackets (`<arrow>`, `<anonymous>`); those fail this test and get a
107
+ * position appended, since every such placeholder in a file is otherwise equal.
108
+ */
109
+ const STABLE_IDENTIFIER = /^[A-Za-z_$][\w$.]*$/;
66
110
  /**
67
111
  * Key a cross-file finding (circular_dependencies, re_export_cycles) on its
68
112
  * sorted file membership — a list of files/segments rather than one location.
@@ -82,28 +126,78 @@ function cycleKey(category, item) {
82
126
  }
83
127
  return null;
84
128
  }
129
+ /**
130
+ * Fingerprint key for one dead-code finding: `dead-code:<category>:<identity>`,
131
+ * where identity is a file and symbol (never a line or column, so the key
132
+ * survives edits that only move the code). When a finding has a file but no
133
+ * symbol — `stale_suppressions`, `policy_violations` and the boundary
134
+ * categories carry only a path — the path alone would collide with its siblings
135
+ * in the same file, so the position is appended to keep them distinct, at the
136
+ * cost of a key that moves. Cross-file findings (cycles) key on the
137
+ * sorted set of files. Returns `null` when no stable identity can be read — the
138
+ * finding still counts toward `issueCount`, it just can't be individually
139
+ * grandfathered (which keeps the slot conservatively red, never silently green).
140
+ */
85
141
  function deadKey(category, item) {
86
142
  if (!isPlainObject(item))
87
143
  return null;
88
144
  const file = str(item['path']) || str(item['file']);
89
145
  const symbol = symbolIdentity(item);
90
- if (file || symbol) {
91
- const identity = [file, symbol].filter(Boolean).join(':');
92
- return `dead-code:${category}:${identity}`;
93
- }
146
+ if (symbol)
147
+ return `dead-code:${category}:${[file, symbol].filter(Boolean).join(':')}`;
148
+ if (file)
149
+ return withPosition(`dead-code:${category}:${file}`, item);
94
150
  return cycleKey(category, item);
95
151
  }
152
+ /**
153
+ * Top-level arrays that are NOT counted in `total_issues`, so keying them would
154
+ * push the key count past the issue count and leave the two out of step: the
155
+ * advisory `next_steps`, the project-level `workspace_diagnostics`, and the
156
+ * three opt-in "health signal" rules that fallow's own `issue-registry.json`
157
+ * marks `counts_in_total: false`. They have to be skipped by name rather than
158
+ * left to {@link deadKey} — `workspace_diagnostics` carries a `path`, and
159
+ * `thin_wrappers` and `duplicate_prop_shapes` a `file`, so all three key
160
+ * readily. Verified against fallow 3.9.1's registry; every other array counts.
161
+ */
162
+ const UNCOUNTED_ARRAYS = new Set([
163
+ 'next_steps',
164
+ 'workspace_diagnostics',
165
+ 'prop_drilling_chains',
166
+ 'thin_wrappers',
167
+ 'duplicate_prop_shapes',
168
+ ]);
169
+ /**
170
+ * Key every counted finding in a dead-code report. Returns the keys — duplicates
171
+ * intact, so a collision stays visible to the caller's tracking check — and how
172
+ * many items carried no stable identity to key on.
173
+ */
174
+ function collectDeadKeys(j) {
175
+ const keys = [];
176
+ let unkeyed = 0;
177
+ for (const [category, value] of Object.entries(j)) {
178
+ if (!Array.isArray(value) || UNCOUNTED_ARRAYS.has(category))
179
+ continue;
180
+ for (const item of value) {
181
+ const key = deadKey(category, item);
182
+ if (key === null)
183
+ unkeyed += 1;
184
+ else
185
+ keys.push(key);
186
+ }
187
+ }
188
+ return { keys, unkeyed };
189
+ }
96
190
  /**
97
191
  * Extract dead-code findings and the authoritative issue count (`total_issues`).
98
192
  * Findings come from the top-level detail arrays (`unused_exports`,
99
193
  * `unused_dev_dependencies`, `circular_dependencies`, …) rather than the summary
100
- * keys: the summary aggregates a few categories under one name (e.g. dev and
194
+ * keys: the summary aggregates a few categories under one name (for example dev and
101
195
  * optional deps both count as `unused_dependencies`) whose items live in
102
- * separately-named arrays, so keying off the summary would miss them. Iterating
103
- * every array and letting {@link deadKey} return `null` for non-finding arrays
104
- * (`next_steps`) covers whatever categories fallow reports — including ones added
105
- * in a future minor — without a hand-maintained list. A category whose items
106
- * carry no stable identity simply isn't fingerprinted; it still counts toward
196
+ * separately named arrays, so keying off the summary would miss them. Iterating
197
+ * every array except the {@link UNCOUNTED_ARRAYS} covers whatever categories
198
+ * fallow reports — including ones added in a future minor — without a
199
+ * hand-maintained list of the ones that do count. A category whose items carry
200
+ * no stable identity simply isn't fingerprinted; it still counts toward
107
201
  * `total_issues`, so the slot stays red (never masked) until it's fixed.
108
202
  */
109
203
  function parseDeadCode(j) {
@@ -112,17 +206,17 @@ function parseDeadCode(j) {
112
206
  if (typeof total !== 'number') {
113
207
  return { ok: false, reason: 'fallow dead-code JSON missing summary.total_issues' };
114
208
  }
115
- const findings = [];
116
- for (const [category, value] of Object.entries(j)) {
117
- if (!Array.isArray(value))
118
- continue;
119
- for (const item of value) {
120
- const key = deadKey(category, item);
121
- if (key !== null)
122
- findings.push(key);
123
- }
124
- }
125
- return { ok: true, kind: 'dead-code', findings, issueCount: total };
209
+ const { keys, unkeyed } = collectDeadKeys(j);
210
+ // Two independent ways the keys can fail to cover the findings, both required
211
+ // because either alone has a blind spot. Counting the items we could not key
212
+ // is direct and exact. Comparing against the authoritative `total` catches
213
+ // findings the iteration never reached at all (nothing to count) — and cannot
214
+ // be substituted for the direct count, because a key produced for something
215
+ // `total` omits would otherwise offset a real un-keyable finding back to zero.
216
+ // `keys.length` is taken BEFORE the Set collapses collisions, so a collision
217
+ // still reads as fully tracked.
218
+ const fullyTracked = unkeyed === 0 && keys.length === total;
219
+ return { ok: true, kind: 'dead-code', findings: keys, issueCount: total, fullyTracked };
126
220
  }
127
221
  /**
128
222
  * Extract duplication findings. Each clone group carries a content-hash
@@ -142,12 +236,14 @@ function parseDupes(j) {
142
236
  if (fp)
143
237
  findings.push(`dupes:${fp}`);
144
238
  }
145
- return { ok: true, kind: 'dupes', findings, issueCount: groups.length };
239
+ return { ok: true, kind: 'dupes', findings, issueCount: groups.length, fullyTracked: findings.length === groups.length };
146
240
  }
147
241
  /**
148
242
  * Extract health (complexity) findings — one per function over threshold, keyed
149
- * on file + function name (no line, so the key survives edits above it). The
150
- * issue count is the number of findings.
243
+ * on file + function name. A named function stays line-free so its key survives
244
+ * edits above it; an anonymous placeholder (`<arrow>`, `<anonymous>`) shares its
245
+ * name with every sibling in the file, so it gets a position appended to stay
246
+ * distinct. The issue count is the number of findings.
151
247
  */
152
248
  function parseHealth(j) {
153
249
  const findings = j['findings'];
@@ -158,16 +254,12 @@ function parseHealth(j) {
158
254
  for (const f of findings) {
159
255
  if (!isPlainObject(f))
160
256
  continue;
161
- keys.push(`health:${str(f['path'])}:${str(f['name'])}`);
257
+ const name = str(f['name']);
258
+ const base = `health:${str(f['path'])}:${name}`;
259
+ keys.push(STABLE_IDENTIFIER.test(name) ? base : withPosition(base, f));
162
260
  }
163
- return { ok: true, kind: 'health', findings: keys, issueCount: findings.length };
261
+ return { ok: true, kind: 'health', findings: keys, issueCount: findings.length, fullyTracked: keys.length === findings.length };
164
262
  }
165
- /**
166
- * Parse a fallow report into findings + an issue count, or the reason it could
167
- * not be trusted. Every failure path (bad JSON, missing/old schema, unknown
168
- * kind, missing count field) is a *loud* failure the caller turns into a red
169
- * slot — checkride never treats an unreadable fallow report as "clean".
170
- */
171
263
  /**
172
264
  * The reason a report's schema can't be trusted, or `null` when it is a
173
265
  * supported version. Split out so {@link parseFallow} stays a thin dispatcher.
@@ -181,16 +273,20 @@ function fallowSchemaError(j) {
181
273
  }
182
274
  return null;
183
275
  }
276
+ /**
277
+ * Parse a fallow report into findings + an issue count, or the reason it could
278
+ * not be trusted. Every failure path (bad JSON, missing/old schema, unknown
279
+ * kind, missing count field) is a *loud* failure the caller turns into a red
280
+ * slot — checkride never treats an unreadable fallow report as "clean".
281
+ */
184
282
  function parseFallow(raw) {
185
283
  if (raw.trim() === '')
186
284
  return { ok: false, reason: 'fallow produced no JSON output' };
187
- let j;
188
- try {
189
- j = JSON.parse(raw);
190
- }
191
- catch {
285
+ // Tolerant of a launcher preamble ahead of the report — see `parseToolJson`.
286
+ const parsed = parseToolJson(raw);
287
+ if (!parsed)
192
288
  return { ok: false, reason: 'fallow did not emit valid JSON' };
193
- }
289
+ const j = parsed.value;
194
290
  if (!isPlainObject(j))
195
291
  return { ok: false, reason: 'fallow output was not a JSON object' };
196
292
  const schemaError = fallowSchemaError(j);
@@ -209,7 +305,7 @@ function parseFallow(raw) {
209
305
  }
210
306
  }
211
307
  /**
212
- * Fallow's fingerprint extractor (baseline part 1), for the extractor registry.
308
+ * Fallow's fingerprint extractor, for the extractor registry.
213
309
  * Returns the finding keys for a recognized, readable report and an empty set
214
310
  * for anything else — the "never throw, empty on malformed" contract every
215
311
  * extractor honours. The *gating* verdict (`fallowVerdict`) is where an
@@ -226,10 +322,14 @@ export function fallowFindings(raw) {
226
322
  * fallow found anything. With a baseline, findings are masked — the slot passes
227
323
  * when every current finding is grandfathered and fails on any new one.
228
324
  *
229
- * A report that parses to *fewer* fingerprints than its issue count has findings
230
- * that can't be individually grandfathered; a baseline must never mask such a
231
- * slot to green, so it stays at the raw findings-based verdict. Correctness over
232
- * precision: better to keep re-surfacing a finding than to silently pass one.
325
+ * A report whose keys don't map one-per-finding onto its issue count — an
326
+ * un-keyable finding, or a key produced for something the count doesn't include
327
+ * — has findings that can't be individually grandfathered; a baseline must never
328
+ * mask such a slot to green, so it stays at the raw findings-based verdict. A key
329
+ * *collision* (two findings sharing a key) is not un-keyable — both are keyed, so
330
+ * the baseline coarsens (grandfathering one grandfathers both) but stays active.
331
+ * Correctness over precision: better to keep re-surfacing a finding than to
332
+ * silently pass one.
233
333
  */
234
334
  export function fallowVerdict(raw, baselineKeys) {
235
335
  const parsed = parseFallow(raw);
@@ -243,9 +343,12 @@ export function fallowVerdict(raw, baselineKeys) {
243
343
  return { ok: rawOk, baselined: 0, newKeys: [], findings, observed: true, reason: issueReason };
244
344
  }
245
345
  const adj = applyBaseline(findings, baselineKeys, rawOk);
246
- const fullyTracked = findings.size === parsed.issueCount;
247
- // Untracked findings block masking: hold the slot at its raw verdict.
248
- if (!fullyTracked) {
346
+ // Findings the keys don't cover block masking: hold the slot at its raw
347
+ // verdict. A key collision does NOT (both findings are keyed, so the report
348
+ // stays fully tracked), so it coarsens the baseline instead of disabling it. A
349
+ // *deduplicated* size comparison could not tell the two apart, which is what
350
+ // disabled the whole slot on any collision.
351
+ if (!parsed.fullyTracked) {
249
352
  return { ok: rawOk, baselined: adj.baselined, newKeys: adj.newKeys, findings, observed: true, reason: issueReason };
250
353
  }
251
354
  // Fully tracked: a masked slot (all findings grandfathered) is green; a slot