@descryy/core 0.4.0 → 0.5.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 (353) hide show
  1. package/dist/budget/reply.d.ts +11 -46
  2. package/dist/budget/reply.d.ts.map +1 -1
  3. package/dist/budget/reply.js +10 -39
  4. package/dist/budget/reply.js.map +1 -1
  5. package/dist/budget/size.d.ts +2 -21
  6. package/dist/budget/size.d.ts.map +1 -1
  7. package/dist/budget/size.js +3 -24
  8. package/dist/budget/size.js.map +1 -1
  9. package/dist/capabilities/preconditions.d.ts +7 -68
  10. package/dist/capabilities/preconditions.d.ts.map +1 -1
  11. package/dist/capabilities/preconditions.js +6 -57
  12. package/dist/capabilities/preconditions.js.map +1 -1
  13. package/dist/contracts/engine.d.ts +23 -11
  14. package/dist/contracts/engine.d.ts.map +1 -1
  15. package/dist/contracts/engine.js +23 -11
  16. package/dist/contracts/engine.js.map +1 -1
  17. package/dist/contracts/orphans.d.ts +18 -122
  18. package/dist/contracts/orphans.d.ts.map +1 -1
  19. package/dist/contracts/orphans.js +36 -134
  20. package/dist/contracts/orphans.js.map +1 -1
  21. package/dist/contracts/paths.d.ts +4 -114
  22. package/dist/contracts/paths.d.ts.map +1 -1
  23. package/dist/contracts/paths.js +7 -140
  24. package/dist/contracts/paths.js.map +1 -1
  25. package/dist/contracts/shapes.d.ts +13 -66
  26. package/dist/contracts/shapes.d.ts.map +1 -1
  27. package/dist/contracts/shapes.js +20 -91
  28. package/dist/contracts/shapes.js.map +1 -1
  29. package/dist/governance/budget.d.ts +12 -48
  30. package/dist/governance/budget.d.ts.map +1 -1
  31. package/dist/governance/budget.js +12 -48
  32. package/dist/governance/budget.js.map +1 -1
  33. package/dist/governance/candidate-boundary.d.ts +4 -32
  34. package/dist/governance/candidate-boundary.d.ts.map +1 -1
  35. package/dist/governance/candidate-boundary.js +4 -32
  36. package/dist/governance/candidate-boundary.js.map +1 -1
  37. package/dist/governance/escalation.d.ts +7 -45
  38. package/dist/governance/escalation.d.ts.map +1 -1
  39. package/dist/governance/escalation.js +7 -45
  40. package/dist/governance/escalation.js.map +1 -1
  41. package/dist/governance/fact-boundary.d.ts +7 -49
  42. package/dist/governance/fact-boundary.d.ts.map +1 -1
  43. package/dist/governance/fact-boundary.js +5 -43
  44. package/dist/governance/fact-boundary.js.map +1 -1
  45. package/dist/governance/finding-funnel.d.ts +11 -72
  46. package/dist/governance/finding-funnel.d.ts.map +1 -1
  47. package/dist/governance/finding-funnel.js +9 -68
  48. package/dist/governance/finding-funnel.js.map +1 -1
  49. package/dist/graph/build.d.ts +9 -90
  50. package/dist/graph/build.d.ts.map +1 -1
  51. package/dist/graph/build.js +15 -62
  52. package/dist/graph/build.js.map +1 -1
  53. package/dist/graph/cross-language.d.ts +4 -43
  54. package/dist/graph/cross-language.d.ts.map +1 -1
  55. package/dist/graph/cross-language.js +9 -77
  56. package/dist/graph/cross-language.js.map +1 -1
  57. package/dist/graph/merge.d.ts +8 -88
  58. package/dist/graph/merge.d.ts.map +1 -1
  59. package/dist/graph/merge.js +9 -93
  60. package/dist/graph/merge.js.map +1 -1
  61. package/dist/graph/observed-tests.d.ts +7 -68
  62. package/dist/graph/observed-tests.d.ts.map +1 -1
  63. package/dist/graph/observed-tests.js +5 -60
  64. package/dist/graph/observed-tests.js.map +1 -1
  65. package/dist/graph/persist.d.ts +2 -25
  66. package/dist/graph/persist.d.ts.map +1 -1
  67. package/dist/graph/persist.js +2 -25
  68. package/dist/graph/persist.js.map +1 -1
  69. package/dist/graph/runtime-confirmation.d.ts +14 -91
  70. package/dist/graph/runtime-confirmation.d.ts.map +1 -1
  71. package/dist/graph/runtime-confirmation.js +19 -103
  72. package/dist/graph/runtime-confirmation.js.map +1 -1
  73. package/dist/ledger/mint.d.ts +9 -43
  74. package/dist/ledger/mint.d.ts.map +1 -1
  75. package/dist/ledger/mint.js +15 -65
  76. package/dist/ledger/mint.js.map +1 -1
  77. package/dist/ledger/resolve.d.ts +13 -81
  78. package/dist/ledger/resolve.d.ts.map +1 -1
  79. package/dist/ledger/resolve.js +13 -76
  80. package/dist/ledger/resolve.js.map +1 -1
  81. package/dist/multipr/fingerprint.d.ts +5 -46
  82. package/dist/multipr/fingerprint.d.ts.map +1 -1
  83. package/dist/multipr/fingerprint.js +22 -74
  84. package/dist/multipr/fingerprint.js.map +1 -1
  85. package/dist/multipr/hidden-dependency.d.ts +13 -73
  86. package/dist/multipr/hidden-dependency.d.ts.map +1 -1
  87. package/dist/multipr/hidden-dependency.js +12 -63
  88. package/dist/multipr/hidden-dependency.js.map +1 -1
  89. package/dist/multipr/mechanical.d.ts +25 -160
  90. package/dist/multipr/mechanical.d.ts.map +1 -1
  91. package/dist/multipr/mechanical.js +21 -137
  92. package/dist/multipr/mechanical.js.map +1 -1
  93. package/dist/multipr/migration-heads.d.ts +18 -105
  94. package/dist/multipr/migration-heads.d.ts.map +1 -1
  95. package/dist/multipr/migration-heads.js +15 -84
  96. package/dist/multipr/migration-heads.js.map +1 -1
  97. package/dist/multipr/overlap.d.ts +16 -94
  98. package/dist/multipr/overlap.d.ts.map +1 -1
  99. package/dist/multipr/overlap.js +25 -105
  100. package/dist/multipr/overlap.js.map +1 -1
  101. package/dist/multipr/scope-store.d.ts +15 -80
  102. package/dist/multipr/scope-store.d.ts.map +1 -1
  103. package/dist/multipr/scope-store.js +17 -87
  104. package/dist/multipr/scope-store.js.map +1 -1
  105. package/dist/multipr/superseded.d.ts +10 -95
  106. package/dist/multipr/superseded.d.ts.map +1 -1
  107. package/dist/multipr/superseded.js +7 -60
  108. package/dist/multipr/superseded.js.map +1 -1
  109. package/dist/query/confirmed-facts.d.ts +12 -58
  110. package/dist/query/confirmed-facts.d.ts.map +1 -1
  111. package/dist/query/confirmed-facts.js +8 -47
  112. package/dist/query/confirmed-facts.js.map +1 -1
  113. package/dist/query/declared-value-closure.d.ts +13 -104
  114. package/dist/query/declared-value-closure.d.ts.map +1 -1
  115. package/dist/query/declared-value-closure.js +15 -104
  116. package/dist/query/declared-value-closure.js.map +1 -1
  117. package/dist/query/memory.d.ts +4 -16
  118. package/dist/query/memory.d.ts.map +1 -1
  119. package/dist/query/memory.js +9 -22
  120. package/dist/query/memory.js.map +1 -1
  121. package/dist/query/prominence.d.ts +6 -55
  122. package/dist/query/prominence.d.ts.map +1 -1
  123. package/dist/query/prominence.js +6 -55
  124. package/dist/query/prominence.js.map +1 -1
  125. package/dist/query/provider.d.ts +27 -93
  126. package/dist/query/provider.d.ts.map +1 -1
  127. package/dist/query/provider.js +8 -29
  128. package/dist/query/provider.js.map +1 -1
  129. package/dist/query/queries.d.ts +15 -65
  130. package/dist/query/queries.d.ts.map +1 -1
  131. package/dist/query/queries.js +30 -104
  132. package/dist/query/queries.js.map +1 -1
  133. package/dist/query/refusal-fetch.d.ts +8 -85
  134. package/dist/query/refusal-fetch.d.ts.map +1 -1
  135. package/dist/query/refusal-fetch.js +11 -93
  136. package/dist/query/refusal-fetch.js.map +1 -1
  137. package/dist/query/refusal-questions.d.ts +17 -92
  138. package/dist/query/refusal-questions.d.ts.map +1 -1
  139. package/dist/query/refusal-questions.js +14 -78
  140. package/dist/query/refusal-questions.js.map +1 -1
  141. package/dist/query/resolution-floor.d.ts +3 -23
  142. package/dist/query/resolution-floor.d.ts.map +1 -1
  143. package/dist/query/resolution-floor.js +3 -23
  144. package/dist/query/resolution-floor.js.map +1 -1
  145. package/dist/query/root-cause-score.d.ts +11 -161
  146. package/dist/query/root-cause-score.d.ts.map +1 -1
  147. package/dist/query/root-cause-score.js +6 -137
  148. package/dist/query/root-cause-score.js.map +1 -1
  149. package/dist/query/row-closure-picture.d.ts +5 -67
  150. package/dist/query/row-closure-picture.d.ts.map +1 -1
  151. package/dist/query/row-closure-picture.js +6 -67
  152. package/dist/query/row-closure-picture.js.map +1 -1
  153. package/dist/query/similar-incidents.d.ts +6 -41
  154. package/dist/query/similar-incidents.d.ts.map +1 -1
  155. package/dist/query/similar-incidents.js +15 -69
  156. package/dist/query/similar-incidents.js.map +1 -1
  157. package/dist/query/sqlite.d.ts +3 -7
  158. package/dist/query/sqlite.d.ts.map +1 -1
  159. package/dist/query/sqlite.js +9 -19
  160. package/dist/query/sqlite.js.map +1 -1
  161. package/dist/query/test-coverage.d.ts +5 -45
  162. package/dist/query/test-coverage.d.ts.map +1 -1
  163. package/dist/query/test-coverage.js +10 -53
  164. package/dist/query/test-coverage.js.map +1 -1
  165. package/dist/query/traverse.d.ts +22 -126
  166. package/dist/query/traverse.d.ts.map +1 -1
  167. package/dist/query/traverse.js +25 -126
  168. package/dist/query/traverse.js.map +1 -1
  169. package/dist/query/unresolved.d.ts +19 -164
  170. package/dist/query/unresolved.d.ts.map +1 -1
  171. package/dist/query/unresolved.js +11 -138
  172. package/dist/query/unresolved.js.map +1 -1
  173. package/dist/query/verification-status.d.ts +12 -109
  174. package/dist/query/verification-status.d.ts.map +1 -1
  175. package/dist/query/verification-status.js +9 -99
  176. package/dist/query/verification-status.js.map +1 -1
  177. package/dist/recording/migrate.d.ts +4 -10
  178. package/dist/recording/migrate.d.ts.map +1 -1
  179. package/dist/recording/migrate.js +4 -10
  180. package/dist/recording/migrate.js.map +1 -1
  181. package/dist/recording/reader.d.ts +2 -7
  182. package/dist/recording/reader.d.ts.map +1 -1
  183. package/dist/recording/reader.js +2 -7
  184. package/dist/recording/reader.js.map +1 -1
  185. package/dist/recording/redact.d.ts +6 -57
  186. package/dist/recording/redact.d.ts.map +1 -1
  187. package/dist/recording/redact.js +8 -64
  188. package/dist/recording/redact.js.map +1 -1
  189. package/dist/recording/schema.d.ts +3 -38
  190. package/dist/recording/schema.d.ts.map +1 -1
  191. package/dist/recording/schema.js +3 -38
  192. package/dist/recording/schema.js.map +1 -1
  193. package/dist/recording/writer.d.ts +11 -74
  194. package/dist/recording/writer.d.ts.map +1 -1
  195. package/dist/recording/writer.js +3 -43
  196. package/dist/recording/writer.js.map +1 -1
  197. package/dist/scoping/fanout.d.ts +6 -68
  198. package/dist/scoping/fanout.d.ts.map +1 -1
  199. package/dist/scoping/fanout.js +6 -68
  200. package/dist/scoping/fanout.js.map +1 -1
  201. package/dist/scoping/score.d.ts +20 -70
  202. package/dist/scoping/score.d.ts.map +1 -1
  203. package/dist/scoping/score.js +20 -70
  204. package/dist/scoping/score.js.map +1 -1
  205. package/dist/scoping/tiers.d.ts +4 -16
  206. package/dist/scoping/tiers.d.ts.map +1 -1
  207. package/dist/scoping/tiers.js +4 -16
  208. package/dist/scoping/tiers.js.map +1 -1
  209. package/dist/scoping/traverse.d.ts +3 -25
  210. package/dist/scoping/traverse.d.ts.map +1 -1
  211. package/dist/scoping/traverse.js +14 -49
  212. package/dist/scoping/traverse.js.map +1 -1
  213. package/dist/scoping/weights.d.ts +10 -74
  214. package/dist/scoping/weights.d.ts.map +1 -1
  215. package/dist/scoping/weights.js +14 -86
  216. package/dist/scoping/weights.js.map +1 -1
  217. package/dist/sources/confirmed/incidents.d.ts +5 -35
  218. package/dist/sources/confirmed/incidents.d.ts.map +1 -1
  219. package/dist/sources/confirmed/incidents.js +14 -48
  220. package/dist/sources/confirmed/incidents.js.map +1 -1
  221. package/dist/sources/git/diff.d.ts +71 -65
  222. package/dist/sources/git/diff.d.ts.map +1 -1
  223. package/dist/sources/git/diff.js +136 -83
  224. package/dist/sources/git/diff.js.map +1 -1
  225. package/dist/sources/git/env.d.ts +7 -0
  226. package/dist/sources/git/env.d.ts.map +1 -1
  227. package/dist/sources/git/env.js +10 -11
  228. package/dist/sources/git/env.js.map +1 -1
  229. package/dist/sources/git/history.d.ts +64 -84
  230. package/dist/sources/git/history.d.ts.map +1 -1
  231. package/dist/sources/git/history.js +141 -112
  232. package/dist/sources/git/history.js.map +1 -1
  233. package/dist/sources/git/incidents.d.ts +13 -75
  234. package/dist/sources/git/incidents.d.ts.map +1 -1
  235. package/dist/sources/git/incidents.js +21 -91
  236. package/dist/sources/git/incidents.js.map +1 -1
  237. package/dist/sources/git/index.d.ts +5 -3
  238. package/dist/sources/git/index.d.ts.map +1 -1
  239. package/dist/sources/git/index.js +3 -2
  240. package/dist/sources/git/index.js.map +1 -1
  241. package/dist/sources/git/source.d.ts +9 -79
  242. package/dist/sources/git/source.d.ts.map +1 -1
  243. package/dist/sources/git/source.js +33 -133
  244. package/dist/sources/git/source.js.map +1 -1
  245. package/dist/sources/migrations/across-change.d.ts +94 -0
  246. package/dist/sources/migrations/across-change.d.ts.map +1 -0
  247. package/dist/sources/migrations/across-change.js +278 -0
  248. package/dist/sources/migrations/across-change.js.map +1 -0
  249. package/dist/sources/migrations/dialects.d.ts +20 -36
  250. package/dist/sources/migrations/dialects.d.ts.map +1 -1
  251. package/dist/sources/migrations/dialects.js +11 -36
  252. package/dist/sources/migrations/dialects.js.map +1 -1
  253. package/dist/sources/migrations/index.d.ts +3 -1
  254. package/dist/sources/migrations/index.d.ts.map +1 -1
  255. package/dist/sources/migrations/index.js +2 -1
  256. package/dist/sources/migrations/index.js.map +1 -1
  257. package/dist/sources/migrations/read.d.ts +14 -79
  258. package/dist/sources/migrations/read.d.ts.map +1 -1
  259. package/dist/sources/migrations/read.js +14 -89
  260. package/dist/sources/migrations/read.js.map +1 -1
  261. package/dist/sources/workspace/workspace.d.ts +6 -42
  262. package/dist/sources/workspace/workspace.d.ts.map +1 -1
  263. package/dist/sources/workspace/workspace.js +10 -53
  264. package/dist/sources/workspace/workspace.js.map +1 -1
  265. package/dist/store/driver/driver.d.ts +9 -38
  266. package/dist/store/driver/driver.d.ts.map +1 -1
  267. package/dist/store/driver/driver.js +4 -22
  268. package/dist/store/driver/driver.js.map +1 -1
  269. package/dist/store/driver/node-sqlite.d.ts +3 -12
  270. package/dist/store/driver/node-sqlite.d.ts.map +1 -1
  271. package/dist/store/driver/node-sqlite.js +4 -18
  272. package/dist/store/driver/node-sqlite.js.map +1 -1
  273. package/dist/store/index.d.ts +2 -7
  274. package/dist/store/index.d.ts.map +1 -1
  275. package/dist/store/index.js +4 -10
  276. package/dist/store/index.js.map +1 -1
  277. package/dist/store/migrate.d.ts +14 -50
  278. package/dist/store/migrate.d.ts.map +1 -1
  279. package/dist/store/migrate.js +15 -52
  280. package/dist/store/migrate.js.map +1 -1
  281. package/dist/store/reader.d.ts +19 -84
  282. package/dist/store/reader.d.ts.map +1 -1
  283. package/dist/store/reader.js +25 -92
  284. package/dist/store/reader.js.map +1 -1
  285. package/dist/store/schema.d.ts +14 -76
  286. package/dist/store/schema.d.ts.map +1 -1
  287. package/dist/store/schema.js +14 -76
  288. package/dist/store/schema.js.map +1 -1
  289. package/dist/store/writer.d.ts +14 -88
  290. package/dist/store/writer.d.ts.map +1 -1
  291. package/dist/store/writer.js +29 -123
  292. package/dist/store/writer.js.map +1 -1
  293. package/dist/tiers/certify.d.ts +46 -281
  294. package/dist/tiers/certify.d.ts.map +1 -1
  295. package/dist/tiers/certify.js +46 -226
  296. package/dist/tiers/certify.js.map +1 -1
  297. package/dist/tiers/ladder.d.ts +7 -51
  298. package/dist/tiers/ladder.d.ts.map +1 -1
  299. package/dist/tiers/ladder.js +13 -89
  300. package/dist/tiers/ladder.js.map +1 -1
  301. package/dist/validation/attributes.d.ts +6 -55
  302. package/dist/validation/attributes.d.ts.map +1 -1
  303. package/dist/validation/attributes.js +6 -55
  304. package/dist/validation/attributes.js.map +1 -1
  305. package/dist/validation/config-graph.d.ts +7 -56
  306. package/dist/validation/config-graph.d.ts.map +1 -1
  307. package/dist/validation/config-graph.js +15 -61
  308. package/dist/validation/config-graph.js.map +1 -1
  309. package/dist/validation/env.d.ts +7 -1
  310. package/dist/validation/env.d.ts.map +1 -1
  311. package/dist/validation/env.js +3 -1
  312. package/dist/validation/env.js.map +1 -1
  313. package/dist/validation/join-substitution.d.ts +22 -120
  314. package/dist/validation/join-substitution.d.ts.map +1 -1
  315. package/dist/validation/join-substitution.js +13 -90
  316. package/dist/validation/join-substitution.js.map +1 -1
  317. package/dist/validation/migration-integrity.d.ts +11 -72
  318. package/dist/validation/migration-integrity.d.ts.map +1 -1
  319. package/dist/validation/migration-integrity.js +16 -79
  320. package/dist/validation/migration-integrity.js.map +1 -1
  321. package/dist/validation/migrations.d.ts +22 -101
  322. package/dist/validation/migrations.d.ts.map +1 -1
  323. package/dist/validation/migrations.js +19 -83
  324. package/dist/validation/migrations.js.map +1 -1
  325. package/dist/validation/nodes.d.ts +3 -1
  326. package/dist/validation/nodes.d.ts.map +1 -1
  327. package/dist/validation/nodes.js +3 -1
  328. package/dist/validation/nodes.js.map +1 -1
  329. package/dist/validation/rollback.d.ts +10 -73
  330. package/dist/validation/rollback.d.ts.map +1 -1
  331. package/dist/validation/rollback.js +15 -82
  332. package/dist/validation/rollback.js.map +1 -1
  333. package/dist/validation/route-drift.d.ts +9 -62
  334. package/dist/validation/route-drift.d.ts.map +1 -1
  335. package/dist/validation/route-drift.js +7 -52
  336. package/dist/validation/route-drift.js.map +1 -1
  337. package/dist/validation/sources.d.ts +16 -94
  338. package/dist/validation/sources.d.ts.map +1 -1
  339. package/dist/validation/sources.js +51 -147
  340. package/dist/validation/sources.js.map +1 -1
  341. package/dist/worker/pool.d.ts +6 -45
  342. package/dist/worker/pool.d.ts.map +1 -1
  343. package/dist/worker/pool.js +5 -42
  344. package/dist/worker/pool.js.map +1 -1
  345. package/dist/worker/protocol.d.ts +7 -20
  346. package/dist/worker/protocol.d.ts.map +1 -1
  347. package/dist/worker/protocol.js +3 -10
  348. package/dist/worker/protocol.js.map +1 -1
  349. package/dist/worker/traversal.worker.d.ts +2 -21
  350. package/dist/worker/traversal.worker.d.ts.map +1 -1
  351. package/dist/worker/traversal.worker.js +4 -23
  352. package/dist/worker/traversal.worker.js.map +1 -1
  353. package/package.json +11 -2
