@vibe-agent-toolkit/utils 0.1.42 → 0.2.0-rc.10

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 (279) hide show
  1. package/README.md +81 -19
  2. package/dist/asset-reference.js +3 -11
  3. package/dist/asset-reference.js.map +1 -1
  4. package/dist/compare-code-units.d.ts +28 -0
  5. package/dist/compare-code-units.d.ts.map +1 -0
  6. package/dist/compare-code-units.js +33 -0
  7. package/dist/compare-code-units.js.map +1 -0
  8. package/dist/crawl-timing.d.ts +657 -0
  9. package/dist/crawl-timing.d.ts.map +1 -0
  10. package/dist/crawl-timing.js +765 -0
  11. package/dist/crawl-timing.js.map +1 -0
  12. package/dist/crawl.d.ts +2 -4
  13. package/dist/crawl.d.ts.map +1 -1
  14. package/dist/crawl.js +2 -4
  15. package/dist/crawl.js.map +1 -1
  16. package/dist/dirent-kind.d.ts +83 -0
  17. package/dist/dirent-kind.d.ts.map +1 -0
  18. package/dist/dirent-kind.js +129 -0
  19. package/dist/dirent-kind.js.map +1 -0
  20. package/dist/entrypoint.d.ts +54 -0
  21. package/dist/entrypoint.d.ts.map +1 -0
  22. package/dist/entrypoint.js +71 -0
  23. package/dist/entrypoint.js.map +1 -0
  24. package/dist/env-flag.d.ts +51 -0
  25. package/dist/env-flag.d.ts.map +1 -0
  26. package/dist/env-flag.js +62 -0
  27. package/dist/env-flag.js.map +1 -0
  28. package/dist/errors/errno.d.ts +51 -0
  29. package/dist/errors/errno.d.ts.map +1 -0
  30. package/dist/errors/errno.js +112 -0
  31. package/dist/errors/errno.js.map +1 -0
  32. package/dist/errors/vat-error.d.ts +60 -0
  33. package/dist/errors/vat-error.d.ts.map +1 -0
  34. package/dist/errors/vat-error.js +86 -0
  35. package/dist/errors/vat-error.js.map +1 -0
  36. package/dist/file-crawler.d.ts +60 -0
  37. package/dist/file-crawler.d.ts.map +1 -1
  38. package/dist/file-crawler.js +214 -24
  39. package/dist/file-crawler.js.map +1 -1
  40. package/dist/fs/file-hash.d.ts.map +1 -1
  41. package/dist/fs/file-hash.js +0 -1
  42. package/dist/fs/file-hash.js.map +1 -1
  43. package/dist/fs-utils.d.ts +628 -38
  44. package/dist/fs-utils.d.ts.map +1 -1
  45. package/dist/fs-utils.js +770 -71
  46. package/dist/fs-utils.js.map +1 -1
  47. package/dist/fs.d.ts +5 -1
  48. package/dist/fs.d.ts.map +1 -1
  49. package/dist/fs.js +9 -1
  50. package/dist/fs.js.map +1 -1
  51. package/dist/git-root-cache.d.ts +44 -0
  52. package/dist/git-root-cache.d.ts.map +1 -0
  53. package/dist/git-root-cache.js +68 -0
  54. package/dist/git-root-cache.js.map +1 -0
  55. package/dist/git-run.d.ts +167 -0
  56. package/dist/git-run.d.ts.map +1 -0
  57. package/dist/git-run.js +154 -0
  58. package/dist/git-run.js.map +1 -0
  59. package/dist/git-snapshot.d.ts +199 -0
  60. package/dist/git-snapshot.d.ts.map +1 -0
  61. package/dist/git-snapshot.js +250 -0
  62. package/dist/git-snapshot.js.map +1 -0
  63. package/dist/git-tracker.d.ts +166 -3
  64. package/dist/git-tracker.d.ts.map +1 -1
  65. package/dist/git-tracker.js +250 -22
  66. package/dist/git-tracker.js.map +1 -1
  67. package/dist/git-utils.d.ts +95 -1
  68. package/dist/git-utils.d.ts.map +1 -1
  69. package/dist/git-utils.js +227 -81
  70. package/dist/git-utils.js.map +1 -1
  71. package/dist/git.d.ts +13 -4
  72. package/dist/git.d.ts.map +1 -1
  73. package/dist/git.js +12 -4
  74. package/dist/git.js.map +1 -1
  75. package/dist/gitignore-checker.d.ts.map +1 -1
  76. package/dist/gitignore-checker.js +15 -6
  77. package/dist/gitignore-checker.js.map +1 -1
  78. package/dist/glob/glob-pattern.js +5 -5
  79. package/dist/glob/glob-pattern.js.map +1 -1
  80. package/dist/index.d.ts +33 -19
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +113 -33
  83. package/dist/index.js.map +1 -1
  84. package/dist/listing-refusal.d.ts +112 -0
  85. package/dist/listing-refusal.d.ts.map +1 -0
  86. package/dist/listing-refusal.js +105 -0
  87. package/dist/listing-refusal.js.map +1 -0
  88. package/dist/numeric-args.d.ts +24 -0
  89. package/dist/numeric-args.d.ts.map +1 -0
  90. package/dist/numeric-args.js +37 -0
  91. package/dist/numeric-args.js.map +1 -0
  92. package/dist/path-containment.d.ts +64 -0
  93. package/dist/path-containment.d.ts.map +1 -0
  94. package/dist/path-containment.js +111 -0
  95. package/dist/path-containment.js.map +1 -0
  96. package/dist/path-core.d.ts +143 -18
  97. package/dist/path-core.d.ts.map +1 -1
  98. package/dist/path-core.js +171 -23
  99. package/dist/path-core.js.map +1 -1
  100. package/dist/path-utils.d.ts.map +1 -1
  101. package/dist/path-utils.js +42 -47
  102. package/dist/path-utils.js.map +1 -1
  103. package/dist/path.d.ts +1 -1
  104. package/dist/path.d.ts.map +1 -1
  105. package/dist/path.js +1 -1
  106. package/dist/path.js.map +1 -1
  107. package/dist/process.d.ts +7 -0
  108. package/dist/process.d.ts.map +1 -1
  109. package/dist/process.js +7 -0
  110. package/dist/process.js.map +1 -1
  111. package/dist/project-utils.d.ts +7 -1
  112. package/dist/project-utils.d.ts.map +1 -1
  113. package/dist/project-utils.js +24 -10
  114. package/dist/project-utils.js.map +1 -1
  115. package/dist/project.d.ts +7 -5
  116. package/dist/project.d.ts.map +1 -1
  117. package/dist/project.js +7 -5
  118. package/dist/project.js.map +1 -1
  119. package/dist/safe-exec.d.ts +40 -11
  120. package/dist/safe-exec.d.ts.map +1 -1
  121. package/dist/safe-exec.js +111 -25
  122. package/dist/safe-exec.js.map +1 -1
  123. package/dist/skill-targets.d.ts +2 -2
  124. package/dist/skill-targets.js +2 -2
  125. package/dist/skill-test/auth-resolver.d.ts +10 -2
  126. package/dist/skill-test/auth-resolver.d.ts.map +1 -1
  127. package/dist/skill-test/auth-resolver.js +26 -11
  128. package/dist/skill-test/auth-resolver.js.map +1 -1
  129. package/dist/skill-test/index.d.ts +1 -1
  130. package/dist/skill-test/index.d.ts.map +1 -1
  131. package/dist/skill-test/index.js +1 -1
  132. package/dist/skill-test/index.js.map +1 -1
  133. package/dist/skill-test/spawn-claude.d.ts +30 -1
  134. package/dist/skill-test/spawn-claude.d.ts.map +1 -1
  135. package/dist/skill-test/spawn-claude.js +71 -5
  136. package/dist/skill-test/spawn-claude.js.map +1 -1
  137. package/dist/skill-test/transcript.d.ts +41 -2
  138. package/dist/skill-test/transcript.d.ts.map +1 -1
  139. package/dist/skill-test/transcript.js +55 -10
  140. package/dist/skill-test/transcript.js.map +1 -1
  141. package/dist/spawn-hardened.js +0 -1
  142. package/dist/spawn-hardened.js.map +1 -1
  143. package/dist/stdio-blocking.js +13 -14
  144. package/dist/stdio-blocking.js.map +1 -1
  145. package/dist/test-helpers.d.ts +130 -79
  146. package/dist/test-helpers.d.ts.map +1 -1
  147. package/dist/test-helpers.js +229 -124
  148. package/dist/test-helpers.js.map +1 -1
  149. package/dist/testing/executables.d.ts +32 -0
  150. package/dist/testing/executables.d.ts.map +1 -0
  151. package/dist/testing/executables.js +70 -0
  152. package/dist/testing/executables.js.map +1 -0
  153. package/dist/testing/hostile-tree.d.ts +79 -0
  154. package/dist/testing/hostile-tree.d.ts.map +1 -0
  155. package/dist/testing/hostile-tree.js +164 -0
  156. package/dist/testing/hostile-tree.js.map +1 -0
  157. package/dist/testing/platform-gates.d.ts +31 -0
  158. package/dist/testing/platform-gates.d.ts.map +1 -0
  159. package/dist/testing/platform-gates.js +31 -0
  160. package/dist/testing/platform-gates.js.map +1 -0
  161. package/dist/testing/temp-dir.d.ts +255 -0
  162. package/dist/testing/temp-dir.d.ts.map +1 -0
  163. package/dist/testing/temp-dir.js +363 -0
  164. package/dist/testing/temp-dir.js.map +1 -0
  165. package/dist/testing.d.ts +94 -3
  166. package/dist/testing.d.ts.map +1 -1
  167. package/dist/testing.js +119 -3
  168. package/dist/testing.js.map +1 -1
  169. package/dist/text-content.d.ts +197 -0
  170. package/dist/text-content.d.ts.map +1 -0
  171. package/dist/text-content.js +354 -0
  172. package/dist/text-content.js.map +1 -0
  173. package/dist/text-file.d.ts +45 -0
  174. package/dist/text-file.d.ts.map +1 -0
  175. package/dist/text-file.js +51 -0
  176. package/dist/text-file.js.map +1 -0
  177. package/dist/text.d.ts +16 -0
  178. package/dist/text.d.ts.map +1 -0
  179. package/dist/text.js +16 -0
  180. package/dist/text.js.map +1 -0
  181. package/dist/timing-dump.d.ts +136 -0
  182. package/dist/timing-dump.d.ts.map +1 -0
  183. package/dist/timing-dump.js +229 -0
  184. package/dist/timing-dump.js.map +1 -0
  185. package/dist/windows-shell.d.ts +0 -1
  186. package/dist/windows-shell.d.ts.map +1 -1
  187. package/dist/windows-shell.js +0 -1
  188. package/dist/windows-shell.js.map +1 -1
  189. package/dist/zod-introspection.js +2 -2
  190. package/dist/zod-introspection.js.map +1 -1
  191. package/eslint/README.md +224 -50
  192. package/eslint/index.cjs +111 -97
  193. package/eslint/index.d.cts +16 -6
  194. package/eslint/rules/callee-name.cjs +23 -0
  195. package/eslint/rules/commands-import-boundary.cjs +148 -0
  196. package/eslint/rules/dead-import.cjs +62 -12
  197. package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
  198. package/eslint/rules/eslint-rule-factory.cjs +21 -3
  199. package/eslint/rules/exempt-path-matcher.cjs +2 -1
  200. package/eslint/rules/explicit-zod-strictness.cjs +120 -0
  201. package/eslint/rules/no-bare-dynamic-import-path.cjs +5 -1
  202. package/eslint/rules/no-bare-executable-spawn.cjs +70 -0
  203. package/eslint/rules/no-bare-symlink-in-tests.cjs +213 -0
  204. package/eslint/rules/no-blind-catch.cjs +171 -0
  205. package/eslint/rules/no-child-process-execSync.cjs +8 -0
  206. package/eslint/rules/no-decaying-referent.cjs +217 -0
  207. package/eslint/rules/no-dotdot-containment.cjs +160 -0
  208. package/eslint/rules/no-file-url-string-concat.cjs +4 -1
  209. package/eslint/rules/no-fragile-entrypoint-guard.cjs +228 -0
  210. package/eslint/rules/no-fs-mkdirSync.cjs +8 -0
  211. package/eslint/rules/no-fs-promises-cp.cjs +7 -0
  212. package/eslint/rules/no-fs-realpathSync.cjs +8 -0
  213. package/eslint/rules/no-hardcoded-path-split.cjs +29 -18
  214. package/eslint/rules/no-io-in-unit-tier.cjs +129 -0
  215. package/eslint/rules/no-literal-process-exit.cjs +129 -0
  216. package/eslint/rules/no-manual-path-normalize.cjs +146 -96
  217. package/eslint/rules/no-os-tmpdir.cjs +8 -0
  218. package/eslint/rules/no-path-operations-in-comparisons.cjs +7 -2
  219. package/eslint/rules/no-path-sep-in-strings.cjs +7 -2
  220. package/eslint/rules/no-path-startswith.cjs +7 -2
  221. package/eslint/rules/no-process-exit-in-phase.cjs +127 -0
  222. package/eslint/rules/no-raw-node-path.cjs +563 -0
  223. package/eslint/rules/no-raw-text-decode.cjs +305 -0
  224. package/eslint/rules/no-registry-count-pin.cjs +255 -0
  225. package/eslint/rules/no-self-package-import.cjs +168 -0
  226. package/eslint/rules/no-test-scoped-functions.cjs +10 -3
  227. package/eslint/rules/no-unix-shell-commands.cjs +6 -2
  228. package/eslint/rules/no-unsafe-root-join.cjs +23 -2
  229. package/eslint/rules/no-url-pathname-for-fs.cjs +5 -1
  230. package/eslint/rules/no-version-literal.cjs +151 -0
  231. package/eslint/rules/prefer-startswith-over-regex.cjs +33 -1
  232. package/eslint/rules/require-justified-skip.cjs +8 -2
  233. package/eslint/rules/safe-import.cjs +1 -1
  234. package/package.json +16 -16
  235. package/dist/link-auth/build-headers.d.ts +0 -34
  236. package/dist/link-auth/build-headers.d.ts.map +0 -1
  237. package/dist/link-auth/build-headers.js +0 -58
  238. package/dist/link-auth/build-headers.js.map +0 -1
  239. package/dist/link-auth/expand-macro.d.ts +0 -38
  240. package/dist/link-auth/expand-macro.d.ts.map +0 -1
  241. package/dist/link-auth/expand-macro.js +0 -133
  242. package/dist/link-auth/expand-macro.js.map +0 -1
  243. package/dist/link-auth/macros.yaml +0 -50
  244. package/dist/link-auth/resolve-token.d.ts +0 -83
  245. package/dist/link-auth/resolve-token.d.ts.map +0 -1
  246. package/dist/link-auth/resolve-token.js +0 -101
  247. package/dist/link-auth/resolve-token.js.map +0 -1
  248. package/dist/link-auth/resolve.d.ts +0 -102
  249. package/dist/link-auth/resolve.d.ts.map +0 -1
  250. package/dist/link-auth/resolve.js +0 -66
  251. package/dist/link-auth/resolve.js.map +0 -1
  252. package/dist/link-auth/rewrite.d.ts +0 -52
  253. package/dist/link-auth/rewrite.d.ts.map +0 -1
  254. package/dist/link-auth/rewrite.js +0 -102
  255. package/dist/link-auth/rewrite.js.map +0 -1
  256. package/dist/link-auth/select-provider.d.ts +0 -30
  257. package/dist/link-auth/select-provider.d.ts.map +0 -1
  258. package/dist/link-auth/select-provider.js +0 -55
  259. package/dist/link-auth/select-provider.js.map +0 -1
  260. package/dist/link-auth/template.d.ts +0 -40
  261. package/dist/link-auth/template.d.ts.map +0 -1
  262. package/dist/link-auth/template.js +0 -89
  263. package/dist/link-auth/template.js.map +0 -1
  264. package/dist/link-auth/transforms.d.ts +0 -46
  265. package/dist/link-auth/transforms.d.ts.map +0 -1
  266. package/dist/link-auth/transforms.js +0 -52
  267. package/dist/link-auth/transforms.js.map +0 -1
  268. package/dist/template-entry.d.ts +0 -10
  269. package/dist/template-entry.d.ts.map +0 -1
  270. package/dist/template-entry.js +0 -10
  271. package/dist/template-entry.js.map +0 -1
  272. package/dist/template.d.ts +0 -7
  273. package/dist/template.d.ts.map +0 -1
  274. package/dist/template.js +0 -18
  275. package/dist/template.js.map +0 -1
  276. package/eslint/rules/no-path-join.cjs +0 -14
  277. package/eslint/rules/no-path-relative.cjs +0 -14
  278. package/eslint/rules/no-path-resolve.cjs +0 -14
  279. package/eslint/rules/path-function-rule-factory.cjs +0 -417
