@vibe-agent-toolkit/utils 0.2.0-rc.1 → 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 (270) hide show
  1. package/README.md +69 -23
  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 +133 -29
  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 +422 -191
  44. package/dist/fs-utils.d.ts.map +1 -1
  45. package/dist/fs-utils.js +534 -204
  46. package/dist/fs-utils.js.map +1 -1
  47. package/dist/fs.d.ts +5 -2
  48. package/dist/fs.d.ts.map +1 -1
  49. package/dist/fs.js +9 -7
  50. package/dist/fs.js.map +1 -1
  51. package/dist/git-run.d.ts +167 -0
  52. package/dist/git-run.d.ts.map +1 -0
  53. package/dist/git-run.js +154 -0
  54. package/dist/git-run.js.map +1 -0
  55. package/dist/git-snapshot.d.ts +199 -0
  56. package/dist/git-snapshot.d.ts.map +1 -0
  57. package/dist/git-snapshot.js +250 -0
  58. package/dist/git-snapshot.js.map +1 -0
  59. package/dist/git-tracker.d.ts +166 -3
  60. package/dist/git-tracker.d.ts.map +1 -1
  61. package/dist/git-tracker.js +250 -22
  62. package/dist/git-tracker.js.map +1 -1
  63. package/dist/git-utils.d.ts +84 -1
  64. package/dist/git-utils.d.ts.map +1 -1
  65. package/dist/git-utils.js +208 -82
  66. package/dist/git-utils.js.map +1 -1
  67. package/dist/git.d.ts +13 -4
  68. package/dist/git.d.ts.map +1 -1
  69. package/dist/git.js +12 -4
  70. package/dist/git.js.map +1 -1
  71. package/dist/gitignore-checker.d.ts.map +1 -1
  72. package/dist/gitignore-checker.js +15 -6
  73. package/dist/gitignore-checker.js.map +1 -1
  74. package/dist/glob/glob-pattern.js +5 -5
  75. package/dist/glob/glob-pattern.js.map +1 -1
  76. package/dist/index.d.ts +32 -20
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +95 -50
  79. package/dist/index.js.map +1 -1
  80. package/dist/listing-refusal.d.ts +112 -0
  81. package/dist/listing-refusal.d.ts.map +1 -0
  82. package/dist/listing-refusal.js +105 -0
  83. package/dist/listing-refusal.js.map +1 -0
  84. package/dist/path-containment.d.ts +64 -0
  85. package/dist/path-containment.d.ts.map +1 -0
  86. package/dist/path-containment.js +111 -0
  87. package/dist/path-containment.js.map +1 -0
  88. package/dist/path-core.d.ts +113 -18
  89. package/dist/path-core.d.ts.map +1 -1
  90. package/dist/path-core.js +139 -23
  91. package/dist/path-core.js.map +1 -1
  92. package/dist/path-utils.d.ts.map +1 -1
  93. package/dist/path-utils.js +42 -47
  94. package/dist/path-utils.js.map +1 -1
  95. package/dist/path.d.ts +1 -1
  96. package/dist/path.d.ts.map +1 -1
  97. package/dist/path.js +1 -1
  98. package/dist/path.js.map +1 -1
  99. package/dist/process.d.ts +7 -0
  100. package/dist/process.d.ts.map +1 -1
  101. package/dist/process.js +7 -0
  102. package/dist/process.js.map +1 -1
  103. package/dist/project-utils.d.ts.map +1 -1
  104. package/dist/project-utils.js +15 -9
  105. package/dist/project-utils.js.map +1 -1
  106. package/dist/project.d.ts +7 -5
  107. package/dist/project.d.ts.map +1 -1
  108. package/dist/project.js +7 -5
  109. package/dist/project.js.map +1 -1
  110. package/dist/safe-exec.d.ts +40 -11
  111. package/dist/safe-exec.d.ts.map +1 -1
  112. package/dist/safe-exec.js +111 -25
  113. package/dist/safe-exec.js.map +1 -1
  114. package/dist/skill-targets.d.ts +2 -2
  115. package/dist/skill-targets.js +2 -2
  116. package/dist/skill-test/auth-resolver.d.ts +10 -2
  117. package/dist/skill-test/auth-resolver.d.ts.map +1 -1
  118. package/dist/skill-test/auth-resolver.js +26 -11
  119. package/dist/skill-test/auth-resolver.js.map +1 -1
  120. package/dist/skill-test/index.d.ts +1 -1
  121. package/dist/skill-test/index.d.ts.map +1 -1
  122. package/dist/skill-test/index.js +1 -1
  123. package/dist/skill-test/index.js.map +1 -1
  124. package/dist/skill-test/spawn-claude.d.ts +30 -1
  125. package/dist/skill-test/spawn-claude.d.ts.map +1 -1
  126. package/dist/skill-test/spawn-claude.js +71 -5
  127. package/dist/skill-test/spawn-claude.js.map +1 -1
  128. package/dist/skill-test/transcript.d.ts +41 -2
  129. package/dist/skill-test/transcript.d.ts.map +1 -1
  130. package/dist/skill-test/transcript.js +55 -10
  131. package/dist/skill-test/transcript.js.map +1 -1
  132. package/dist/spawn-hardened.js +0 -1
  133. package/dist/spawn-hardened.js.map +1 -1
  134. package/dist/stdio-blocking.js +13 -14
  135. package/dist/stdio-blocking.js.map +1 -1
  136. package/dist/test-helpers.d.ts +135 -100
  137. package/dist/test-helpers.d.ts.map +1 -1
  138. package/dist/test-helpers.js +224 -146
  139. package/dist/test-helpers.js.map +1 -1
  140. package/dist/testing/executables.d.ts +32 -0
  141. package/dist/testing/executables.d.ts.map +1 -0
  142. package/dist/testing/executables.js +70 -0
  143. package/dist/testing/executables.js.map +1 -0
  144. package/dist/testing/hostile-tree.d.ts +79 -0
  145. package/dist/testing/hostile-tree.d.ts.map +1 -0
  146. package/dist/testing/hostile-tree.js +164 -0
  147. package/dist/testing/hostile-tree.js.map +1 -0
  148. package/dist/testing/platform-gates.d.ts +31 -0
  149. package/dist/testing/platform-gates.d.ts.map +1 -0
  150. package/dist/testing/platform-gates.js +31 -0
  151. package/dist/testing/platform-gates.js.map +1 -0
  152. package/dist/testing/temp-dir.d.ts +255 -0
  153. package/dist/testing/temp-dir.d.ts.map +1 -0
  154. package/dist/testing/temp-dir.js +363 -0
  155. package/dist/testing/temp-dir.js.map +1 -0
  156. package/dist/testing.d.ts +94 -3
  157. package/dist/testing.d.ts.map +1 -1
  158. package/dist/testing.js +119 -3
  159. package/dist/testing.js.map +1 -1
  160. package/dist/text-content.d.ts +197 -0
  161. package/dist/text-content.d.ts.map +1 -0
  162. package/dist/text-content.js +354 -0
  163. package/dist/text-content.js.map +1 -0
  164. package/dist/text-file.d.ts +45 -0
  165. package/dist/text-file.d.ts.map +1 -0
  166. package/dist/text-file.js +51 -0
  167. package/dist/text-file.js.map +1 -0
  168. package/dist/text.d.ts +16 -0
  169. package/dist/text.d.ts.map +1 -0
  170. package/dist/text.js +16 -0
  171. package/dist/text.js.map +1 -0
  172. package/dist/timing-dump.d.ts +136 -0
  173. package/dist/timing-dump.d.ts.map +1 -0
  174. package/dist/timing-dump.js +229 -0
  175. package/dist/timing-dump.js.map +1 -0
  176. package/dist/windows-shell.d.ts +0 -1
  177. package/dist/windows-shell.d.ts.map +1 -1
  178. package/dist/windows-shell.js +0 -1
  179. package/dist/windows-shell.js.map +1 -1
  180. package/dist/zod-introspection.js +2 -2
  181. package/dist/zod-introspection.js.map +1 -1
  182. package/eslint/README.md +223 -49
  183. package/eslint/index.cjs +111 -97
  184. package/eslint/index.d.cts +16 -6
  185. package/eslint/rules/callee-name.cjs +23 -0
  186. package/eslint/rules/commands-import-boundary.cjs +148 -0
  187. package/eslint/rules/dead-import.cjs +1 -1
  188. package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
  189. package/eslint/rules/eslint-rule-factory.cjs +5 -2
  190. package/eslint/rules/exempt-path-matcher.cjs +2 -1
  191. package/eslint/rules/explicit-zod-strictness.cjs +120 -0
  192. package/eslint/rules/no-bare-dynamic-import-path.cjs +5 -1
  193. package/eslint/rules/no-bare-executable-spawn.cjs +70 -0
  194. package/eslint/rules/no-bare-symlink-in-tests.cjs +213 -0
  195. package/eslint/rules/no-blind-catch.cjs +171 -0
  196. package/eslint/rules/no-child-process-execSync.cjs +8 -0
  197. package/eslint/rules/no-decaying-referent.cjs +217 -0
  198. package/eslint/rules/no-dotdot-containment.cjs +160 -0
  199. package/eslint/rules/no-file-url-string-concat.cjs +4 -1
  200. package/eslint/rules/no-fragile-entrypoint-guard.cjs +228 -0
  201. package/eslint/rules/no-fs-mkdirSync.cjs +8 -0
  202. package/eslint/rules/no-fs-promises-cp.cjs +7 -0
  203. package/eslint/rules/no-fs-realpathSync.cjs +8 -0
  204. package/eslint/rules/no-hardcoded-path-split.cjs +29 -18
  205. package/eslint/rules/no-io-in-unit-tier.cjs +129 -0
  206. package/eslint/rules/no-literal-process-exit.cjs +129 -0
  207. package/eslint/rules/no-manual-path-normalize.cjs +136 -107
  208. package/eslint/rules/no-os-tmpdir.cjs +8 -0
  209. package/eslint/rules/no-path-operations-in-comparisons.cjs +7 -2
  210. package/eslint/rules/no-path-sep-in-strings.cjs +7 -2
  211. package/eslint/rules/no-path-startswith.cjs +7 -2
  212. package/eslint/rules/no-process-exit-in-phase.cjs +127 -0
  213. package/eslint/rules/no-raw-node-path.cjs +563 -0
  214. package/eslint/rules/no-raw-text-decode.cjs +305 -0
  215. package/eslint/rules/no-registry-count-pin.cjs +255 -0
  216. package/eslint/rules/no-self-package-import.cjs +168 -0
  217. package/eslint/rules/no-test-scoped-functions.cjs +10 -3
  218. package/eslint/rules/no-unix-shell-commands.cjs +6 -2
  219. package/eslint/rules/no-unsafe-root-join.cjs +23 -2
  220. package/eslint/rules/no-url-pathname-for-fs.cjs +5 -1
  221. package/eslint/rules/no-version-literal.cjs +151 -0
  222. package/eslint/rules/prefer-startswith-over-regex.cjs +9 -0
  223. package/eslint/rules/require-justified-skip.cjs +8 -2
  224. package/eslint/rules/safe-import.cjs +1 -1
  225. package/package.json +16 -16
  226. package/dist/link-auth/build-headers.d.ts +0 -34
  227. package/dist/link-auth/build-headers.d.ts.map +0 -1
  228. package/dist/link-auth/build-headers.js +0 -58
  229. package/dist/link-auth/build-headers.js.map +0 -1
  230. package/dist/link-auth/expand-macro.d.ts +0 -38
  231. package/dist/link-auth/expand-macro.d.ts.map +0 -1
  232. package/dist/link-auth/expand-macro.js +0 -133
  233. package/dist/link-auth/expand-macro.js.map +0 -1
  234. package/dist/link-auth/macros.yaml +0 -50
  235. package/dist/link-auth/resolve-token.d.ts +0 -83
  236. package/dist/link-auth/resolve-token.d.ts.map +0 -1
  237. package/dist/link-auth/resolve-token.js +0 -101
  238. package/dist/link-auth/resolve-token.js.map +0 -1
  239. package/dist/link-auth/resolve.d.ts +0 -102
  240. package/dist/link-auth/resolve.d.ts.map +0 -1
  241. package/dist/link-auth/resolve.js +0 -66
  242. package/dist/link-auth/resolve.js.map +0 -1
  243. package/dist/link-auth/rewrite.d.ts +0 -52
  244. package/dist/link-auth/rewrite.d.ts.map +0 -1
  245. package/dist/link-auth/rewrite.js +0 -102
  246. package/dist/link-auth/rewrite.js.map +0 -1
  247. package/dist/link-auth/select-provider.d.ts +0 -30
  248. package/dist/link-auth/select-provider.d.ts.map +0 -1
  249. package/dist/link-auth/select-provider.js +0 -55
  250. package/dist/link-auth/select-provider.js.map +0 -1
  251. package/dist/link-auth/template.d.ts +0 -40
  252. package/dist/link-auth/template.d.ts.map +0 -1
  253. package/dist/link-auth/template.js +0 -89
  254. package/dist/link-auth/template.js.map +0 -1
  255. package/dist/link-auth/transforms.d.ts +0 -46
  256. package/dist/link-auth/transforms.d.ts.map +0 -1
  257. package/dist/link-auth/transforms.js +0 -52
  258. package/dist/link-auth/transforms.js.map +0 -1
  259. package/dist/template-entry.d.ts +0 -10
  260. package/dist/template-entry.d.ts.map +0 -1
  261. package/dist/template-entry.js +0 -10
  262. package/dist/template-entry.js.map +0 -1
  263. package/dist/template.d.ts +0 -7
  264. package/dist/template.d.ts.map +0 -1
  265. package/dist/template.js +0 -18
  266. package/dist/template.js.map +0 -1
  267. package/eslint/rules/no-path-join.cjs +0 -14
  268. package/eslint/rules/no-path-relative.cjs +0 -14
  269. package/eslint/rules/no-path-resolve.cjs +0 -14
  270. package/eslint/rules/path-function-rule-factory.cjs +0 -496