@@ -1,21 +1,20 @@
1
1
  /**
2
- * The variables git exports into every hook and injects into every
3
- * subprocess it spawns. Left in place, an inherited `GIT_DIR` (or
4
- * `GIT_WORK_TREE`/`GIT_INDEX_FILE`/`GIT_COMMON_DIR`) overrides repository
5
- * discovery even when a git subprocess is given an explicit `cwd` — confirmed
6
- * directly: `GIT_DIR=<other>/.git` with `cwd` pointed elsewhere still
7
- * initialises `<other>/.git`, not a repository at `cwd`.
8
- *
9
- * Anything here that shells out to git against a caller-supplied path must
10
- * not inherit these, or a caller running inside a git hook (which always has
11
- * `GIT_DIR` set) silently operates on the hook's own repository instead of
12
- * the one it was given.
2
+ * Git env vars that override repo discovery even with an explicit `cwd` (confirmed: `GIT_DIR` elsewhere still wins). Must be stripped before shelling out against a
3
+ * caller-supplied path, or a caller inside a git hook silently operates on the hook's own repo.
13
4
  */
14
5
  const LEAKY_GIT_VARS = ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"];
6
+ /** B-11: `cwd` may be an untrusted repo (e.g. an extracted archive with an
7
+ * attacker-authored `.git/config`). `GIT_CONFIG_NOSYSTEM` closes only the
8
+ * system-config layer, not repo-local config — see `HARDENED_GIT_CONFIG_ARGS`. */
15
9
  export function cleanGitEnv(base = process.env) {
16
10
  const env = { ...base };
17
11
  for (const key of LEAKY_GIT_VARS)
18
12
  delete env[key];
13
+ env["GIT_CONFIG_NOSYSTEM"] = "1";
19
14
  return env;
20
15
  }