@@ -0,0 +1,199 @@
1
+ /**
2
+ * A **dirty-corrected git tree snapshot**, rebased onto absolute paths.
3
+ *
4
+ * `@vibe-validate/git`'s `getGitTreeSnapshot` answers the question git is
5
+ * uniquely good at: every path git can see under a directory, each with a blob
6
+ * OID naming *the bytes that are actually on disk right now* — dirty working-tree
7
+ * edits included, not the stale committed-index SHA — plus one deterministic
8
+ * `write-tree` hash over the whole set. Membership is `git add --all` without
9
+ * `--force`, i.e. `tracked ∪ (untracked ∧ ¬ignored)`.
10
+ *
11
+ * This module is the thin layer VAT needs on top, and it exists for one reason
12
+ * that is easy to get wrong: **git spells those paths relative to the repository
13
+ * root, not to the directory you asked about.** A caller scanning
14
+ * `<repo>/packages/foo` gets back `packages/foo/x.md`, and a caller that joined
15
+ * those onto its own root would build `<repo>/packages/foo/packages/foo/x.md` —
16
+ * a path that does not exist, so every consumer reads it as "absent" rather than
17
+ * as a bug. Resolving against the repository root here makes that unrepresentable
18
+ * at the call site.
19
+ *
20
+ * ## ⚠️ This is not a pure read
21
+ *
22
+ * Taking a snapshot runs `git add --all` against a throwaway index, which
23
+ * **writes loose blob objects into the target repository's `.git/objects`** for
24
+ * any content git has not already stored. The real index and working tree are
25
+ * never touched, and the objects are ordinary unreferenced blobs that `git gc`
26
+ * reclaims — but a command that advertises itself as read-only is, at the byte
27
+ * level, not. `vibe-validate` accepts this cost on every commit it gates; a VAT
28
+ * lane that adopts it inherits the same trade rather than a different one.
29
+ *
30
+ * ## ⚠️ A symlink's OID names its TARGET STRING, not a file's bytes
31
+ *
32
+ * Git stores a symlink as a blob whose content is the link target, under mode
33
+ * `120000`. Two links with the same relative target but different resolutions
34
+ * therefore share an OID while a consumer that follows them reads two different
35
+ * documents. {@link GitSnapshotEntry.isSymlink} is computed here precisely so
36
+ * that a consumer keying work off `oid` can exclude them — see
37
+ * `packages/resources/src/content-key.ts`, whose standing rule is that a git SHA
38
+ * may be a *lookup hint whose miss is free* and must never be the key itself.
39
+ *
40
+ * ## ⚠️ Two consumers want the SAME snapshot — see {@link withGitSnapshotCache}
41
+ *
42
+ * One command takes this snapshot twice, of one repository, sequentially, and
43
+ * keeps a different half of each: the projection store keeps `hash` as its
44
+ * cache key, and the git crawl source keeps `entries`. Bracketing the command
45
+ * makes that one snapshot. Everything outside the bracket still pays per call,
46
+ * deliberately.
47
+ */
48
+ /** One path in a {@link GitTreeSnapshot}, located absolutely. */
49
+ export interface GitSnapshotEntry {
50
+ /** Absolute, forward-slashed path, resolved against the repository root. */
51
+ absolutePath: string;
52
+ /**
53
+ * Blob OID for this path's **on-disk** bytes.
54
+ *
55
+ * Equal OIDs mean equal bytes, which is what makes this usable as a lookup
56
+ * hint. It is NOT usable as a content key — see the module docstring.
57
+ */
58
+ oid: string;
59
+ /** Git's six-digit mode, verbatim: `100644`, `100755`, `120000`, `160000`. */
60
+ mode: string;
61
+ /** Mode `120000`. Its {@link GitSnapshotEntry.oid} is the target string. */
62
+ isSymlink: boolean;
63
+ /**
64
+ * Mode `160000` — a submodule.
65
+ *
66
+ * The OID is a **commit**, not a blob, so `cat-file` on it yields no file
67
+ * bytes, and none of the submodule's own files appear anywhere in the
68
+ * snapshot. A consumer that needs them takes a second snapshot rooted there.
69
+ */
70
+ isSubmodule: boolean;
71
+ }
72
+ /** One repository's snapshot. */
73
+ export interface GitTreeSnapshot {
74
+ /**
75
+ * `git write-tree` over the snapshot — a deterministic key for the whole set.
76
+ *
77
+ * Byte-identical content always produces it, because a tree object carries no
78
+ * timestamp. (A `stash create` would not: a stash is a commit, and two calls
79
+ * over identical content agree only within the same wall-clock second.)
80
+ */
81
+ hash: string;
82
+ /** The repository root every entry was resolved against, forward-slashed. */
83
+ repositoryRoot: string;
84
+ /** Every path git can see, in git's own order. */
85
+ entries: GitSnapshotEntry[];
86
+ }
87
+ /**
88
+ * Run work that may snapshot the same repository more than once, and pay for
89
+ * each repository ONCE.
90
+ *
91
+ * ## What this buys, in both currencies
92
+ *
93
+ * A snapshot is not a read — it copies the index, runs `git add --all` into it
94
+ * and then `git write-tree`. One `vat` command took two of them, of the same
95
+ * repository, back to back: `openPopulationCache` needs `hash` to key the
96
+ * projection store, and `GitCrawlSource` needs `entries` to enumerate. Measured
97
+ * on a large monorepo, the pair cost 195.22 ms and 158.55 ms.
98
+ *
99
+ * The cost is the smaller half. Two snapshots taken 195 ms apart are two
100
+ * *different answers* whenever the working tree changes in between, and the
101
+ * command then files the second one's extent under the first one's key — a
102
+ * cache entry whose key does not describe its contents, produced silently and
103
+ * discoverable only as a wrong answer much later. One snapshot removes the race
104
+ * rather than merely the duplicate.
105
+ *
106
+ * ## Where to open it
107
+ *
108
+ * At the level that already brackets the whole command, so that it encloses
109
+ * *every* snapshotting consumer. Opened deeper than one of them, the dedupe
110
+ * silently does nothing and looks exactly like a dedupe that works.
111
+ *
112
+ * ## What it deliberately does NOT do
113
+ *
114
+ * Inside the bracket, a working-tree edit made between two calls is not
115
+ * observed by the second — the first snapshot is the answer for the whole
116
+ * bracket. That is the race being closed, not a limitation to work around. Work
117
+ * that must see edits as they land does not belong inside one bracket.
118
+ *
119
+ * Nesting is safe and inner-most wins: an inner bracket starts an empty memo,
120
+ * and the outer one's entries are restored when it returns.
121
+ *
122
+ * @param run - The work to run with the memo open. Sync or async — the return
123
+ * value is passed straight through, so an async `run` keeps the memo for the
124
+ * whole promise it returns
125
+ * @returns Whatever `run` returned
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * return withGitSnapshotCache(async () => {
130
+ * const opened = await openPopulationCache(options); // snapshots
131
+ * return work(opened?.cache); // crawls, snapshots
132
+ * });
133
+ * ```
134
+ */
135
+ export declare function withGitSnapshotCache<T>(run: () => T): T;
136
+ /**
137
+ * Snapshot everything git can see under a directory.
138
+ *
139
+ * Inside a {@link withGitSnapshotCache} bracket the answer for a given
140
+ * repository is taken once and reused — including a `null`. Outside one, every
141
+ * call spawns git, which is what it has always done.
142
+ *
143
+ * @param options - Where to look
144
+ * @param options.cwd - Any directory inside the repository of interest. Git
145
+ * resolves upward to the worktree root, and the snapshot covers that whole
146
+ * root — NOT only the subtree named here. Narrowing is the caller's job, and
147
+ * is why {@link GitSnapshotEntry.absolutePath} is absolute
148
+ * @returns The snapshot, or `null` when git could not answer — no `git` on
149
+ * `PATH`, not a repository, a bare or unreadable one. An empty `entries` is a
150
+ * real answer (an initialized repository with no files) and stays
151
+ * distinguishable from it
152
+ */
153
+ export declare function gitTreeSnapshot(options: {
154
+ cwd: string;
155
+ }): GitTreeSnapshot | null;
156
+ /**
157
+ * A snapshot taken NOW, whatever bracket is open — never read from or written to
158
+ * its memo.
159
+ *
160
+ * ⛔ The bracket makes two consumers agree with EACH OTHER; it cannot make them
161
+ * agree with files read later. A population keyed inside a bracket reads the
162
+ * working tree afterwards, so an edit landing in between is read and then filed
163
+ * under the pre-edit key. This is how a caller checks, before filing, that the
164
+ * bracket's answer still describes the tree. It costs a full snapshot, so it
165
+ * belongs on the write path, never on a hit.
166
+ *
167
+ * @param options - Where to look
168
+ * @param options.cwd - Any directory inside the repository of interest
169
+ * @returns The snapshot, or `null` when git could not answer
170
+ */
171
+ export declare function freshGitTreeSnapshot(options: {
172
+ cwd: string;
173
+ }): GitTreeSnapshot | null;
174
+ /**
175
+ * The snapshot this bracket ALREADY holds for a repository — never a new one.
176
+ *
177
+ * The difference from {@link gitTreeSnapshot} is the whole reason this exists:
178
+ * that function TAKES a snapshot when the memo misses, and a snapshot is three
179
+ * spawns and a `git add --all`. This one only ever reports what a previous
180
+ * caller already paid for, so a consumer can prefer it and fall back to its own
181
+ * cheaper question without any risk of turning a miss into the most expensive
182
+ * git operation in the package.
183
+ *
184
+ * That asymmetry is what makes it safe to reach for from shared code. The
185
+ * {@link GitTracker} does exactly this: inside the projection's git lane a
186
+ * snapshot is already in hand and describes precisely the set its own
187
+ * `git ls-files --cached --others --exclude-standard` would spawn to rebuild,
188
+ * while on the incumbent walk no snapshot is ever taken and the tracker must
189
+ * keep spawning. One consumer, two lanes, and neither pays for the other's
190
+ * strategy.
191
+ *
192
+ * @param cwd - Any directory inside the repository of interest
193
+ * @returns The memoized snapshot, or `undefined` when this bracket has none —
194
+ * which includes there being no bracket open at all, and includes a memoized
195
+ * `null` (git could not answer). Callers treat all three the same way: ask
196
+ * your own question instead
197
+ */
198
+ export declare function peekGitTreeSnapshot(cwd: string): GitTreeSnapshot | undefined;
199
+ //# sourceMappingURL=git-snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-snapshot.d.ts","sourceRoot":"","sources":["../src/git-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AASH,iEAAiE;AACjE,MAAM,WAAW,gBAAgB;IAC/B,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,iCAAiC;AACjC,MAAM,WAAW,eAAe;IAC9B;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,cAAc,EAAE,MAAM,CAAC;IACvB,kDAAkD;IAClD,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAuBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAEvD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,eAAe,GAAG,IAAI,CA2BhF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,eAAe,GAAG,IAAI,CAErF;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAY5E"}
@@ -0,0 +1,250 @@
1
+ /**
2
+ * A **dirty-corrected git tree snapshot**, rebased onto absolute paths.
3
+ *
4
+ * `@vibe-validate/git`'s `getGitTreeSnapshot` answers the question git is
5
+ * uniquely good at: every path git can see under a directory, each with a blob
6
+ * OID naming *the bytes that are actually on disk right now* — dirty working-tree
7
+ * edits included, not the stale committed-index SHA — plus one deterministic
8
+ * `write-tree` hash over the whole set. Membership is `git add --all` without
9
+ * `--force`, i.e. `tracked ∪ (untracked ∧ ¬ignored)`.
10
+ *
11
+ * This module is the thin layer VAT needs on top, and it exists for one reason
12
+ * that is easy to get wrong: **git spells those paths relative to the repository
13
+ * root, not to the directory you asked about.** A caller scanning
14
+ * `<repo>/packages/foo` gets back `packages/foo/x.md`, and a caller that joined
15
+ * those onto its own root would build `<repo>/packages/foo/packages/foo/x.md` —
16
+ * a path that does not exist, so every consumer reads it as "absent" rather than
17
+ * as a bug. Resolving against the repository root here makes that unrepresentable
18
+ * at the call site.
19
+ *
20
+ * ## ⚠️ This is not a pure read
21
+ *
22
+ * Taking a snapshot runs `git add --all` against a throwaway index, which
23
+ * **writes loose blob objects into the target repository's `.git/objects`** for
24
+ * any content git has not already stored. The real index and working tree are
25
+ * never touched, and the objects are ordinary unreferenced blobs that `git gc`
26
+ * reclaims — but a command that advertises itself as read-only is, at the byte
27
+ * level, not. `vibe-validate` accepts this cost on every commit it gates; a VAT
28
+ * lane that adopts it inherits the same trade rather than a different one.
29
+ *
30
+ * ## ⚠️ A symlink's OID names its TARGET STRING, not a file's bytes
31
+ *
32
+ * Git stores a symlink as a blob whose content is the link target, under mode
33
+ * `120000`. Two links with the same relative target but different resolutions
34
+ * therefore share an OID while a consumer that follows them reads two different
35
+ * documents. {@link GitSnapshotEntry.isSymlink} is computed here precisely so
36
+ * that a consumer keying work off `oid` can exclude them — see
37
+ * `packages/resources/src/content-key.ts`, whose standing rule is that a git SHA
38
+ * may be a *lookup hint whose miss is free* and must never be the key itself.
39
+ *
40
+ * ## ⚠️ Two consumers want the SAME snapshot — see {@link withGitSnapshotCache}
41
+ *
42
+ * One command takes this snapshot twice, of one repository, sequentially, and
43
+ * keeps a different half of each: the projection store keeps `hash` as its
44
+ * cache key, and the git crawl source keeps `entries`. Bracketing the command
45
+ * makes that one snapshot. Everything outside the bracket still pays per call,
46
+ * deliberately.
47
+ */
48
+ import { AsyncLocalStorage } from 'node:async_hooks';
49
+ import { getGitTreeSnapshot, GIT_MODE_GITLINK, GIT_MODE_SYMLINK } from '@vibe-validate/git';
50
+ import { gitFindRoot } from './git-utils.js';
51
+ import { safePath } from './path-utils.js';
52
+ /**
53
+ * The snapshots taken inside the bracket now open, keyed by repository root.
54
+ *
55
+ * `AsyncLocalStorage` rather than a module-level `Map`, and the distinction is
56
+ * the whole design. A blanket memo would dedupe the same two calls — and would
57
+ * then hand a stale snapshot to anything that mutates a repository and
58
+ * re-snapshots it in the same process, which is what a vitest worker running
59
+ * many suites does all day. Scoping it to a bracket makes the memo's lifetime
60
+ * something a caller states rather than something the module assumes; outside
61
+ * one, {@link gitTreeSnapshot} behaves exactly as it did before this existed.
62
+ *
63
+ * Async-scoped rather than a plain variable because the bracket spans a whole
64
+ * command's `await`s, and two commands may be open at once in one process —
65
+ * `crawl-timing.ts` reaches for the same primitive for the same reason.
66
+ *
67
+ * `null` is a stored value, not an absence: "git could not answer" costs a full
68
+ * `add`/`write-tree` attempt to discover, so repeating it is pure cost. Absence
69
+ * is `undefined`, which `Map.get` returns and this module never stores.
70
+ */
71
+ const snapshotsInBracket = new AsyncLocalStorage();
72
+ /**
73
+ * Run work that may snapshot the same repository more than once, and pay for
74
+ * each repository ONCE.
75
+ *
76
+ * ## What this buys, in both currencies
77
+ *
78
+ * A snapshot is not a read — it copies the index, runs `git add --all` into it
79
+ * and then `git write-tree`. One `vat` command took two of them, of the same
80
+ * repository, back to back: `openPopulationCache` needs `hash` to key the
81
+ * projection store, and `GitCrawlSource` needs `entries` to enumerate. Measured
82
+ * on a large monorepo, the pair cost 195.22 ms and 158.55 ms.
83
+ *
84
+ * The cost is the smaller half. Two snapshots taken 195 ms apart are two
85
+ * *different answers* whenever the working tree changes in between, and the
86
+ * command then files the second one's extent under the first one's key — a
87
+ * cache entry whose key does not describe its contents, produced silently and
88
+ * discoverable only as a wrong answer much later. One snapshot removes the race
89
+ * rather than merely the duplicate.
90
+ *
91
+ * ## Where to open it
92
+ *
93
+ * At the level that already brackets the whole command, so that it encloses
94
+ * *every* snapshotting consumer. Opened deeper than one of them, the dedupe
95
+ * silently does nothing and looks exactly like a dedupe that works.
96
+ *
97
+ * ## What it deliberately does NOT do
98
+ *
99
+ * Inside the bracket, a working-tree edit made between two calls is not
100
+ * observed by the second — the first snapshot is the answer for the whole
101
+ * bracket. That is the race being closed, not a limitation to work around. Work
102
+ * that must see edits as they land does not belong inside one bracket.
103
+ *
104
+ * Nesting is safe and inner-most wins: an inner bracket starts an empty memo,
105
+ * and the outer one's entries are restored when it returns.
106
+ *
107
+ * @param run - The work to run with the memo open. Sync or async — the return
108
+ * value is passed straight through, so an async `run` keeps the memo for the
109
+ * whole promise it returns
110
+ * @returns Whatever `run` returned
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * return withGitSnapshotCache(async () => {
115
+ * const opened = await openPopulationCache(options); // snapshots
116
+ * return work(opened?.cache); // crawls, snapshots
117
+ * });
118
+ * ```
119
+ */
120
+ export function withGitSnapshotCache(run) {
121
+ return snapshotsInBracket.run(new Map(), run);
122
+ }
123
+ /**
124
+ * Snapshot everything git can see under a directory.
125
+ *
126
+ * Inside a {@link withGitSnapshotCache} bracket the answer for a given
127
+ * repository is taken once and reused — including a `null`. Outside one, every
128
+ * call spawns git, which is what it has always done.
129
+ *
130
+ * @param options - Where to look
131
+ * @param options.cwd - Any directory inside the repository of interest. Git
132
+ * resolves upward to the worktree root, and the snapshot covers that whole
133
+ * root — NOT only the subtree named here. Narrowing is the caller's job, and
134
+ * is why {@link GitSnapshotEntry.absolutePath} is absolute
135
+ * @returns The snapshot, or `null` when git could not answer — no `git` on
136
+ * `PATH`, not a repository, a bare or unreadable one. An empty `entries` is a
137
+ * real answer (an initialized repository with no files) and stays
138
+ * distinguishable from it
139
+ */
140
+ export function gitTreeSnapshot(options) {
141
+ // Resolved from the filesystem BEFORE spawning anything, and deliberately not
142
+ // from a `rev-parse` of our own: `getGitTreeSnapshot` has already paid for that
143
+ // question internally, and asking it twice invites the two answers to disagree
144
+ // on a worktree, where the git directory and the worktree root are different
145
+ // places. `gitFindRoot` is memoized, so this is free after the first call.
146
+ const repositoryRoot = gitFindRoot(options.cwd);
147
+ if (repositoryRoot === null) {
148
+ // Not memoized, and it does not need to be: this branch spawns nothing, and
149
+ // `gitFindRoot` has its own memo. There is also no key to file it under —
150
+ // the memo is keyed by repository root, and this is the case with none.
151
+ return null;
152
+ }
153
+ // Keyed by the RESOLVED root rather than by `options.cwd`, because a snapshot
154
+ // covers the whole repository however deep the directory it was asked from.
155
+ // The two call sites this bracket exists for pass different directories — the
156
+ // corpus root and the project root — so keying on `cwd` would produce two
157
+ // entries holding the identical answer, i.e. a dedupe that does nothing in
158
+ // exactly the case it was written for.
159
+ const memo = snapshotsInBracket.getStore();
160
+ const memoized = memo?.get(repositoryRoot);
161
+ if (memoized !== undefined)
162
+ return memoized;
163
+ const snapshot = takeSnapshot(options.cwd, repositoryRoot);
164
+ memo?.set(repositoryRoot, snapshot);
165
+ return snapshot;
166
+ }
167
+ /**
168
+ * A snapshot taken NOW, whatever bracket is open — never read from or written to
169
+ * its memo.
170
+ *
171
+ * ⛔ The bracket makes two consumers agree with EACH OTHER; it cannot make them
172
+ * agree with files read later. A population keyed inside a bracket reads the
173
+ * working tree afterwards, so an edit landing in between is read and then filed
174
+ * under the pre-edit key. This is how a caller checks, before filing, that the
175
+ * bracket's answer still describes the tree. It costs a full snapshot, so it
176
+ * belongs on the write path, never on a hit.
177
+ *
178
+ * @param options - Where to look
179
+ * @param options.cwd - Any directory inside the repository of interest
180
+ * @returns The snapshot, or `null` when git could not answer
181
+ */
182
+ export function freshGitTreeSnapshot(options) {
183
+ return snapshotsInBracket.exit(() => gitTreeSnapshot(options));
184
+ }
185
+ /**
186
+ * The snapshot this bracket ALREADY holds for a repository — never a new one.
187
+ *
188
+ * The difference from {@link gitTreeSnapshot} is the whole reason this exists:
189
+ * that function TAKES a snapshot when the memo misses, and a snapshot is three
190
+ * spawns and a `git add --all`. This one only ever reports what a previous
191
+ * caller already paid for, so a consumer can prefer it and fall back to its own
192
+ * cheaper question without any risk of turning a miss into the most expensive
193
+ * git operation in the package.
194
+ *
195
+ * That asymmetry is what makes it safe to reach for from shared code. The
196
+ * {@link GitTracker} does exactly this: inside the projection's git lane a
197
+ * snapshot is already in hand and describes precisely the set its own
198
+ * `git ls-files --cached --others --exclude-standard` would spawn to rebuild,
199
+ * while on the incumbent walk no snapshot is ever taken and the tracker must
200
+ * keep spawning. One consumer, two lanes, and neither pays for the other's
201
+ * strategy.
202
+ *
203
+ * @param cwd - Any directory inside the repository of interest
204
+ * @returns The memoized snapshot, or `undefined` when this bracket has none —
205
+ * which includes there being no bracket open at all, and includes a memoized
206
+ * `null` (git could not answer). Callers treat all three the same way: ask
207
+ * your own question instead
208
+ */
209
+ export function peekGitTreeSnapshot(cwd) {
210
+ const memo = snapshotsInBracket.getStore();
211
+ if (memo === undefined)
212
+ return undefined;
213
+ // Resolved the same way `gitTreeSnapshot` resolves it, because the memo is
214
+ // keyed by repository root and not by the directory anyone asked from. A peek
215
+ // keyed on `cwd` would miss every time the snapshot was taken from a different
216
+ // depth — which is the normal case, and a miss here is silent.
217
+ const repositoryRoot = gitFindRoot(cwd);
218
+ if (repositoryRoot === null)
219
+ return undefined;
220
+ return memo.get(repositoryRoot) ?? undefined;
221
+ }
222
+ /**
223
+ * Ask git, and rebase the answer onto absolute paths.
224
+ *
225
+ * Split out so {@link gitTreeSnapshot} reads as "key, look up, or take one" —
226
+ * the memo has to sit above the spawn, and a memo interleaved with the mapping
227
+ * is how a later edit ends up caching the wrong half.
228
+ *
229
+ * @param cwd - The directory to ask git from
230
+ * @param repositoryRoot - The already-resolved root every entry is rebased onto
231
+ * @returns The snapshot, or `null` when git could not answer
232
+ */
233
+ function takeSnapshot(cwd, repositoryRoot) {
234
+ const snapshot = getGitTreeSnapshot({ cwd });
235
+ if (snapshot === null) {
236
+ return null;
237
+ }
238
+ return {
239
+ hash: snapshot.hash,
240
+ repositoryRoot,
241
+ entries: snapshot.entries.map((entry) => ({
242
+ absolutePath: safePath.resolve(repositoryRoot, entry.path),
243
+ oid: entry.oid,
244
+ mode: entry.mode,
245
+ isSymlink: entry.mode === GIT_MODE_SYMLINK,
246
+ isSubmodule: entry.mode === GIT_MODE_GITLINK,
247
+ })),
248
+ };
249
+ }
250
+ //# sourceMappingURL=git-snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-snapshot.js","sourceRoot":"","sources":["../src/git-snapshot.ts"],"names":[],"mappings":"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"}