@@ -6,6 +6,8 @@
6
6
  */
7
7
  import { existsSync } from 'node:fs';
8
8
  import { dirname } from 'node:path';
9
+ import { CRAWL_SHARED_GIT_TRACKER_ID, crawlTimingStart, recordSharedPass, } from './crawl-timing.js';
10
+ import { peekGitTreeSnapshot } from './git-snapshot.js';
9
11
  import { gitLsFiles, isGitIgnored } from './git-utils.js';
10
12
  import { safePath, toForwardSlash } from './path-utils.js';
11
13
  /**
@@ -42,6 +44,33 @@ export class GitTracker {
42
44
  activeSet = new Set();
43
45
  /** Absolute paths of every directory that contains at least one active-set file. */
44
46
  activeAncestors = new Set();
47
+ /**
48
+ * Lowercased absolute path → the root-relative path spelled the way git
49
+ * spelled it. Built from the very same `git ls-files` output `activeSet`
50
+ * comes from, so it costs one extra `Map` and no extra git invocation.
51
+ */
52
+ indexPaths = new Map();
53
+ /**
54
+ * Absolute, forward-slashed directories the active-set listing could not
55
+ * OPEN, so nothing beneath them is in the set — read off `git ls-files
56
+ * --others`' stderr through the listing's `unreadable` policy (`degrade`).
57
+ *
58
+ * 🚨 Beneath one of these the active set has no opinion. Its rule is "absent
59
+ * from the set and present on disk ⇒ ignored", and that rule is sound only
60
+ * where the listing LOOKED: an untracked, non-ignored file under a `--x`
61
+ * directory is absent because git could not read the directory, not because
62
+ * a pattern excludes it, and `git check-ignore` — which answers from the
63
+ * patterns and needs no listing — says so. Before this was recorded the
64
+ * tracker called such a file gitignored, silently, and every consumer of
65
+ * that answer (the leak judge, the link-graph walker, audit's distributed
66
+ * lane) was one refused `opendir` from a wrong verdict.
67
+ *
68
+ * The directory ITSELF is non-ignored by construction — git opened it
69
+ * precisely because no pattern excluded it — so it and its ancestors are
70
+ * entered into {@link activeAncestors}: a walker must not prune what the
71
+ * listing could not see into.
72
+ */
73
+ unlistableDirectories = [];
45
74
  initialized = false;