16
+ /** `-c` overrides ahead of every git subcommand against a caller-supplied repo (B-11): neutralises `core.fsmonitor`/`core.pager` (external-command hooks; `GIT_CONFIG_NOSYSTEM` can't reach repo-local config), zero behavioural cost since output here never hits a tty.
17
+ * `diff.external` handled separately via `--no-ext-diff` at its two call sites (`diff.ts`, `multipr/fingerprint.ts`).
18
+ * `filter.*.clean` is NOT addressed — driver name is dynamic per-repo; unreachable through any call here today, disclosed as a residual gap. */
19
+ export const HARDENED_GIT_CONFIG_ARGS = ["-c", "core.fsmonitor=false", "-c", "core.pager=cat"];
21
20
  //# sourceMappingURL=env.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"env.js","sourceRoot":"","sources":["../../../src/sources/git/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,cAAc,GAAG,CAAC,SAAS,EAAE,eAAe,EAAE,gBAAgB,EAAE,gBAAgB,CAAU,CAAC;AAEjG,MAAM,UAAU,WAAW,CAAC,OAA0B,OAAO,CAAC,GAAG;IAC/D,MAAM,GAAG,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC;IACxB,KAAK,MAAM,GAAG,IAAI,cAAc;QAAE,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;IAClD,OAAO,GAAG,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../../../src/sources/git/env.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,cAAc,GAAG,CAAC,SAAS,EAAE,eAAe,EAAE,gBAAgB,EAAE,gBAAgB,CAAU,CAAC;AAEjG;;mFAEmF;AACnF,MAAM,UAAU,WAAW,CAAC,OAA0B,OAAO,CAAC,GAAG;IAC/D,MAAM,GAAG,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC;IACxB,KAAK,MAAM,GAAG,IAAI,cAAc;QAAE,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;IAClD,GAAG,CAAC,qBAAqB,CAAC,GAAG,GAAG,CAAC;IACjC,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;iJAEiJ;AACjJ,MAAM,CAAC,MAAM,wBAAwB,GAAsB,CAAC,IAAI,EAAE,sBAAsB,EAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC"}
@@ -1,24 +1,6 @@
1
1
  /**
2
- * Reading git history.
3
- *
4
- * No dependency: `git` is invoked directly. A library would buy parsing help and
5
- * cost a dependency in a package that has none by design, and the format below
6
- * is chosen so parsing is unambiguous rather than clever.
7
- *
8
- * ## Renames are resolved, not ignored
9
- *
10
- * `CHANGES_WITH` is file-to-file, and `FILE` identity is DEC-004's documented
11
- * path fallback — a file's only stable name *is* its path. So a rename silently
12
- * splits a file's history in two: the co-change weight built up over two years
13
- * resets to zero, and nothing in the output reveals it happened. That is the
14
- * exact failure DEC-004 exists to prevent, arriving through the one node type
15
- * that cannot avoid it.
16
- *
17
- * The fix is `-M` plus a canonicalisation pass: history is walked newest-first,
18
- * and every historical path is mapped forward to the name the file has today. It
19
- * makes DEC-004's guarantee total for files that git can *follow*; where git's
20
- * rename detection fails (a rewrite with no similarity), the guarantee stays
21
- * partial and that is now a bounded, stated gap rather than an unbounded one.
2
+ * Reading git history. No dependency: git invoked directly, no parsing library. Renames are resolved, not ignored: FILE identity is path-based (DEC-004), so an unresolved rename would silently reset co-change weight to zero.
3
+ * `-M` plus a newest-first canonicalisation pass maps every historical path forward to today's name — total where git can follow the rename, a disclosed partial gap where it can't (a rewrite with no similarity match).
22
4
  */