46
75
  activeSetPopulated = false;
47
76
  /** Whether `git ls-files` actually answered during {@link initialize}. */
@@ -61,27 +90,121 @@ export class GitTracker {
61
90
  * With `includeUntracked: false`, only tracked files are pre-populated.
62
91
  * Untracked non-ignored files will miss the cache and fall through to
63
92
  * `git check-ignore` via {@link isIgnored}.
93
+ *
94
+ * ## This is the one bracket in the crawl-timing seam's `shared` stratum
95
+ *
96
+ * The `git ls-files` spawn below is preparation BOTH crawlers consume and
97
+ * NEITHER owns — the incumbent link walk and the projection's contributors are
98
+ * each handed a tracker by their caller — so it is charged to
99
+ * {@link CRAWL_SHARED_GIT_TRACKER_ID}, in a stratum belonging to no arm.
100
+ *
101
+ * The bracket is here, inside the class, and not at the six sites that build a
102
+ * tracker, for the reason `crawl-timing.ts` gives about `ResourceRegistry`: six
103
+ * copies are six chances to disagree, and a seventh site added later would
104
+ * silently go uncharged. Here, every caller is covered by construction — which
105
+ * includes `@vibe-agent-toolkit/discovery`, a package that could not have filed
106
+ * a row from its own call site at all, since it depends on `utils` alone.
107
+ *
108
+ * The early return above is deliberately OUTSIDE it: a re-entrant call does no
109
+ * work, and charging it would inflate `calls` with questions rather than
110
+ * spawns.
64
111
  */
65
112
  async initialize(options) {
66
113
  if (this.initialized) {
67
114
  return;
68
115
  }
116
+ const startedAt = crawlTimingStart();
69
117
  const includeUntracked = options?.includeUntracked ?? true;
70
- const files = gitLsFiles({
118
+ const files = this.activePathsFromOpenSnapshot(includeUntracked) ?? gitLsFiles({
71
119
  cwd: this.projectRoot,
72
120
  ...(includeUntracked ? { includeUntracked: true } : {}),
121
+ // Degrade, and RECORD: a directory git could not open is a shorter list
122
+ // nothing can tell from a complete one — see {@link unlistableDirectories}
123
+ // for how the tracker keeps that gap honest. Only the untracked listing
124
+ // walks the tree, so the tracked-only one never calls this.
125
+ unreadable: {
126
+ degrade: (refusal) => {
127
+ this.unlistableDirectories.push(refusal.directory);
128
+ },
129
+ },
73
130
  });
74
131
  if (files !== null) {
75
132
  for (const relativePath of files) {
76
133
  const absolutePath = safePath.resolve(this.projectRoot, relativePath);
77
134
  this.cache.set(absolutePath, false); // false = not ignored
78
135
  this.activeSet.add(absolutePath);
136
+ this.indexPaths.set(absolutePath.toLowerCase(), toForwardSlash(relativePath));
79
137
  }
80
138
  this.populateAncestorSet();
81
139
  }
82
140
  this.gitAnswered = files !== null;
83
141
  this.activeSetPopulated = includeUntracked && files !== null;
84
142
  this.initialized = true;
143
+ // After the state above is settled, so a throw from the seam could never
144
+ // leave a half-initialized tracker; and charged even when git did not answer,
145
+ // because a failed `git ls-files` still spawned a process and still cost the
146
+ // command the time it took to fail.
147
+ recordSharedPass(CRAWL_SHARED_GIT_TRACKER_ID, startedAt);
148
+ }
149
+ /**
150
+ * The active set read off a snapshot this process has ALREADY taken — or
151
+ * `null` to say "ask git yourself".
152
+ *
153
+ * ## Why this is the same question, not a similar one
154
+ *
155
+ * `git ls-files --cached --others --exclude-standard` returns
156
+ * `tracked ∪ (untracked ∧ ¬ignored)`. A snapshot is `git add --all` **without**
157
+ * `--force` staged into a throwaway index, whose membership is that same set
158
+ * by construction — the exclusion of ignored paths is `--force`'s absence in
159
+ * both cases. So where a snapshot is already in hand, the spawn this method
160
+ * replaces would re-derive a set the process is already holding.
161
+ *
162
+ * It is deliberately a PEEK ({@link peekGitTreeSnapshot}) and never a take: a
163
+ * snapshot costs a `git add --all`, so causing one to avoid an `ls-files`
164
+ * would be a large loss dressed as a saving. On the incumbent walk no snapshot
165
+ * is ever taken, the peek misses, and this tracker spawns exactly as it always
166
+ * has.
167
+ *
168
+ * ## The one divergence, and why it is an improvement
169
+ *
170
+ * `--cached` reads the REAL index, so a tracked file deleted from the working
171
+ * tree is still listed. `git add --all` stages that deletion, so the snapshot
172
+ * omits it. The set is therefore not identical — it is the same set minus
173
+ * paths that do not exist, which is what {@link isIgnoredByActiveSet} already
174
+ * documents the active set to be ("it can only ever contain paths that
175
+ * EXIST"). The snapshot-sourced set honours that sentence more exactly than
176
+ * the spawn does.
177
+ *
178
+ * @param includeUntracked - The caller's requested membership
179
+ * @returns Root-relative paths in `git ls-files` shape, or `null` when no
180
+ * snapshot is available or the request is one a snapshot cannot answer
181
+ */
182
+ activePathsFromOpenSnapshot(includeUntracked) {
183
+ // A snapshot cannot express the tracked-ONLY set: `add --all` stages
184
+ // untracked-not-ignored files too, and nothing in the result marks which
185
+ // entries were already tracked. Declining is the only correct answer.
186
+ if (!includeUntracked)
187
+ return null;
188
+ const snapshot = peekGitTreeSnapshot(this.projectRoot);
189
+ if (snapshot === undefined)
190
+ return null;
191
+ const relativePaths = [];
192
+ for (const entry of snapshot.entries) {
193
+ // A snapshot covers the whole REPOSITORY, which may be an ancestor of this
194
+ // tracker's root — `git ls-files` run at that root scopes its listing to
195
+ // it, so anything above is not this tracker's business and must not enter
196
+ // the active set. Everything below is kept verbatim, symlinks and
197
+ // submodule gitlinks included, because `--cached` lists those too.
198
+ const relativePath = toForwardSlash(safePath.relative(this.normalizedProjectRoot, entry.absolutePath));
199
+ // A leading `..` SEGMENT, not a `../` prefix: compared as a path segment,
200
+ // a file legitimately named `..hidden` is kept rather than silently
201
+ // dropped as if it were an escape.
202
+ if (relativePath.length === 0 || relativePath.split('/')[0] === '..') {
203
+ continue;
204
+ }
205
+ relativePaths.push(relativePath);
206
+ }
207
+ return relativePaths;
85
208
  }
86
209
  /**
87
210
  * Did git actually answer, or is this tracker an empty shell?
@@ -109,24 +232,49 @@ export class GitTracker {
109
232
  * `hasActiveDescendant` / `isIgnoredByActiveSet` ancestor lookup misses on Windows.
110
233
  */
111
234
  populateAncestorSet() {
112
- const root = this.normalizedProjectRoot;
113
235
  for (const absolutePath of this.activeSet) {
114
- let current = toForwardSlash(dirname(absolutePath));
115
- while (current !== root && current.length > root.length) {
116
- if (this.activeAncestors.has(current)) {
117
- // Ancestor (and all of its ancestors) already recorded — avoid redundant work.
118
- break;
119
- }
120
- this.activeAncestors.add(current);
121
- const parent = toForwardSlash(dirname(current));
122
- if (parent === current) {
123
- break;
124
- }
125
- current = parent;
126
- }
236
+ this.recordAncestorsFrom(toForwardSlash(dirname(absolutePath)));
237
+ }
238
+ // A directory the listing could not open is itself non-ignored (git opened
239
+ // it because no pattern excluded it) and may hold active files nobody could
240
+ // list — so it counts as an ancestor from ITSELF down, not from its parent.
241
+ for (const directory of this.unlistableDirectories) {
242
+ this.recordAncestorsFrom(directory);
127
243
  }
128
244
  // projectRoot itself is always an implicit ancestor of everything under it.
129
- this.activeAncestors.add(root);
245
+ this.activeAncestors.add(this.normalizedProjectRoot);
246
+ }
247
+ /**
248
+ * Record `start` and every directory above it up to (not including) the
249
+ * project root as active ancestors, stopping at the first one already known
250
+ * — everything above it necessarily is too.
251
+ *
252
+ * @param start - Forward-slashed absolute directory to begin at
253
+ */
254
+ recordAncestorsFrom(start) {
255
+ const root = this.normalizedProjectRoot;
256
+ let current = start;
257
+ while (current !== root && current.length > root.length) {
258
+ if (this.activeAncestors.has(current)) {
259
+ break;
260
+ }
261
+ this.activeAncestors.add(current);
262
+ const parent = toForwardSlash(dirname(current));
263
+ if (parent === current) {
264
+ break;
265
+ }
266
+ current = parent;
267
+ }
268
+ }
269
+ /**
270
+ * Whether the active set can say anything about a path: it cannot beneath a
271
+ * directory the listing was refused — see {@link unlistableDirectories}.
272
+ *
273
+ * @param normalizedAbsolutePath - Forward-slashed absolute path
274
+ * @returns True when the path lies strictly beneath a refused directory
275
+ */
276
+ beneathUnlistable(normalizedAbsolutePath) {
277
+ return this.unlistableDirectories.some((directory) => normalizedAbsolutePath.startsWith(`${directory}/`));
130
278
  }
131
279
  /**
132
280
  * Returns true if the given absolute path IS an active-set file OR is an
@@ -145,7 +293,9 @@ export class GitTracker {
145
293
  return true;
146
294
  }
147
295
  const normalized = safePath.resolve(absolutePath);
148
- return this.activeSet.has(normalized) || this.activeAncestors.has(normalized);
296
+ // Beneath a refused directory nothing was listed, so "no active descendant"
297
+ // would be a claim the listing never earned.
298
+ return this.activeSet.has(normalized) || this.activeAncestors.has(normalized) || this.beneathUnlistable(normalized);
149
299
  }
150
300
  /**
151
301
  * Fast O(1) ignore check against the pre-populated active set.
@@ -153,7 +303,9 @@ export class GitTracker {
153
303
  * For paths INSIDE the project root **that exist on disk**, membership in the
154
304
  * active set is authoritative: such a path is ignored iff it is not in the
155
305
  * active set AND not an ancestor of any active-set path. No `git
156
- * check-ignore` spawn.
306
+ * check-ignore` spawn. The one exception is a path beneath a directory the
307
+ * listing could not OPEN — see {@link unlistableDirectories} — where absence
308
+ * says nothing and the question goes to `git check-ignore`.
157
309
  *
158
310
  * The existence qualifier is load-bearing, not a caveat. The active set is
159
311
  * built from `git ls-files`, so it can only ever contain paths that EXIST — a
@@ -167,7 +319,38 @@ export class GitTracker {
167
319
  * so a repeated miss on the same path stays O(1).
168
320
  *
169
321
  * For paths OUTSIDE the project root, falls back to {@link isIgnored} so
170
- * legacy behavior is preserved.
322
+ * legacy behavior is preserved. That fallback is the expensive one, and its
323
+ * cost does not resemble the in-root cost at all.
324
+ *
325
+ * ## ⚠️ The out-of-root fallback costs an order of magnitude more per path
326
+ *
327
+ * Measured in the D9 parity fixture: an out-of-root path costs
328
+ * **185–427 ms**, against **12–28 ms** for every in-repo path — ≈7–36× on
329
+ * those ranges.
330
+ *
331
+ * The mechanism is the delegation above plus what git does with it.
332
+ * {@link isIgnored} runs `git check-ignore` with the PROJECT ROOT as cwd; git
333
+ * exits 128 for a path outside that repository; and {@link isGitIgnored}'s
334
+ * exit-128 recovery walk then spawns `check-ignore` ONCE PER ANCESTOR up to
335
+ * `/`. No ancestor of an out-of-root path is ever the project root that would
336
+ * stop the walk, and every one of those ancestors is outside the repository
337
+ * too — so each also exits 128 and the walk never breaks early either. The
338
+ * verdict is cached, so the price is paid once per DISTINCT out-of-root path
339
+ * rather than once per call.
340
+ *
341
+ * The consequence for a caller choosing between the two oracles: switching
342
+ * from {@link isIgnored} to this method removes the spawn population **only
343
+ * for in-root paths**. A lane feeding out-of-root paths in bulk keeps the
344
+ * per-path spawn in full. So a comment asserting that the out-of-project case
345
+ * is rare, safe or cheap is asserting something this measurement contradicts:
346
+ * it is rare in some lanes, and it is never cheap.
347
+ *
348
+ * The exit-128 walk itself is documented in {@link isGitIgnored} — but only
349
+ * for the two cases already answered there: the SYMLINK case it exists to
350
+ * serve, and the NO-REPOSITORY case, which an early return now short-circuits
351
+ * before any spawn. The IN-REPO OUT-OF-ROOT path is the third case, it is
352
+ * documented nowhere there, and it is the one that still pays the walk in
353
+ * full. That is why the number lives here.
171
354
  *
172
355
  * Requires {@link initialize} with `includeUntracked: true` (the default).
173
356
  * When initialized without untracked files, this method delegates to
@@ -175,8 +358,21 @@ export class GitTracker {
175
358
  * possible per-path spawn.
176
359
  *
177
360
  * @param absolutePath - Absolute path to check
361
+ * @param knownToExist - The caller's own answer to the existence question, when
362
+ * it has already asked. Supplying it skips this method's `existsSync`, which
363
+ * is otherwise paid once per path that is absent from the active set — i.e.
364
+ * once per ignored path, and the projection's `filesystem` extent enumerates
365
+ * all of them (11,108 calls on an 8,496-path adopter tree).
366
+ *
367
+ * **It must mean what `existsSync` means: `stat` succeeds, following
368
+ * symlinks.** A caller holding only an `lstat` result has a DIFFERENT fact —
369
+ * `lstat` succeeds on a dangling symlink where `existsSync` returns false —
370
+ * and must narrow it to `exists && symlinkResolves !== false` rather than
371
+ * pass the `lstat` boolean through, or dangling symlinks silently stop
372
+ * falling back to `git check-ignore` and start reporting as ignored.
373
+ * Omit it and nothing changes.
178
374
  */
179
- isIgnoredByActiveSet(absolutePath) {
375
+ isIgnoredByActiveSet(absolutePath, knownToExist) {
180
376
  if (!this.activeSetPopulated) {
181
377
  return this.isIgnored(absolutePath);
182
378
  }
@@ -188,10 +384,16 @@ export class GitTracker {
188
384
  if (this.activeSet.has(normalized) || this.activeAncestors.has(normalized)) {
189
385
  return false;
190
386
  }
387
+ // Absent from the set beneath a directory the listing could not open: the
388
+ // set never looked there, so absence is not evidence — see
389
+ // {@link unlistableDirectories}. Only `git check-ignore` can answer.
390
+ if (this.beneathUnlistable(normalized)) {
391
+ return this.isIgnored(absolutePath);
392
+ }
191
393
  // Absent from the active set. That means "ignored" only for a path that is
192
394
  // actually there; otherwise the set has no opinion and git must be asked.
193
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- caller-supplied path, read-only existence probe
194
- if (!existsSync(normalized)) {
395
+ const present = knownToExist ?? existsSync(normalized);
396
+ if (!present) {
195
397
  return this.isIgnored(absolutePath);
196
398
  }
197
399
  return true;
@@ -228,6 +430,30 @@ export class GitTracker {
228
430
  this.cache.set(cacheKey, ignored);
229
431
  return ignored;
230
432
  }
433
+ /**
434
+ * The spelling git records for a path, or `null` if git has no record of it.
435
+ *
436
+ * This is the casing oracle, not another ignore check. On a case-insensitive
437
+ * filesystem `docs/Readme.md` and `docs/README.md` are one inode with two
438
+ * spellings, and Node's two `realpath` implementations disagree about which
439
+ * one they hand back — so anything that derives an identity from a path needs
440
+ * a single authoritative spelling, and git's is it wherever git has one.
441
+ *
442
+ * The lookup key is lowercased, which is the point: the caller asks with
443
+ * whatever casing it observed and gets back the casing git holds.
444
+ *
445
+ * Answers only from the pre-populated set — never spawns. A path git does not
446
+ * know (untracked-and-ignored, non-existent, outside the project root, or any
447
+ * path at all when `git ls-files` did not answer) returns `null`, and the
448
+ * caller falls back to the on-disk casing.
449
+ *
450
+ * @param absolutePath - Absolute path to look up
451
+ * @returns Root-relative, forward-slashed path as git spells it — relative to
452
+ * THIS tracker's project root — or `null` when git has no record of it
453
+ */
454
+ indexPathFor(absolutePath) {
455
+ return this.indexPaths.get(safePath.resolve(absolutePath).toLowerCase()) ?? null;
456
+ }
231
457
  /**
232
458
  * Get cache statistics.
233
459
  */
@@ -245,6 +471,8 @@ export class GitTracker {
245
471
  this.cache.clear();
246
472
  this.activeSet.clear();
247
473
  this.activeAncestors.clear();
474
+ this.indexPaths.clear();
475
+ this.unlistableDirectories.length = 0;
248
476
  this.initialized = false;
249
477
  this.activeSetPopulated = false;
250
478
  this.gitAnswered = false;
@@ -1 +1 @@
1
- {"version":3,"file":"git-tracker.js","sourceRoot":"","sources":["../src/git-tracker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"git-tracker.js","sourceRoot":"","sources":["../src/git-tracker.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,42 @@
2
2
  * Centralized git command wrapper.
3
3
  * All git commands should go through this module for consistency and testability.
4
4
  */
5
+ import { type UnreadablePolicy } from './listing-refusal.js';
6
+ /**
7
+ * The directories a git listing was REFUSED, read off its stderr.
8
+ *
9
+ * Pure. Returns each refused directory once, worktree-relative and without
10
+ * git's trailing slash, with the errno its reason maps to. A directory git
11
+ * could not open because it no longer exists is not returned: that is the
12
+ * `absent` outcome of {@link listingFailure}, decided there and not here, so
13
+ * the two routes classify a vanished directory the same way.
14
+ *
15
+ * @param stderr - What the listing wrote to stderr
16
+ * @returns The refusals, in the order git printed them, deduplicated
17
+ */
18
+ export declare function unlistableDirectoriesIn(stderr: string): {
19
+ directory: string;
20
+ code: string;
21
+ }[];
22
+ /**
23
+ * The policy every working-tree listing here REQUIRES.
24
+ *
25
+ * A refused directory is otherwise invisible: git's exit status is 0 and its
26
+ * stdout is simply shorter. Each directory git could not open is settled as
27
+ * the SAME {@link DirectoryRefusal} the filesystem walk produces
28
+ * (`file-crawler.ts`) — absolute, forward-slashed, with `transient` derived
29
+ * beside the errno list in `fs-utils.ts` — under the same
30
+ * {@link UnreadablePolicy}, so a caller decides once for both routes.
31
+ *
32
+ * 🪤 This was `onUnreadable?`, and when it was omitted the refusals git printed
33
+ * were DROPPED — not thrown, as the walk did, but read off stderr and ignored.
34
+ * The one lane whose only witness to the gap is stderr was the one lane that
35
+ * defaulted to silence. There is no default now.
36
+ */
37
+ export interface GitListingOptions {
38
+ /** What to do with a directory git could not open while walking the working tree. */
39
+ unreadable: UnreadablePolicy;
40
+ }
5
41
  /**
6
42
  * Find the git repository root by walking up from the given directory.
7
43
  *
@@ -27,6 +63,8 @@ export declare function gitFindRoot(startDir: string): string | null;
27
63
  * @param options.cwd - Working directory (git repository root or subdirectory)
28
64
  * @param options.patterns - Optional glob patterns to filter files (e.g., '*.md', 'docs/**\/*.ts')
29
65
  * @param options.includeUntracked - Include untracked files that aren't gitignored (default: false)
66
+ * @param options.unreadable - See {@link GitListingOptions}. Only the untracked
67
+ * listing walks the working tree, so only it can have skipped a directory
30
68
  * @returns Array of file paths relative to the git root, or null if not in a git repo
31
69
  *
32
70
  * @example
@@ -38,11 +76,56 @@ export declare function gitFindRoot(startDir: string): string | null;
38
76
  * const allFiles = gitLsFiles({ cwd: '/project', includeUntracked: true });
39
77
  * ```
40
78
  */
41
- export declare function gitLsFiles(options: {
79
+ export declare function gitLsFiles(options: GitListingOptions & {
42
80
  cwd: string;
43
81
  patterns?: string[];
44
82
  includeUntracked?: boolean;
45
83
  }): string[] | null;
84
+ /**
85
+ * List paths git does NOT track, with each wholly-untracked directory collapsed
86
+ * to a single entry.
87
+ *
88
+ * This is the **prune list**, not a file list, and the distinction is the whole
89
+ * value. `--others --ignored --exclude-standard` alone returns every ignored file
90
+ * individually: measured on an 8,496-path adopter working tree that is 533,557
91
+ * paths in 1.19 s — worse than the crawl it was meant to replace, because
92
+ * `.turbo/cache` alone contributed 418,518 of them. Adding `--directory`
93
+ * collapses each wholly-ignored directory to one entry: **369 entries in 60 ms**.
94
+ * A caller can then decide per directory whether to descend, and skip a
95
+ * half-million paths by name without ever entering them.
96
+ *
97
+ * A collapsed directory entry is returned with a trailing `/`, exactly as git
98
+ * spells it. That is how a caller tells "this whole subtree" from "this one
99
+ * file", so it is deliberately not normalized away here.
100
+ *
101
+ * @param options - Configuration options
102
+ * @param options.cwd - Working directory inside the repository
103
+ * @param options.ignored - Restrict to ignored paths. Off, the listing is
104
+ * untracked-but-not-ignored paths — which is the only way to see an EMPTY
105
+ * untracked directory, since a directory with no files in it is invisible to
106
+ * `ls-files` and to any tree object
107
+ * @param options.directory - Collapse a wholly-untracked directory to one entry
108
+ * @param options.unreadable - See {@link GitListingOptions}. ⚠️ The two
109
+ * listings meet different refusals: without `--ignored`, git collapses a
110
+ * WHOLLY-untracked directory without opening it, but it still opens a
111
+ * non-ignored directory that holds tracked files (to look for untracked
112
+ * siblings) and warns when it cannot; the `ignored` listing opens every
113
+ * non-ignored directory and so meets every refusal the first one does and
114
+ * more (measured on git 2.50)
115
+ * @returns Paths relative to the git root, or null if git did not answer
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * // The prune list: where the ignored territory is, without enumerating it.
120
+ * const prune = gitLsOthers({ cwd: root, ignored: true, directory: true });
121
+ * // → ['dist/', 'node_modules/', '.turbo/', 'notes.local.md']
122
+ * ```
123
+ */
124
+ export declare function gitLsOthers(options: GitListingOptions & {
125
+ cwd: string;
126
+ ignored?: boolean;
127
+ directory?: boolean;
128
+ }): string[] | null;
46
129
  /**
47
130
  * Check if a file path is ignored by git
48
131
  *
@@ -1 +1 @@
1
- {"version":3,"file":"git-utils.d.ts","sourceRoot":"","sources":["../src/git-utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAYH;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmB3D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,MAAM,EAAE,GAAG,IAAI,CA6ClB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,MAAsB,GAAG,OAAO,CAkEnF"}
1
+ {"version":3,"file":"git-utils.d.ts","sourceRoot":"","sources":["../src/git-utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,OAAO,EAA0C,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAmDrG;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAW7F;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,iBAAiB;IAChC,qFAAqF;IACrF,UAAU,EAAE,gBAAgB,CAAC;CAC9B;AAkDD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB3D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG;IACtD,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,MAAM,EAAE,GAAG,IAAI,CAqBlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,MAAM,EAAE,GAAG,IAAI,CAiBlB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,MAAsB,GAAG,OAAO,CAiDnF"}