23
5
  export interface CommitChange {
24
6
  /** Path as it is known **today**, after following renames forward. */
@@ -42,15 +24,20 @@ export interface HistoryOptions {
42
24
  readonly maxFilesPerCommit?: number;
43
25
  }
44
26
  export declare function gitHead(cwd: string): Promise<string>;
27
+ /** Thrown when the git process itself could not be run (missing binary,
28
+ * bad cwd) — distinct from git running and answering "not a repository".
29
+ * Collapsing the two (as this used to) teaches a false fact with no way to detect it; every caller must let this propagate rather than treat it as "not a repository". */
30
+ export declare class GitUnavailableError extends Error {
31
+ constructor(cwd: string, cause: unknown);
32
+ }
45
33
  export declare function isGitRepository(cwd: string): Promise<boolean>;
46
- /**
47
- * Read history newest-first, with renames followed forward to today's paths.
48
- *
49
- * `--no-merges` is deliberate: a merge commit's file list is the union of both
50
- * branches, so counting it as co-change would report every file in a large merge
51
- * as coupled to every other. That is the single largest source of false
52
- * `CHANGES_WITH` edges, and it is free to exclude.
53
- */
34
+ /** Whether `absolutePath` is tracked by git in `cwd`. A-F1: a bare module
35
+ * specifier resolves against the scanned repo's own node_modules — a real
36
+ * `npm install` leaves it untracked, but a hostile repo can COMMIT a payload there for a victim who only clones and never runs install; tracked-in-git is the discriminator. Returns null (not false) when `cwd` isn't a git repo at all — "cannot determine" must never collapse into "untracked, therefore safe". Outside `cwd` entirely returns false. */
37
+ export declare function isPathTrackedByGit(cwd: string, absolutePath: string): Promise<boolean | null>;
38
+ /** Read history newest-first, renames followed forward. `--no-merges` is
39
+ * deliberate: a merge's file list is the union of both branches — the
40
+ * single largest source of false CHANGES_WITH edges, and free to exclude. */
54
41
  export declare function readHistory(cwd: string, options?: HistoryOptions): Promise<Commit[]>;
55
42
  export interface PathLogOptions {
56
43
  /** Cap on commits returned. Default 20 — this is a live, on-demand read, not a bulk ingestion. */
@@ -70,71 +57,64 @@ export interface PathLogCommit {
70
57
  }
71
58
  export interface PathLogResult {
72
59
  readonly commits: readonly PathLogCommit[];
73
- /**
74
- * How many commits touch this path in total, independent of `maxCommits`.
75
- * A second, cheap `git log --format=%H` count — not `commits.length`,
76
- * which stops at the cap — so a caller can tell "here are the 20 most
77
- * recent" from "here are all 3 there are" without guessing from the cap.
78
- */
60
+ /** Total commits touching this path, independent of `maxCommits` — a
61
+ * second cheap count, not `commits.length`, which stops at the cap. */
79
62
  readonly totalTouching: number;
80
63
  }
81
- /**
82
- * Live commit history for one file, right now — distinct from `history.ts`'s
83
- * `changesWith`, which only reads the graph's stored `CHANGES_WITH` ledger
84
- * built once at `analyze` time. `--follow` tracks the file across renames;
85
- * unlike `readHistory`'s whole-repo walk, each commit here reports its own
86
- * historical name rather than today's — the rename itself is part of what a
87
- * caller asking "what happened to this file" wants to see.
88
- */
64
+ /** Live commit history for one file — distinct from the graph's stored
65
+ * CHANGES_WITH ledger. `--follow` tracks renames; each commit reports its
66
+ * own historical name, not today's — the rename is part of what's asked. */
89
67
  export declare function logForPath(cwd: string, path: string, options?: PathLogOptions): Promise<PathLogResult>;
68
+ /** Resolve one commit by sha, bypassing readHistory's HEAD-reachability and
69
+ * --no-merges filters — a revert can name a merge or an unreachable, not-yet-GC'd
70
+ * commit that readHistory's index can't see. Returns null only if the object is truly gone; caller must disclose, never drop silently (rule 7). Paths aren't forward-canonicalised (disclosed gap). */
71
+ export declare function resolveCommitBySha(cwd: string, sha: string, maxFilesPerCommit?: number): Promise<Commit | null>;
72
+ /** Files changed between two refs, for PR analysis. `base...head` (three
73
+ * dots, not two) diffs against the merge base — two dots would include every
74
+ * file changed on base since the branch point, someone else's change. Deletions included: what called the removed thing is the useful question. */
75
+ export declare function changedFiles(cwd: string, base: string, head?: string): Promise<string[]>;
76
+ /** `git show --name-only` for one commit, with today's paths where known. */
77
+ export declare function filesInCommit(cwd: string, sha: string): Promise<string[]>;
90
78
  /**
91
- * Resolve one commit directly by sha, bypassing both `readHistory`'s
92
- * reachability filter and its `--no-merges` exclusion.
79
+ * Which of these paths git reports as uncommitted — modified, staged or untracked.
93
80
  *
94
- * `readHistory` walks history from `HEAD` only — co-change must stay scoped to
95
- * mainline, or work in progress on other branches would pollute
96
- * `CHANGES_WITH` — and drops merge commits, because a merge's file list is
97
- * the union of both branches (see the module doc). Both are correct for
98
- * co-change. Neither is correct for resolving what a revert commit names:
99
- * `git revert`'s `This reverts commit <sha>` line can point at a commit that
100
- * **is** a merge ("Revert Merge pull request #N"), or at a commit that is
101
- * real and still in the object database but not an ancestor of `HEAD` — an
102
- * abandoned branch, or one deleted after a squash/rebase merge whose objects
103
- * have not yet been garbage-collected. Neither case means the commit does
104
- * not exist; `readHistory`'s index just cannot see it.
81
+ * One `git status --porcelain` over the named paths rather than a call per
82
+ * file. `--porcelain` is the stable machine format, guaranteed across versions,
83
+ * and its two status columns cover every state that matters here at once: a
84
+ * file the index has and `HEAD` does not, one the working tree has and the
85
+ * index does not, and one git has never seen.
105
86
  *
106
- * `git cat-file` and `git diff-tree` operate on the object database directly
107
- * and enforce neither constraint, so this is the fallback `createGitSource`
108
- * uses once `readHistory`'s own index has already missed a revert target.
109
- *
110
- * Returns `null` only when the object genuinely does not exist — the
111
- * squash-merge case, where the original commit was never an ancestor of
112
- * anything that survived and its object is gone from the store. That is the
113
- * one case nothing can resolve; the caller must disclose it rather than
114
- * drop it silently (CLAUDE.md rule 7 — every fallback is labelled).
115
- *
116
- * File paths returned here are **not** forward-canonicalised through
117
- * `readHistory`'s rename map — this commit was never visited by that walk.
118
- * A file renamed on mainline *after* this commit landed will show under its
119
- * name at the time, not today's name. That is a narrower, disclosed version
120
- * of the same rename gap the module doc already states for the primary walk.
87
+ * The distinction this exists to make is not cosmetic. A path absent from a
88
+ * ref-to-ref diff might be uncommitted work, or it might be a file the caller
89
+ * named that this branch simply did not touch. Both make a coverage section
90
+ * describe something else; only the first is a fact about timing that the
91
+ * developer can act on in the next thirty seconds.
121
92
  */
122
- export declare function resolveCommitBySha(cwd: string, sha: string, maxFilesPerCommit?: number): Promise<Commit | null>;
93
+ export declare function uncommittedAmong(cwd: string, paths: readonly string[]): Promise<readonly string[]>;
94
+ export interface WorkingTreeFilesOptions {
95
+ /** Which tracked-file comparison to make. Default `"all"` — everything uncommitted. */
96
+ readonly scope?: "unstaged" | "staged" | "all";
97
+ /** Include files git has never seen. Default true — see below. */
98
+ readonly includeUntracked?: boolean;
99
+ /**
100
+ * Directory prefixes whose contents are not the change under review — a
101
+ * tool's own state, where a `.gitignore` does not happen to cover it.
102
+ * Matched at a segment boundary, so `.descry` leaves `src/.descryfile.ts`.
103
+ */
104
+ readonly exclude?: readonly string[];
105
+ }
123
106
  /**
124
- * Files changed between two refs — the input to PR analysis.
107
+ * The paths a working tree changes, as {@link changedFiles} does for a range.
125
108
  *
126
- * `base...head` (three dots) rather than `base..head`, and the difference is the
127
- * whole point: two dots diffs the two tips, so every file changed on `base`
128
- * since the branch point appears in the result as though the PR had touched it.
129
- * On a branch a week behind `main` that is most of the diff, and the blast
130
- * radius it produces belongs to somebody else's change. Three dots diffs against
131
- * the merge base, which is what a reviewer is actually looking at.
109
+ * Name-only, so it is the cheap counterpart to `gitWorkingTreeChange`: a caller
110
+ * that only needs to know *which* files changed should not pay for every patch.
132
111
  *
133
- * Deletions are included. A deleted file's nodes may still be in the graph, and
134
- * *what called the thing you just removed* is the most useful question a diff
135
- * can be asked.
112
+ * **Untracked is on by default here**, unlike the diff-producing function. That
113
+ * one is a library primitive whose existing callers must not change behaviour
114
+ * by upgrading. This one exists to answer "what is the change about to be
115
+ * committed", and a new file written and not yet added is part of that change —
116
+ * defaulting it off would mean the function misses, by default, the case it was
117
+ * written for.
136
118
  */
137
- export declare function changedFiles(cwd: string, base: string, head?: string): Promise<string[]>;
138
- /** `git show --name-only` for one commit, with today's paths where known. */
139
- export declare function filesInCommit(cwd: string, sha: string): Promise<string[]>;
119
+ export declare function workingTreeChangedFiles(cwd: string, options?: WorkingTreeFilesOptions): Promise<string[]>;
140
120
  //# sourceMappingURL=history.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../../src/sources/git/history.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AA0BH,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IAC7C,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG1D;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE;AA0BD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA4D9F;AAED,MAAM,WAAW,cAAc;IAC7B,kGAAkG;IAClG,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,+GAA+G;AAC/G,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IAC7C,uGAAuG;IACvG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,aAAa,CAAC,CAuDxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,iBAAiB,SAAK,GACrB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoExB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,SAAS,GACZ,OAAO,CAAC,MAAM,EAAE,CAAC,CASnB;AAED,6EAA6E;AAC7E,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAU/E"}
1
+ {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../../src/sources/git/history.ts"],"names":[],"mappings":"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"}
@@ -1,32 +1,16 @@
1
1
  /**
2
- * Reading git history.
3
- *
4
- * No dependency: `git` is invoked directly. A library would buy parsing help and
5
- * cost a dependency in a package that has none by design, and the format below
6
- * is chosen so parsing is unambiguous rather than clever.
7
- *
8
- * ## Renames are resolved, not ignored
9
- *
10
- * `CHANGES_WITH` is file-to-file, and `FILE` identity is DEC-004's documented
11
- * path fallback — a file's only stable name *is* its path. So a rename silently
12
- * splits a file's history in two: the co-change weight built up over two years
13
- * resets to zero, and nothing in the output reveals it happened. That is the
14
- * exact failure DEC-004 exists to prevent, arriving through the one node type
15
- * that cannot avoid it.
16
- *
17
- * The fix is `-M` plus a canonicalisation pass: history is walked newest-first,
18
- * and every historical path is mapped forward to the name the file has today. It
19
- * makes DEC-004's guarantee total for files that git can *follow*; where git's
20
- * rename detection fails (a rewrite with no similarity), the guarantee stays
21
- * partial and that is now a bounded, stated gap rather than an unbounded one.
2
+ * Reading git history. No dependency: git invoked directly, no parsing library. Renames are resolved, not ignored: FILE identity is path-based (DEC-004), so an unresolved rename would silently reset co-change weight to zero.
3
+ * `-M` plus a newest-first canonicalisation pass maps every historical path forward to today's name — total where git can follow the rename, a disclosed partial gap where it can't (a rewrite with no similarity match).
22
4
  */
23
5
  import { execFile } from "node:child_process";
6
+ import { isAbsolute, relative } from "node:path";
24
7
  import { promisify } from "node:util";
25
- import { cleanGitEnv } from "./env.js";
8
+ import { cleanGitEnv, HARDENED_GIT_CONFIG_ARGS } from "./env.js";
26
9
  const execFileAsync = promisify(execFile);
27
- // Always strips GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE/GIT_COMMON_DIR — see env.ts.
10
+ // Always strips GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE/GIT_COMMON_DIR, sets
11
+ // GIT_CONFIG_NOSYSTEM and applies HARDENED_GIT_CONFIG_ARGS — see env.ts (B-11).
28
12
  function run(command, args, options) {
29
- return execFileAsync(command, args, {
13
+ return execFileAsync(command, [...HARDENED_GIT_CONFIG_ARGS, ...args], {
30
14
  ...options,
31
15
  encoding: "utf8",
32
16
  env: cleanGitEnv(),
@@ -39,24 +23,62 @@ export async function gitHead(cwd) {
39
23
  const { stdout } = await run("git", ["rev-parse", "HEAD"], { cwd });
40
24
  return stdout.trim();
41
25
  }
26
+ /** Thrown when the git process itself could not be run (missing binary,
27
+ * bad cwd) — distinct from git running and answering "not a repository".
28
+ * Collapsing the two (as this used to) teaches a false fact with no way to detect it; every caller must let this propagate rather than treat it as "not a repository". */
29
+ export class GitUnavailableError extends Error {
30
+ constructor(cwd, cause) {
31
+ const detail = cause instanceof Error ? cause.message : String(cause);
32
+ super(`git could not be run in "${cwd}" (${detail}). This is not a fact about whether that ` +
33
+ "directory is a git repository — check that the directory exists and that git is " +
34
+ "installed and reachable on PATH.");
35
+ this.name = "GitUnavailableError";
36
+ this.cause = cause;
37
+ }
38
+ }
39
+ /** True only when the git process never started (spawn failed — string
40
+ * `code`, no stdout/stderr), vs. running and exiting non-zero (numeric
41
+ * code, populated output) — the only signal distinguishing the two cases. */
42
+ function isSpawnFailure(err) {
43
+ return (typeof err === "object" &&
44
+ err !== null &&
45
+ "code" in err &&
46
+ typeof err.code === "string");
47
+ }
42
48
  export async function isGitRepository(cwd) {
43
49
  try {
44
50
  const { stdout } = await run("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
45
51
  return stdout.trim() === "true";
46
52
  }
47
- catch {
53
+ catch (err) {
54
+ if (isSpawnFailure(err))
55
+ throw new GitUnavailableError(cwd, err);
48
56
  return false;
49
57
  }
50
58
  }
59
+ /** Whether `absolutePath` is tracked by git in `cwd`. A-F1: a bare module
60
+ * specifier resolves against the scanned repo's own node_modules — a real
61
+ * `npm install` leaves it untracked, but a hostile repo can COMMIT a payload there for a victim who only clones and never runs install; tracked-in-git is the discriminator. Returns null (not false) when `cwd` isn't a git repo at all — "cannot determine" must never collapse into "untracked, therefore safe". Outside `cwd` entirely returns false. */
62
+ export async function isPathTrackedByGit(cwd, absolutePath) {
63
+ const rel = relative(cwd, absolutePath);
64
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel))
65
+ return false;
66
+ if (!(await isGitRepository(cwd)))
67
+ return null;
68
+ try {
69
+ const { stdout } = await run("git", ["ls-files", "-z", "--", rel], { cwd });
70
+ return stdout.length > 0;
71
+ }
72
+ catch {
73
+ // A repo that refuses ls-files (corrupted index, permissions) is the
74
+ // same "cannot determine" case as no repository — not evidence either way.
75
+ return null;
76
+ }
77
+ }
51
78
  const LOG_FORMAT = `${RECORD}%H${FIELD}%at${FIELD}%s${FIELD}%b${FIELD}`;
52
- /**
53
- * Split one `--format=<RECORD>%H<FIELD>%at<FIELD>%s<FIELD>%b<FIELD>` record
54
- * into its four fields plus the raw `--name-status` block that follows them.
55
- * Shared by `readHistory` and `logForPath` — everything past this point
56
- * (how paths are resolved, whether a rename mutates shared state) differs
57
- * enough between the two that sharing further would cost more than it
58
- * saves; see `readHistory`'s own resolve closure for why.
59
- */
79
+ /** Splits one log record into its four fields plus the raw name-status block.
80
+ * Shared by readHistory/logForPath; sharing further isn't worth it since
81
+ * path resolution diverges sharply between the two. */
60
82
  function splitRecord(record) {
61
83
  if (record.trim() === "")
62
84
  return null;
@@ -72,14 +94,9 @@ function splitRecord(record) {
72
94
  nameStatus: parts.slice(4).join(FIELD),
73
95
  };
74
96
  }
75
- /**
76
- * Read history newest-first, with renames followed forward to today's paths.
77
- *
78
- * `--no-merges` is deliberate: a merge commit's file list is the union of both
79
- * branches, so counting it as co-change would report every file in a large merge
80
- * as coupled to every other. That is the single largest source of false
81
- * `CHANGES_WITH` edges, and it is free to exclude.
82
- */
97
+ /** Read history newest-first, renames followed forward. `--no-merges` is
98
+ * deliberate: a merge's file list is the union of both branches — the
99
+ * single largest source of false CHANGES_WITH edges, and free to exclude. */
83
100
  export async function readHistory(cwd, options = {}) {
84
101
  const maxCommits = options.maxCommits ?? 2000;
85
102
  const maxFiles = options.maxFilesPerCommit ?? 50;
@@ -106,9 +123,8 @@ export async function readHistory(cwd, options = {}) {
106
123
  const to = fields[2];
107
124
  if (from === undefined || to === undefined)
108
125
  continue;
109
- // Walking newest-first, so `to` is the newer name: anything older that
110
- // called this file `from` is the same file, known today as whatever
111
- // `to` currently resolves to.
126
+ // Walking newest-first: `to` is the newer name, so anything older
127
+ // called `from` is the same file, known today as `to` resolves.
112
128
  if (code.startsWith("R"))
113
129
  canonical.set(from, resolve(to));
114
130
  changes.push({ path: resolve(to), status: code.startsWith("R") ? "R" : "C", fromPath: resolve(from) });
@@ -122,10 +138,8 @@ export async function readHistory(cwd, options = {}) {
122
138
  continue;
123
139
  changes.push({ path: resolve(path), status });
124
140
  }
125
- // A commit touching hundreds of files is a reformat, a dependency bump or a
126
- // move — it says nothing about which files are logically coupled, and
127
- // including it makes every file in it co-change with every other. That is
128
- // O(n^2) noise from a single commit.
141
+ // A commit touching hundreds of files (reformat, dependency bump) says
142
+ // nothing about coupling — including it is O(n^2) noise from one commit.
129
143
  if (changes.length === 0 || changes.length > maxFiles) {
130
144
  commits.push({ sha, timestamp, subject, body, changes: [] });
131
145
  continue;
@@ -134,14 +148,9 @@ export async function readHistory(cwd, options = {}) {
134
148
  }
135
149
  return commits;
136
150
  }
137
- /**
138
- * Live commit history for one file, right now — distinct from `history.ts`'s
139
- * `changesWith`, which only reads the graph's stored `CHANGES_WITH` ledger
140
- * built once at `analyze` time. `--follow` tracks the file across renames;
141
- * unlike `readHistory`'s whole-repo walk, each commit here reports its own
142
- * historical name rather than today's — the rename itself is part of what a
143
- * caller asking "what happened to this file" wants to see.
144
- */
151
+ /** Live commit history for one file — distinct from the graph's stored
152
+ * CHANGES_WITH ledger. `--follow` tracks renames; each commit reports its
153
+ * own historical name, not today's — the rename is part of what's asked. */
145
154
  export async function logForPath(cwd, path, options = {}) {
146
155
  const maxCommits = options.maxCommits ?? 20;
147
156
  const { stdout } = await run("git", ["log", "--no-merges", "--follow", "-M", "--name-status", `--max-count=${maxCommits}`, `--format=${LOG_FORMAT}`, "--", path], { cwd, maxBuffer: 64 * 1024 * 1024 });
@@ -151,9 +160,8 @@ export async function logForPath(cwd, path, options = {}) {
151
160
  if (split === null)
152
161
  continue;
153
162
  const { sha, timestamp, subject, body, nameStatus } = split;
154
- // `--follow` scopes the walk to one file's lineage, so exactly one
155
- // name-status line is expected per commit; the first is taken rather
156
- // than asserting an invariant a future git version could quietly break.
163
+ // `--follow` scopes to one file's lineage: exactly one line expected
164
+ // per commit, taken rather than asserted (a future git could break it).
157
165
  const line = nameStatus.split("\n").find((l) => l.trim() !== "");
158
166
  if (line === undefined)
159
167
  continue;
@@ -189,38 +197,9 @@ export async function logForPath(cwd, path, options = {}) {
189
197
  const totalTouching = countOut.split("\n").filter((l) => l.trim() !== "").length;
190
198
  return { commits, totalTouching };
191
199
  }
192
- /**
193
- * Resolve one commit directly by sha, bypassing both `readHistory`'s
194
- * reachability filter and its `--no-merges` exclusion.
195
- *
196
- * `readHistory` walks history from `HEAD` only — co-change must stay scoped to
197
- * mainline, or work in progress on other branches would pollute
198
- * `CHANGES_WITH` — and drops merge commits, because a merge's file list is
199
- * the union of both branches (see the module doc). Both are correct for
200
- * co-change. Neither is correct for resolving what a revert commit names:
201
- * `git revert`'s `This reverts commit <sha>` line can point at a commit that
202
- * **is** a merge ("Revert Merge pull request #N"), or at a commit that is
203
- * real and still in the object database but not an ancestor of `HEAD` — an
204
- * abandoned branch, or one deleted after a squash/rebase merge whose objects
205
- * have not yet been garbage-collected. Neither case means the commit does
206
- * not exist; `readHistory`'s index just cannot see it.
207
- *
208
- * `git cat-file` and `git diff-tree` operate on the object database directly
209
- * and enforce neither constraint, so this is the fallback `createGitSource`
210
- * uses once `readHistory`'s own index has already missed a revert target.
211
- *
212
- * Returns `null` only when the object genuinely does not exist — the
213
- * squash-merge case, where the original commit was never an ancestor of
214
- * anything that survived and its object is gone from the store. That is the
215
- * one case nothing can resolve; the caller must disclose it rather than
216
- * drop it silently (CLAUDE.md rule 7 — every fallback is labelled).
217
- *
218
- * File paths returned here are **not** forward-canonicalised through
219
- * `readHistory`'s rename map — this commit was never visited by that walk.
220
- * A file renamed on mainline *after* this commit landed will show under its
221
- * name at the time, not today's name. That is a narrower, disclosed version
222
- * of the same rename gap the module doc already states for the primary walk.
223
- */
200
+ /** Resolve one commit by sha, bypassing readHistory's HEAD-reachability and
201
+ * --no-merges filters — a revert can name a merge or an unreachable, not-yet-GC'd
202
+ * commit that readHistory's index can't see. Returns null only if the object is truly gone; caller must disclose, never drop silently (rule 7). Paths aren't forward-canonicalised (disclosed gap). */
224
203
  export async function resolveCommitBySha(cwd, sha, maxFilesPerCommit = 50) {
225
204
  try {
226
205
  await run("git", ["cat-file", "-e", `${sha}^{commit}`], { cwd });
@@ -233,11 +212,9 @@ export async function resolveCommitBySha(cwd, sha, maxFilesPerCommit = 50) {
233
212
  const [tsRaw, subject, ...bodyParts] = metaOut.split(FIELD);
234
213
  const timestamp = Number(tsRaw ?? 0);
235
214
  const body = bodyParts.join(FIELD).replace(/\n+$/, "");
236
- // Diff against the first parent: for an ordinary commit that is its only
237
- // parent, so this is identical to a normal diff. For a merge commit it is
238
- // "what this merge brought in relative to mainline" — exactly the file
239
- // list a revert of that merge names. A root commit has no parent at all;
240
- // fall back to a diff against the empty tree.
215
+ // Diff against the first parent — identical to normal for an ordinary
216
+ // commit, "what this merge brought in" for a merge. Root commit: diff
217
+ // against the empty tree.
241
218
  let nameStatus;
242
219
  try {
243
220
  ({ stdout: nameStatus } = await run("git", ["diff-tree", "-M", "--no-commit-id", "--name-status", "-r", `${sha}^1`, sha], { cwd, maxBuffer: 64 * 1024 * 1024 }));
@@ -273,26 +250,14 @@ export async function resolveCommitBySha(cwd, sha, maxFilesPerCommit = 50) {
273
250
  timestamp,
274
251
  subject: subject ?? "",
275
252
  body,
276
- // Same reformat/dependency-bump cap `readHistory` applies — a revert of a
277
- // merge that brought in hundreds of files should not fan
278
- // INCIDENT_CORRELATED out into hundreds of edges either.
253
+ // Same reformat/dependency-bump cap readHistory applies — a big merge
254
+ // revert shouldn't fan INCIDENT_CORRELATED into hundreds of edges.
279
255
  changes: changes.length > maxFilesPerCommit ? [] : changes,
280
256
  };
281
257
  }
282
- /**
283
- * Files changed between two refs — the input to PR analysis.
284
- *
285
- * `base...head` (three dots) rather than `base..head`, and the difference is the
286
- * whole point: two dots diffs the two tips, so every file changed on `base`
287
- * since the branch point appears in the result as though the PR had touched it.
288
- * On a branch a week behind `main` that is most of the diff, and the blast
289
- * radius it produces belongs to somebody else's change. Three dots diffs against
290
- * the merge base, which is what a reviewer is actually looking at.
291
- *
292
- * Deletions are included. A deleted file's nodes may still be in the graph, and
293
- * *what called the thing you just removed* is the most useful question a diff
294
- * can be asked.
295
- */
258
+ /** Files changed between two refs, for PR analysis. `base...head` (three
259
+ * dots, not two) diffs against the merge base — two dots would include every
260
+ * file changed on base since the branch point, someone else's change. Deletions included: what called the removed thing is the useful question. */
296
261
  export async function changedFiles(cwd, base, head = "HEAD") {
297
262
  const { stdout } = await run("git", ["diff", "--name-only", "-M", `${base}...${head}`], {
298
263
  cwd,
@@ -311,4 +276,68 @@ export async function filesInCommit(cwd, sha) {
311
276
  .map((l) => l.trim())
312
277
  .filter((l) => l !== "");
313
278
  }
279
+ /**
280
+ * Which of these paths git reports as uncommitted — modified, staged or untracked.
281
+ *
282
+ * One `git status --porcelain` over the named paths rather than a call per
283
+ * file. `--porcelain` is the stable machine format, guaranteed across versions,
284
+ * and its two status columns cover every state that matters here at once: a
285
+ * file the index has and `HEAD` does not, one the working tree has and the
286
+ * index does not, and one git has never seen.
287
+ *
288
+ * The distinction this exists to make is not cosmetic. A path absent from a
289
+ * ref-to-ref diff might be uncommitted work, or it might be a file the caller
290
+ * named that this branch simply did not touch. Both make a coverage section
291
+ * describe something else; only the first is a fact about timing that the
292
+ * developer can act on in the next thirty seconds.
293
+ */
294
+ export async function uncommittedAmong(cwd, paths) {
295
+ if (paths.length === 0)
296
+ return [];
297
+ const { stdout } = await run("git", ["status", "--porcelain", "--untracked-files=all", "--", ...paths], { cwd, maxBuffer: 64 * 1024 * 1024 });
298
+ const named = new Set(paths);
299
+ const out = [];
300
+ for (const line of stdout.split("\n")) {
301
+ // `XY <path>`, and for a rename `XY <old> -> <new>`. The path starts at
302
+ // column 3; a rename's new name is what the working tree has.
303
+ if (line.length < 4)
304
+ continue;
305
+ const rest = line.slice(3).trim();
306
+ const path = rest.includes(" -> ") ? rest.slice(rest.indexOf(" -> ") + 4) : rest;
307
+ const unquoted = path.startsWith('"') && path.endsWith('"') ? path.slice(1, -1) : path;
308
+ if (named.has(unquoted))
309
+ out.push(unquoted);
310
+ }
311
+ return out.sort();
312
+ }
313
+ /**
314
+ * The paths a working tree changes, as {@link changedFiles} does for a range.
315
+ *
316
+ * Name-only, so it is the cheap counterpart to `gitWorkingTreeChange`: a caller
317
+ * that only needs to know *which* files changed should not pay for every patch.
318
+ *
319
+ * **Untracked is on by default here**, unlike the diff-producing function. That
320
+ * one is a library primitive whose existing callers must not change behaviour
321
+ * by upgrading. This one exists to answer "what is the change about to be
322
+ * committed", and a new file written and not yet added is part of that change —
323
+ * defaulting it off would mean the function misses, by default, the case it was
324
+ * written for.
325
+ */
326
+ export async function workingTreeChangedFiles(cwd, options = {}) {
327
+ const scope = options.scope ?? "all";
328
+ const revisionArgs = scope === "staged" ? ["--cached"] : scope === "all" ? ["HEAD"] : [];
329
+ const read = async (args) => {
330
+ const { stdout } = await run("git", args, { cwd, maxBuffer: 64 * 1024 * 1024 });
331
+ return stdout
332
+ .split("\n")
333
+ .map((line) => line.trim())
334
+ .filter((line) => line !== "");
335
+ };
336
+ const tracked = await read(["diff", "--name-only", "-M", ...revisionArgs]);
337
+ const untracked = options.includeUntracked === false
338
+ ? []
339
+ : await read(["ls-files", "--others", "--exclude-standard"]);
340
+ const excluded = (path) => (options.exclude ?? []).some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
341
+ return [...new Set([...tracked, ...untracked])].filter((p) => !excluded(p)).sort();
342
+ }
314
343
  //# sourceMappingURL=history.js.map