@vibe-agent-toolkit/utils 0.2.0-rc.11 → 0.2.0-rc.12

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 (322) hide show
  1. package/README.md +111 -9
  2. package/dist/archive/archive.d.ts +129 -0
  3. package/dist/archive/archive.d.ts.map +1 -0
  4. package/dist/archive/archive.js +223 -0
  5. package/dist/archive/archive.js.map +1 -0
  6. package/dist/archive/entries.d.ts +132 -0
  7. package/dist/archive/entries.d.ts.map +1 -0
  8. package/dist/archive/entries.js +223 -0
  9. package/dist/archive/entries.js.map +1 -0
  10. package/dist/archive/limits.d.ts +39 -0
  11. package/dist/archive/limits.d.ts.map +1 -0
  12. package/dist/archive/limits.js +59 -0
  13. package/dist/archive/limits.js.map +1 -0
  14. package/dist/archive/records.d.ts +14 -0
  15. package/dist/archive/records.d.ts.map +1 -0
  16. package/dist/archive/records.js +3 -0
  17. package/dist/archive/records.js.map +1 -0
  18. package/dist/archive/tar-reader.d.ts +18 -0
  19. package/dist/archive/tar-reader.d.ts.map +1 -0
  20. package/dist/archive/tar-reader.js +173 -0
  21. package/dist/archive/tar-reader.js.map +1 -0
  22. package/dist/archive/zip-reader.d.ts +76 -0
  23. package/dist/archive/zip-reader.d.ts.map +1 -0
  24. package/dist/archive/zip-reader.js +376 -0
  25. package/dist/archive/zip-reader.js.map +1 -0
  26. package/dist/archive.d.ts +14 -0
  27. package/dist/archive.d.ts.map +1 -0
  28. package/dist/archive.js +12 -0
  29. package/dist/archive.js.map +1 -0
  30. package/dist/asset-reference.d.ts +17 -1
  31. package/dist/asset-reference.d.ts.map +1 -1
  32. package/dist/asset-reference.js +31 -7
  33. package/dist/asset-reference.js.map +1 -1
  34. package/dist/dirent-kind.d.ts +13 -40
  35. package/dist/dirent-kind.d.ts.map +1 -1
  36. package/dist/dirent-kind.js +21 -69
  37. package/dist/dirent-kind.js.map +1 -1
  38. package/dist/entrypoint.d.ts +2 -2
  39. package/dist/entrypoint.d.ts.map +1 -1
  40. package/dist/entrypoint.js +4 -3
  41. package/dist/entrypoint.js.map +1 -1
  42. package/dist/env-flag.d.ts +2 -0
  43. package/dist/env-flag.d.ts.map +1 -1
  44. package/dist/env-flag.js +2 -0
  45. package/dist/env-flag.js.map +1 -1
  46. package/dist/errors/confirmed-absent.d.ts +41 -0
  47. package/dist/errors/confirmed-absent.d.ts.map +1 -0
  48. package/dist/errors/confirmed-absent.js +43 -0
  49. package/dist/errors/confirmed-absent.js.map +1 -0
  50. package/dist/errors/errno-table.d.ts +177 -0
  51. package/dist/errors/errno-table.d.ts.map +1 -0
  52. package/dist/errors/errno-table.js +343 -0
  53. package/dist/errors/errno-table.js.map +1 -0
  54. package/dist/errors/fs-boundary.d.ts +36 -0
  55. package/dist/errors/fs-boundary.d.ts.map +1 -0
  56. package/dist/errors/fs-boundary.js +121 -0
  57. package/dist/errors/fs-boundary.js.map +1 -0
  58. package/dist/errors/fs-fault.d.ts +176 -0
  59. package/dist/errors/fs-fault.d.ts.map +1 -0
  60. package/dist/errors/fs-fault.js +215 -0
  61. package/dist/errors/fs-fault.js.map +1 -0
  62. package/dist/errors/git-errno.d.ts +13 -0
  63. package/dist/errors/git-errno.d.ts.map +1 -0
  64. package/dist/errors/git-errno.js +45 -0
  65. package/dist/errors/git-errno.js.map +1 -0
  66. package/dist/errors/path-present.d.ts +72 -0
  67. package/dist/errors/path-present.d.ts.map +1 -0
  68. package/dist/errors/path-present.js +110 -0
  69. package/dist/errors/path-present.js.map +1 -0
  70. package/dist/errors/rag-index-empty.d.ts +16 -0
  71. package/dist/errors/rag-index-empty.d.ts.map +1 -0
  72. package/dist/errors/rag-index-empty.js +16 -0
  73. package/dist/errors/rag-index-empty.js.map +1 -0
  74. package/dist/errors/suppressed-faults.d.ts +34 -0
  75. package/dist/errors/suppressed-faults.d.ts.map +1 -0
  76. package/dist/errors/suppressed-faults.js +66 -0
  77. package/dist/errors/suppressed-faults.js.map +1 -0
  78. package/dist/file-crawler.d.ts +13 -18
  79. package/dist/file-crawler.d.ts.map +1 -1
  80. package/dist/file-crawler.js +140 -24
  81. package/dist/file-crawler.js.map +1 -1
  82. package/dist/fs-utils.d.ts +75 -50
  83. package/dist/fs-utils.d.ts.map +1 -1
  84. package/dist/fs-utils.js +148 -158
  85. package/dist/fs-utils.js.map +1 -1
  86. package/dist/fs.d.ts +4 -4
  87. package/dist/fs.d.ts.map +1 -1
  88. package/dist/fs.js +4 -4
  89. package/dist/fs.js.map +1 -1
  90. package/dist/git-snapshot.d.ts +25 -17
  91. package/dist/git-snapshot.d.ts.map +1 -1
  92. package/dist/git-snapshot.js +65 -11
  93. package/dist/git-snapshot.js.map +1 -1
  94. package/dist/git-tracker.d.ts +11 -3
  95. package/dist/git-tracker.d.ts.map +1 -1
  96. package/dist/git-tracker.js +29 -6
  97. package/dist/git-tracker.js.map +1 -1
  98. package/dist/git-utils.d.ts.map +1 -1
  99. package/dist/git-utils.js +7 -35
  100. package/dist/git-utils.js.map +1 -1
  101. package/dist/git.d.ts +1 -1
  102. package/dist/git.d.ts.map +1 -1
  103. package/dist/git.js +1 -1
  104. package/dist/git.js.map +1 -1
  105. package/dist/gitignore-checker.d.ts.map +1 -1
  106. package/dist/gitignore-checker.js +5 -4
  107. package/dist/gitignore-checker.js.map +1 -1
  108. package/dist/in-order.d.ts +93 -0
  109. package/dist/in-order.d.ts.map +1 -0
  110. package/dist/in-order.js +165 -0
  111. package/dist/in-order.js.map +1 -0
  112. package/dist/index.d.ts +20 -4
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +30 -4
  115. package/dist/index.js.map +1 -1
  116. package/dist/kill-process-tree.d.ts +43 -0
  117. package/dist/kill-process-tree.d.ts.map +1 -0
  118. package/dist/kill-process-tree.js +48 -0
  119. package/dist/kill-process-tree.js.map +1 -0
  120. package/dist/link-following.d.ts +115 -0
  121. package/dist/link-following.d.ts.map +1 -0
  122. package/dist/link-following.js +283 -0
  123. package/dist/link-following.js.map +1 -0
  124. package/dist/listing-refusal.d.ts +44 -0
  125. package/dist/listing-refusal.d.ts.map +1 -1
  126. package/dist/listing-refusal.js +79 -2
  127. package/dist/listing-refusal.js.map +1 -1
  128. package/dist/name-fold.d.ts +25 -0
  129. package/dist/name-fold.d.ts.map +1 -0
  130. package/dist/name-fold.js +40 -0
  131. package/dist/name-fold.js.map +1 -0
  132. package/dist/path-containment.d.ts +3 -1
  133. package/dist/path-containment.d.ts.map +1 -1
  134. package/dist/path-containment.js +13 -7
  135. package/dist/path-containment.js.map +1 -1
  136. package/dist/path-utils.d.ts +14 -17
  137. package/dist/path-utils.d.ts.map +1 -1
  138. package/dist/path-utils.js +37 -55
  139. package/dist/path-utils.js.map +1 -1
  140. package/dist/process.d.ts +2 -0
  141. package/dist/process.d.ts.map +1 -1
  142. package/dist/process.js +2 -0
  143. package/dist/process.js.map +1 -1
  144. package/dist/project-utils.d.ts.map +1 -1
  145. package/dist/project-utils.js +8 -9
  146. package/dist/project-utils.js.map +1 -1
  147. package/dist/safe-exec.d.ts +6 -2
  148. package/dist/safe-exec.d.ts.map +1 -1
  149. package/dist/safe-exec.js +1 -1
  150. package/dist/safe-exec.js.map +1 -1
  151. package/dist/skill-targets.d.ts.map +1 -1
  152. package/dist/skill-targets.js +3 -1
  153. package/dist/skill-targets.js.map +1 -1
  154. package/dist/skill-test/spawn-claude.d.ts +0 -15
  155. package/dist/skill-test/spawn-claude.d.ts.map +1 -1
  156. package/dist/skill-test/spawn-claude.js +11 -50
  157. package/dist/skill-test/spawn-claude.js.map +1 -1
  158. package/dist/special-file.d.ts +18 -0
  159. package/dist/special-file.d.ts.map +1 -0
  160. package/dist/special-file.js +21 -0
  161. package/dist/special-file.js.map +1 -0
  162. package/dist/terminating-signals.d.ts +129 -0
  163. package/dist/terminating-signals.d.ts.map +1 -0
  164. package/dist/terminating-signals.js +210 -0
  165. package/dist/terminating-signals.js.map +1 -0
  166. package/dist/test-helpers.d.ts +9 -5
  167. package/dist/test-helpers.d.ts.map +1 -1
  168. package/dist/test-helpers.js +18 -21
  169. package/dist/test-helpers.js.map +1 -1
  170. package/dist/test-scratch-guard.d.ts +34 -0
  171. package/dist/test-scratch-guard.d.ts.map +1 -0
  172. package/dist/test-scratch-guard.js +45 -0
  173. package/dist/test-scratch-guard.js.map +1 -0
  174. package/dist/testing/archive-fixtures.d.ts +147 -0
  175. package/dist/testing/archive-fixtures.d.ts.map +1 -0
  176. package/dist/testing/archive-fixtures.js +449 -0
  177. package/dist/testing/archive-fixtures.js.map +1 -0
  178. package/dist/testing/executables.d.ts +6 -0
  179. package/dist/testing/executables.d.ts.map +1 -1
  180. package/dist/testing/executables.js +20 -9
  181. package/dist/testing/executables.js.map +1 -1
  182. package/dist/testing/fault-fs-preload.d.ts +16 -0
  183. package/dist/testing/fault-fs-preload.d.ts.map +1 -0
  184. package/dist/testing/fault-fs-preload.js +22 -0
  185. package/dist/testing/fault-fs-preload.js.map +1 -0
  186. package/dist/testing/fault-fs.d.ts +115 -0
  187. package/dist/testing/fault-fs.d.ts.map +1 -0
  188. package/dist/testing/fault-fs.js +411 -0
  189. package/dist/testing/fault-fs.js.map +1 -0
  190. package/dist/testing/fault-spec.d.ts +83 -0
  191. package/dist/testing/fault-spec.d.ts.map +1 -0
  192. package/dist/testing/fault-spec.js +132 -0
  193. package/dist/testing/fault-spec.js.map +1 -0
  194. package/dist/testing/hostile-tree.d.ts.map +1 -1
  195. package/dist/testing/hostile-tree.js +3 -4
  196. package/dist/testing/hostile-tree.js.map +1 -1
  197. package/dist/testing/platform-gates.d.ts +8 -0
  198. package/dist/testing/platform-gates.d.ts.map +1 -1
  199. package/dist/testing/platform-gates.js +24 -0
  200. package/dist/testing/platform-gates.js.map +1 -1
  201. package/dist/testing/temp-dir.d.ts +31 -0
  202. package/dist/testing/temp-dir.d.ts.map +1 -1
  203. package/dist/testing/temp-dir.js +63 -0
  204. package/dist/testing/temp-dir.js.map +1 -1
  205. package/dist/testing/tier-timeout.d.ts +34 -0
  206. package/dist/testing/tier-timeout.d.ts.map +1 -0
  207. package/dist/testing/tier-timeout.js +45 -0
  208. package/dist/testing/tier-timeout.js.map +1 -0
  209. package/dist/testing/tree-snapshot.d.ts +71 -0
  210. package/dist/testing/tree-snapshot.d.ts.map +1 -0
  211. package/dist/testing/tree-snapshot.js +163 -0
  212. package/dist/testing/tree-snapshot.js.map +1 -0
  213. package/dist/testing.d.ts +6 -0
  214. package/dist/testing.d.ts.map +1 -1
  215. package/dist/testing.js +8 -1
  216. package/dist/testing.js.map +1 -1
  217. package/dist/text-file.d.ts +14 -1
  218. package/dist/text-file.d.ts.map +1 -1
  219. package/dist/text-file.js +41 -8
  220. package/dist/text-file.js.map +1 -1
  221. package/dist/timing-dump.d.ts.map +1 -1
  222. package/dist/timing-dump.js +2 -10
  223. package/dist/timing-dump.js.map +1 -1
  224. package/dist/tree-change/apply-decisions.d.ts +101 -0
  225. package/dist/tree-change/apply-decisions.d.ts.map +1 -0
  226. package/dist/tree-change/apply-decisions.js +139 -0
  227. package/dist/tree-change/apply-decisions.js.map +1 -0
  228. package/dist/tree-change/apply.d.ts +98 -0
  229. package/dist/tree-change/apply.d.ts.map +1 -0
  230. package/dist/tree-change/apply.js +364 -0
  231. package/dist/tree-change/apply.js.map +1 -0
  232. package/dist/tree-change/copy-decisions.d.ts +58 -0
  233. package/dist/tree-change/copy-decisions.d.ts.map +1 -0
  234. package/dist/tree-change/copy-decisions.js +66 -0
  235. package/dist/tree-change/copy-decisions.js.map +1 -0
  236. package/dist/tree-change/copy-tree.d.ts +125 -0
  237. package/dist/tree-change/copy-tree.d.ts.map +1 -0
  238. package/dist/tree-change/copy-tree.js +279 -0
  239. package/dist/tree-change/copy-tree.js.map +1 -0
  240. package/dist/tree-change/files.d.ts +167 -0
  241. package/dist/tree-change/files.d.ts.map +1 -0
  242. package/dist/tree-change/files.js +423 -0
  243. package/dist/tree-change/files.js.map +1 -0
  244. package/dist/tree-change/followed-walk.d.ts +54 -0
  245. package/dist/tree-change/followed-walk.d.ts.map +1 -0
  246. package/dist/tree-change/followed-walk.js +81 -0
  247. package/dist/tree-change/followed-walk.js.map +1 -0
  248. package/dist/tree-change/host-chain.d.ts +48 -0
  249. package/dist/tree-change/host-chain.d.ts.map +1 -0
  250. package/dist/tree-change/host-chain.js +85 -0
  251. package/dist/tree-change/host-chain.js.map +1 -0
  252. package/dist/tree-change/identity-compare.d.ts +61 -0
  253. package/dist/tree-change/identity-compare.d.ts.map +1 -0
  254. package/dist/tree-change/identity-compare.js +115 -0
  255. package/dist/tree-change/identity-compare.js.map +1 -0
  256. package/dist/tree-change/identity.d.ts +62 -0
  257. package/dist/tree-change/identity.d.ts.map +1 -0
  258. package/dist/tree-change/identity.js +152 -0
  259. package/dist/tree-change/identity.js.map +1 -0
  260. package/dist/tree-change/index.d.ts +24 -0
  261. package/dist/tree-change/index.d.ts.map +1 -0
  262. package/dist/tree-change/index.js +14 -0
  263. package/dist/tree-change/index.js.map +1 -0
  264. package/dist/tree-change/link-chain.d.ts +180 -0
  265. package/dist/tree-change/link-chain.d.ts.map +1 -0
  266. package/dist/tree-change/link-chain.js +377 -0
  267. package/dist/tree-change/link-chain.js.map +1 -0
  268. package/dist/tree-change/link-containment.d.ts +105 -0
  269. package/dist/tree-change/link-containment.d.ts.map +1 -0
  270. package/dist/tree-change/link-containment.js +196 -0
  271. package/dist/tree-change/link-containment.js.map +1 -0
  272. package/dist/tree-change/link-walk.d.ts +49 -0
  273. package/dist/tree-change/link-walk.d.ts.map +1 -0
  274. package/dist/tree-change/link-walk.js +69 -0
  275. package/dist/tree-change/link-walk.js.map +1 -0
  276. package/dist/tree-change/plan.d.ts +206 -0
  277. package/dist/tree-change/plan.d.ts.map +1 -0
  278. package/dist/tree-change/plan.js +320 -0
  279. package/dist/tree-change/plan.js.map +1 -0
  280. package/dist/tree-change/readable-tree.d.ts +44 -0
  281. package/dist/tree-change/readable-tree.d.ts.map +1 -0
  282. package/dist/tree-change/readable-tree.js +52 -0
  283. package/dist/tree-change/readable-tree.js.map +1 -0
  284. package/dist/tree-change/rollback-error.d.ts +29 -0
  285. package/dist/tree-change/rollback-error.d.ts.map +1 -0
  286. package/dist/tree-change/rollback-error.js +31 -0
  287. package/dist/tree-change/rollback-error.js.map +1 -0
  288. package/dist/tree-change/staging-names.d.ts +27 -0
  289. package/dist/tree-change/staging-names.d.ts.map +1 -0
  290. package/dist/tree-change/staging-names.js +40 -0
  291. package/dist/tree-change/staging-names.js.map +1 -0
  292. package/dist/tree-change/tree-walk.d.ts +81 -0
  293. package/dist/tree-change/tree-walk.d.ts.map +1 -0
  294. package/dist/tree-change/tree-walk.js +212 -0
  295. package/dist/tree-change/tree-walk.js.map +1 -0
  296. package/dist/win32-namespace.d.ts +24 -0
  297. package/dist/win32-namespace.d.ts.map +1 -0
  298. package/dist/win32-namespace.js +31 -0
  299. package/dist/win32-namespace.js.map +1 -0
  300. package/dist/yaml/surgical-yaml.d.ts +12 -3
  301. package/dist/yaml/surgical-yaml.d.ts.map +1 -1
  302. package/dist/yaml/surgical-yaml.js +16 -6
  303. package/dist/yaml/surgical-yaml.js.map +1 -1
  304. package/eslint/README.md +12 -4
  305. package/eslint/rules/fs-binding-scan.cjs +278 -0
  306. package/eslint/rules/no-adhoc-errno.cjs +364 -0
  307. package/eslint/rules/no-bare-symlink-in-tests.cjs +2 -3
  308. package/eslint/rules/no-blind-catch.cjs +1 -1
  309. package/eslint/rules/no-destructive-fs.cjs +359 -0
  310. package/eslint/rules/no-existssync.cjs +70 -0
  311. package/eslint/rules/no-fragile-entrypoint-guard.cjs +3 -2
  312. package/eslint/rules/no-link-following-fs.cjs +68 -0
  313. package/eslint/rules/no-literal-process-exit.cjs +3 -16
  314. package/eslint/rules/no-literal-test-timeout.cjs +214 -0
  315. package/eslint/rules/no-raw-signal-listener.cjs +224 -0
  316. package/eslint/rules/no-stdout-outside-writer.cjs +135 -0
  317. package/package.json +10 -2
  318. package/dist/errors/errno.d.ts +0 -51
  319. package/dist/errors/errno.d.ts.map +0 -1
  320. package/dist/errors/errno.js +0 -112
  321. package/dist/errors/errno.js.map +0 -1
  322. package/eslint/rules/no-fs-promises-cp.cjs +0 -43
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Shared scanner: every read of a named `node:fs` function, however it is reached.
3
+ *
4
+ * `no-existssync` and `no-link-following-fs` both ban functions of the fs module,
5
+ * and both must see the same shapes — a named import (aliased too) that is called
6
+ * or handed on as a value, a namespace, default, `require()` or `await import()`
7
+ * binding read as `fs.name` (or `fs.promises.name`), the same read straight off the
8
+ * loading expression (`require('node:fs').name`), a destructure of it out of any
9
+ * of those, and a re-export. One copy, so the two rules cannot drift apart on what
10
+ * counts as a use.
11
+ *
12
+ * It follows bindings through scope, not values through data flow: an fs namespace
13
+ * copied first (`const f = fs`) or passed as an argument, `import('node:fs').then`,
14
+ * and a `require` built by `createRequire` are not seen. A project module that
15
+ * re-exports a banned function is caught at its own re-export.
16
+ *
17
+ * Also the one shape of such a rule ({@link productSourceFsBan}): its scope, its `allowFiles`
18
+ * option and its report — each rule supplies only what it bans and why.
19
+ *
20
+ * Exports no `meta`, so the plugin entry point does not register it as a rule.
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ const { createExemptPathMatcher, isTestFile, normalizeForMatch } = require('./exempt-path-matcher.cjs');
26
+
27
+ /** `packages/<pkg>/src/` anywhere in a forward-slashed path. */
28
+ const PACKAGE_SRC_DIR = /(?:^|\/)packages\/[^/]+\/src\//u;
29
+
30
+ /** Whether `filename` is product source: under a package's `src/`, and not a test file. */
31
+ function isProductSource(filename) {
32
+ if (!filename) {
33
+ return false;
34
+ }
35
+ const normalized = normalizeForMatch(filename);
36
+ return PACKAGE_SRC_DIR.test(normalized) && !isTestFile(normalized);
37
+ }
38
+
39
+ /** The string a key spells: an identifier's name or a string literal's value, else null. */
40
+ function spelledName(node, computed) {
41
+ if (node?.type === 'Literal' && typeof node.value === 'string') {
42
+ return node.value;
43
+ }
44
+ return !computed && node?.type === 'Identifier' ? node.name : null;
45
+ }
46
+
47
+ /** TypeScript wrappers that change a value's type, never the value: `x as T`, `x!`, `x satisfies T`. */
48
+ const TYPE_ONLY_WRAPPERS = new Set(['TSAsExpression', 'TSNonNullExpression', 'TSSatisfiesExpression']);
49
+
50
+ /** `node` with every type-only wrapper and one `await` peeled off. */
51
+ function unwrapped(node) {
52
+ let expr = node;
53
+ while (TYPE_ONLY_WRAPPERS.has(expr?.type)) {
54
+ expr = expr.expression;
55
+ }
56
+ return expr?.type === 'AwaitExpression' ? unwrapped(expr.argument) : expr;
57
+ }
58
+
59
+ /**
60
+ * The module a `require('x')`, `import('x')` or `await import('x')` initialiser
61
+ * loads (a `require('x') as typeof X` too), or null for any other initialiser
62
+ * (or a computed specifier).
63
+ */
64
+ function loadedModuleOf(init) {
65
+ const expr = unwrapped(init);
66
+ if (expr?.type === 'ImportExpression') {
67
+ return spelledName(expr.source, true);
68
+ }
69
+ const isRequire = expr?.type === 'CallExpression' && expr.callee.type === 'Identifier' && expr.callee.name === 'require';
70
+ return isRequire ? spelledName(expr.arguments[0], true) : null;
71
+ }
72
+
73
+ /** The namespace of the fs module's promise API, read off the module: `fs.promises`. */
74
+ const PROMISES = 'promises';
75
+
76
+ /**
77
+ * The visitors that report every use of a `banned` function of a `modules` module.
78
+ *
79
+ * @param {import('eslint').Rule.RuleContext} context - The rule's context
80
+ * @param {{ modules: ReadonlySet<string>, banned: ReadonlySet<string>, report: (node: object, name: string) => void }} spec
81
+ * - `modules`: the specifiers that load the fs module (`node:fs`, `fs`, and any promise flavour)
82
+ * - `banned`: the function names to report
83
+ * - `report`: called once per use, with the node and the banned name it reaches
84
+ */
85
+ function fsBindingVisitors(context, { modules, banned, report }) {
86
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
87
+
88
+ /** Every read of a binding that holds a banned function itself. */
89
+ function reportReads(variable, name) {
90
+ for (const ref of variable?.references ?? []) {
91
+ if (ref.isRead()) {
92
+ report(ref.identifier, name);
93
+ }
94
+ }
95
+ }
96
+
97
+ /** `const { name } = <fs>`: the bindings the pattern makes of a banned function, or of `promises`. */
98
+ function reportDestructured(declarator) {
99
+ if (declarator.id.type !== 'ObjectPattern') {
100
+ return;
101
+ }
102
+ const variables = sourceCode.getDeclaredVariables(declarator);
103
+ for (const prop of declarator.id.properties) {
104
+ const key = prop.type === 'Property' ? spelledName(prop.key, prop.computed) : null;
105
+ if (prop.type !== 'Property' || prop.value.type !== 'Identifier' || key === null) {
106
+ continue;
107
+ }
108
+ const variable = variables.find((candidate) => candidate.name === prop.value.name);
109
+ if (banned.has(key)) {
110
+ reportReads(variable, key);
111
+ } else if (key === PROMISES) {
112
+ scanNamespace(variable);
113
+ }
114
+ }
115
+ }
116
+
117
+ /** `<ns>.name`, `<ns>['name']` and `<ns>.promises.name` off one namespace expression. */
118
+ function scanMember(node) {
119
+ const parent = node.parent;
120
+ if (parent.type !== 'MemberExpression' || parent.object !== node) {
121
+ return;
122
+ }
123
+ const name = spelledName(parent.property, parent.computed);
124
+ if (banned.has(name)) {
125
+ report(parent, name);
126
+ } else if (name === PROMISES) {
127
+ scanMember(parent);
128
+ }
129
+ }
130
+
131
+ /** A binding of the whole fs module: `fs.name` / `fs['name']`, and a destructure of it. */
132
+ function scanNamespace(variable) {
133
+ for (const ref of variable?.references ?? []) {
134
+ const id = ref.identifier;
135
+ const parent = id.parent;
136
+ if (!ref.isRead()) {
137
+ continue;
138
+ }
139
+ if (parent.type === 'VariableDeclarator' && parent.init === id) {
140
+ reportDestructured(parent);
141
+ } else {
142
+ scanMember(id);
143
+ }
144
+ }
145
+ }
146
+
147
+ return {
148
+ ImportDeclaration(node) {
149
+ if (node.importKind === 'type' || !modules.has(node.source.value)) {
150
+ return;
151
+ }
152
+ for (const spec of node.specifiers) {
153
+ const [variable] = sourceCode.getDeclaredVariables(spec);
154
+ const imported = spec.type === 'ImportSpecifier' ? spelledName(spec.imported, false) : null;
155
+ if (spec.type !== 'ImportSpecifier' || imported === PROMISES) {
156
+ scanNamespace(variable);
157
+ } else if (spec.importKind !== 'type' && banned.has(imported)) {
158
+ reportReads(variable, imported);
159
+ }
160
+ }
161
+ },
162
+ // `import fs = require('node:fs')`.
163
+ TSImportEqualsDeclaration(node) {
164
+ const ref = node.moduleReference;
165
+ if (ref.type === 'TSExternalModuleReference' && modules.has(spelledName(ref.expression, true))) {
166
+ scanNamespace(sourceCode.getDeclaredVariables(node)[0]);
167
+ }
168
+ },
169
+ VariableDeclarator(node) {
170
+ if (!modules.has(loadedModuleOf(node.init))) {
171
+ return;
172
+ }
173
+ if (node.id.type === 'Identifier') {
174
+ scanNamespace(sourceCode.getDeclaredVariables(node)[0]);
175
+ } else {
176
+ reportDestructured(node);
177
+ }
178
+ },
179
+ // `require('node:fs').name` / `(await import('node:fs')).name`: no binding to follow.
180
+ MemberExpression(node) {
181
+ const name = spelledName(node.property, node.computed);
182
+ if (banned.has(name) && modules.has(loadedModuleOf(node.object))) {
183
+ report(node, name);
184
+ }
185
+ },
186
+ ExportNamedDeclaration(node) {
187
+ if (!modules.has(node.source?.value)) {
188
+ return;
189
+ }
190
+ for (const spec of node.specifiers) {
191
+ const name = spelledName(spec.local, false);
192
+ if (banned.has(name)) {
193
+ report(spec, name);
194
+ }
195
+ }
196
+ },
197
+ ExportAllDeclaration(node) {
198
+ if (modules.has(node.source.value)) {
199
+ report(node, '*');
200
+ }
201
+ },
202
+ };
203
+ }
204
+
205
+ /** The syntax of a value that is a function, never a link type: `() => …`, `function () {}`. */
206
+ const FUNCTION_LITERALS = new Set(['ArrowFunctionExpression', 'FunctionExpression']);
207
+
208
+ /**
209
+ * Whether the use at `node` is a CALL that passes a third argument which can be a link type — not a
210
+ * function literal (a callback), not `undefined`, not `null`. Syntax only: a callback passed by
211
+ * name in the third place (`fs.symlink(t, p, done)`) reads as a type, as a variable holding one does.
212
+ */
213
+ function passesThirdArgument(node) {
214
+ const call = node.parent;
215
+ if (call?.type !== 'CallExpression' || call.callee !== node) {
216
+ return false;
217
+ }
218
+ const third = call.arguments[2];
219
+ if (third === undefined || FUNCTION_LITERALS.has(third.type)) {
220
+ return false;
221
+ }
222
+ return !(third.type === 'Identifier' && third.name === 'undefined') && !(third.type === 'Literal' && third.value === null);
223
+ }
224
+
225
+ /**
226
+ * A rule banning fs functions in product source (`packages/<pkg>/src/`, not a test file), with an
227
+ * `allowFiles: string[]` option of repo-relative paths it skips. Not recommended: the scope is a
228
+ * claim about this repo's layout, and an adopter's tree has its own.
229
+ *
230
+ * @param {object} spec
231
+ * @param {{ description: string, bans: string, useInstead: string }} spec.docs - The rule's docs
232
+ * @param {string} spec.messageId - The id of its message for a `banned` function
233
+ * @param {string} spec.message - The message; `{{name}}` is the banned function's name
234
+ * @param {Set<string>} spec.modules - The module specifiers whose functions are banned
235
+ * @param {Set<string>} spec.banned - The function names banned in every use
236
+ * @param {{ names: Set<string>, messageId: string, message: string } | null} spec.untyped - Functions
237
+ * banned in every use EXCEPT a call that passes a third (type) argument — `symlink`, whose type
238
+ * Node on Windows otherwise picks by following the target — and their message; `null` for none
239
+ */
240
+ function productSourceFsBan({ docs, messageId, message, modules, banned, untyped }) {
241
+ const untypedNames = untyped?.names ?? new Set();
242
+ const scanned = new Set([...banned, ...untypedNames]);
243
+ const messages = untyped === null ? { [messageId]: message } : { [messageId]: message, [untyped.messageId]: untyped.message };
244
+ return {
245
+ meta: {
246
+ type: 'problem',
247
+ docs: { ...docs, category: 'Filesystem and process', recommended: false, recommendedSeverity: 'error' },
248
+ schema: [
249
+ {
250
+ type: 'object',
251
+ properties: {
252
+ allowFiles: { type: 'array', items: { type: 'string' }, uniqueItems: true },
253
+ },
254
+ additionalProperties: false,
255
+ },
256
+ ],
257
+ messages,
258
+ },
259
+
260
+ create(context) {
261
+ const filename = context.filename ?? context.getFilename();
262
+ const isAllowed = createExemptPathMatcher(context.options?.[0]?.allowFiles ?? []);
263
+ if (!isProductSource(filename) || isAllowed(filename)) {
264
+ return {};
265
+ }
266
+ const report = (node, name) => {
267
+ if (!untypedNames.has(name)) {
268
+ context.report({ node, messageId, data: { name } });
269
+ } else if (!passesThirdArgument(node)) {
270
+ context.report({ node, messageId: untyped.messageId, data: { name } });
271
+ }
272
+ };
273
+ return fsBindingVisitors(context, { modules, banned: scanned, report });
274
+ },
275
+ };
276
+ }
277
+
278
+ module.exports = { fsBindingVisitors, isProductSource, productSourceFsBan };
@@ -0,0 +1,364 @@
1
+ /**
2
+ * ESLint rule: no-adhoc-errno
3
+ *
4
+ * Bans classifying a filesystem failure by hand. An errno name written as a string
5
+ * in a classifying position is a private errno table, and every private table
6
+ * decides a refusal code a little differently from its neighbours. One classifier
7
+ * (`fsFaultOf` / `classifyFsFault`, plus the single-errno predicates such as
8
+ * `isPathAbsentError`) owns the vocabulary; everything else asks it.
9
+ *
10
+ * Two reports:
11
+ *
12
+ * - `literal` — an errno name as a string that is (a) an operand of `===` `!==`
13
+ * `==` `!=`, (b) a `switch` `case` test, (c) an element of an array literal that
14
+ * is passed to `new Set(...)` or bound to a `const`, or (d) the argument of
15
+ * `.has()` / `.includes()`. (d) also catches the message match
16
+ * `errorMessage.includes('ENOENT')`, which hides an errno in prose.
17
+ * - `adhocRefusal` — inside a `catch (e)`, a `new CommandRefusalError(...)` or
18
+ * `new VatError(...)` whose options carry `cause: e`, where the same catch body
19
+ * also reads `e.code` or calls `fsFaultOf(e)` / `isPathAbsentError(e)`. The catch
20
+ * is deciding the refusal itself; `classifyFsFault` decides it once.
21
+ *
22
+ * An inline array is a lookup table too: `['ENOENT', 'EACCES'].includes(e.code)` (and
23
+ * `.some` / `.indexOf` / `.find` / `.findIndex` / `.every`). An identifier bound once
24
+ * by `const` to an errno string (`const C = 'ENOENT'; e.code === C`) is flagged at its
25
+ * use, and a template literal with no expressions counts as the string it spells.
26
+ *
27
+ * What counts as an errno is a SET, not a pattern: the union of
28
+ * `os.constants.errno` across hosts, plus `EFTYPE` and `UNKNOWN` (libuv names Node
29
+ * does not export) and `EHOSTDOWN` (which Node on macOS omits). A name that merely
30
+ * looks like one (`EXAMPLE`) is not flagged, and `EOF` is not an errno, so it needs
31
+ * no special case.
32
+ *
33
+ * Deliberately out of scope (each is a position this rule does not read):
34
+ * - helper-call arguments, `isErr(e, 'ENOENT')`: the callee is the private table,
35
+ * and judging arbitrary calls would flag every legitimate use of a name;
36
+ * - object literals, `{ code: 'ENOENT' }` / `{ ENOENT: 1 }`: those build or describe
37
+ * an error (fixtures, tables keyed by name), they do not classify one;
38
+ * - dynamic strings, built with a template expression or `+`: not decidable statically.
39
+ *
40
+ * Exempt: test files, and the files named by `exemptFiles` (the ones that must name
41
+ * errnos: the errno table, the git-stderr parser, the fault-injecting fs). No
42
+ * default exemption ships, so a consumer declares its own.
43
+ */
44
+
45
+ const {
46
+ EXEMPT_FILES_SCHEMA,
47
+ UNANCHORED_EXEMPT_FILE,
48
+ UNANCHORED_EXEMPT_MESSAGE,
49
+ createConfigurableExemptPathMatcher,
50
+ isTestFile,
51
+ reportUnanchoredExemptEntries,
52
+ } = require('./exempt-path-matcher.cjs');
53
+
54
+ /**
55
+ * The errno vocabulary, spelled out so the set is identical on every host.
56
+ *
57
+ * `os.constants.errno` differs by platform (Linux exports names macOS omits, and
58
+ * macOS omits `EHOSTDOWN`), so reading it at lint time would make the rule pass on
59
+ * one CI leg and fail on another. A rule module also may not require any external
60
+ * module (`subpath-purity.test.ts`). So the union is written out here, and
61
+ * `no-adhoc-errno.test.ts` fails if this host's `os.constants.errno` has a key the
62
+ * list lacks. `EFTYPE` and `UNKNOWN` are libuv names Node does not export.
63
+ */
64
+ const ERRNO_NAMES = new Set([
65
+ 'E2BIG', 'EACCES', 'EADDRINUSE', 'EADDRNOTAVAIL', 'EAFNOSUPPORT', 'EAGAIN', 'EALREADY',
66
+ 'EBADF', 'EBADMSG', 'EBUSY', 'ECANCELED', 'ECHILD', 'ECONNABORTED', 'ECONNREFUSED',
67
+ 'ECONNRESET', 'EDEADLK', 'EDESTADDRREQ', 'EDOM', 'EDQUOT', 'EEXIST', 'EFAULT', 'EFBIG',
68
+ 'EHOSTUNREACH', 'EIDRM', 'EILSEQ', 'EINPROGRESS', 'EINTR', 'EINVAL', 'EIO', 'EISCONN',
69
+ 'EISDIR', 'ELOOP', 'EMFILE', 'EMLINK', 'EMSGSIZE', 'EMULTIHOP', 'ENAMETOOLONG', 'ENETDOWN',
70
+ 'ENETRESET', 'ENETUNREACH', 'ENFILE', 'ENOBUFS', 'ENODATA', 'ENODEV', 'ENOENT', 'ENOEXEC',
71
+ 'ENOLCK', 'ENOLINK', 'ENOMEM', 'ENOMSG', 'ENOPROTOOPT', 'ENOSPC', 'ENOSR', 'ENOSTR',
72
+ 'ENOSYS', 'ENOTCONN', 'ENOTDIR', 'ENOTEMPTY', 'ENOTSOCK', 'ENOTSUP', 'ENOTTY', 'ENXIO',
73
+ 'EOPNOTSUPP', 'EOVERFLOW', 'EPERM', 'EPIPE', 'EPROTO', 'EPROTONOSUPPORT', 'EPROTOTYPE',
74
+ 'ERANGE', 'EROFS', 'ESPIPE', 'ESRCH', 'ESTALE', 'ETIME', 'ETIMEDOUT', 'ETXTBSY',
75
+ 'EWOULDBLOCK', 'EXDEV',
76
+ // Linux-only names:
77
+ 'EADV', 'EBADE', 'EBADFD', 'EBADR', 'EBADRQC', 'EBADSLT', 'EBFONT', 'ECHRNG', 'ECOMM',
78
+ 'EDEADLOCK', 'EDOTDOT', 'EISNAM', 'EKEYEXPIRED', 'EKEYREJECTED', 'EKEYREVOKED', 'EL2HLT',
79
+ 'EL2NSYNC', 'EL3HLT', 'EL3RST', 'ELIBACC', 'ELIBBAD', 'ELIBEXEC', 'ELIBMAX', 'ELIBSCN',
80
+ 'ELNRNG', 'EMEDIUMTYPE', 'ENAVAIL', 'ENOANO', 'ENOKEY', 'ENOMEDIUM', 'ENONET', 'ENOPKG',
81
+ 'ENOTBLK', 'ENOTNAM', 'ENOTRECOVERABLE', 'ENOTUNIQ', 'EOWNERDEAD', 'EPFNOSUPPORT', 'EREMCHG',
82
+ 'EREMOTE', 'EREMOTEIO', 'ERESTART', 'ERFKILL', 'ESHUTDOWN', 'ESOCKTNOSUPPORT', 'ESRMNT',
83
+ 'ESTRPIPE', 'ETOOMANYREFS', 'EUCLEAN', 'EUNATCH', 'EUSERS', 'EXFULL',
84
+ // Not in os.constants.errno on every host:
85
+ 'EFTYPE', 'UNKNOWN', 'EHOSTDOWN',
86
+ ]);
87
+
88
+ const COMPARISON_OPERATORS = new Set(['===', '!==', '==', '!=']);
89
+ const MEMBERSHIP_METHODS = new Set(['has', 'includes']);
90
+ /** Methods that make an INLINE array literal a lookup table: `['ENOENT'].includes(e.code)`. */
91
+ const ARRAY_LOOKUP_METHODS = new Set(['includes', 'some', 'indexOf', 'find', 'findIndex', 'every']);
92
+ const FUNCTION_TYPES = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression']);
93
+ const REFUSAL_CLASSES = new Set(['CommandRefusalError', 'VatError']);
94
+ const ERRNO_PREDICATES = new Set(['fsFaultOf', 'isPathAbsentError']);
95
+
96
+ /**
97
+ * The string a literal spells: a string `Literal`, or a template literal with no
98
+ * expressions (a backticked ENOENT is the same literal).
99
+ *
100
+ * @returns {string | undefined}
101
+ */
102
+ function literalText(node) {
103
+ if (node?.type === 'Literal') {
104
+ return typeof node.value === 'string' ? node.value : undefined;
105
+ }
106
+ if (node?.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis.length === 1) {
107
+ return node.quasis[0].value.cooked ?? undefined;
108
+ }
109
+ return undefined;
110
+ }
111
+
112
+ /** @returns {boolean} True when `node` is a string literal naming an errno. */
113
+ function isErrnoLiteral(node) {
114
+ const text = literalText(node);
115
+ return text !== undefined && ERRNO_NAMES.has(text);
116
+ }
117
+
118
+ /** The declaring variable of an identifier, found by walking out through its scopes. */
119
+ function resolveVariable(sourceCode, node) {
120
+ for (let scope = sourceCode.getScope(node); scope; scope = scope.upper) {
121
+ const variable = scope.set.get(node.name);
122
+ if (variable) {
123
+ return variable;
124
+ }
125
+ }
126
+ return undefined;
127
+ }
128
+
129
+ /** True when `node` is an identifier bound once, by `const`, to an errno literal. */
130
+ function isConstErrnoAlias(sourceCode, node) {
131
+ if (node.type !== 'Identifier') {
132
+ return false;
133
+ }
134
+ const variable = resolveVariable(sourceCode, node);
135
+ const definition = variable?.defs.length === 1 ? variable.defs[0] : undefined;
136
+ return (
137
+ definition?.type === 'Variable' &&
138
+ definition.parent?.kind === 'const' &&
139
+ definition.node.id.type === 'Identifier' &&
140
+ isErrnoLiteral(unwrapExpression(definition.node.init))
141
+ );
142
+ }
143
+
144
+ /** Step over `as const`, `satisfies` and `!` wrappers to the expression inside. */
145
+ function unwrapExpression(node) {
146
+ let current = node;
147
+ while (
148
+ current &&
149
+ (current.type === 'TSAsExpression' ||
150
+ current.type === 'TSSatisfiesExpression' ||
151
+ current.type === 'TSNonNullExpression')
152
+ ) {
153
+ current = current.expression;
154
+ }
155
+ return current;
156
+ }
157
+
158
+ /** The array literal's enclosing context: the thing that makes it a lookup table. */
159
+ function isLookupArray(arrayNode) {
160
+ // Climb through `as const` wrappers so `['ENOENT'] as const` is seen as bound.
161
+ let child = arrayNode;
162
+ let parent = arrayNode.parent;
163
+ while (parent && unwrapExpression(parent) !== parent && parent.expression === child) {
164
+ child = parent;
165
+ parent = parent.parent;
166
+ }
167
+ if (parent?.type === 'MemberExpression') {
168
+ return (
169
+ parent.object === child &&
170
+ !parent.computed &&
171
+ parent.property.type === 'Identifier' &&
172
+ ARRAY_LOOKUP_METHODS.has(parent.property.name)
173
+ );
174
+ }
175
+ if (parent?.type === 'NewExpression') {
176
+ return parent.callee.type === 'Identifier' && parent.callee.name === 'Set' && parent.arguments[0] === child;
177
+ }
178
+ return (
179
+ parent?.type === 'VariableDeclarator' &&
180
+ parent.init === child &&
181
+ parent.parent?.type === 'VariableDeclaration' &&
182
+ parent.parent.kind === 'const'
183
+ );
184
+ }
185
+
186
+ /** @returns {boolean} True when the literal sits in one of the four classifying positions. */
187
+ function isClassifyingPosition(node) {
188
+ const parent = node.parent;
189
+ switch (parent?.type) {
190
+ case 'BinaryExpression':
191
+ return COMPARISON_OPERATORS.has(parent.operator);
192
+ case 'SwitchCase':
193
+ return parent.test === node;
194
+ case 'ArrayExpression':
195
+ return isLookupArray(parent);
196
+ case 'CallExpression':
197
+ return (
198
+ parent.arguments.includes(node) &&
199
+ parent.callee.type === 'MemberExpression' &&
200
+ !parent.callee.computed &&
201
+ parent.callee.property.type === 'Identifier' &&
202
+ MEMBERSHIP_METHODS.has(parent.callee.property.name)
203
+ );
204
+ default:
205
+ return false;
206
+ }
207
+ }
208
+
209
+ /** Every AST child of `node`, skipping the back-pointer. */
210
+ function childNodes(node) {
211
+ const children = [];
212
+ for (const [key, value] of Object.entries(node)) {
213
+ if (key === 'parent') {
214
+ continue;
215
+ }
216
+ for (const item of Array.isArray(value) ? value : [value]) {
217
+ if (item && typeof item.type === 'string') {
218
+ children.push(item);
219
+ }
220
+ }
221
+ }
222
+ return children;
223
+ }
224
+
225
+ /**
226
+ * Collect every node under `root` (inclusive) that `predicate` accepts, without
227
+ * descending into a nested catch or function: those judge their own bodies, so a
228
+ * re-wrap is reported once, by the catch that owns it.
229
+ */
230
+ function collectNodes(root, predicate) {
231
+ const found = [];
232
+ const pending = [root];
233
+ while (pending.length > 0) {
234
+ const node = pending.pop();
235
+ if (predicate(node)) {
236
+ found.push(node);
237
+ }
238
+ if (node.type !== 'CatchClause' && !FUNCTION_TYPES.has(node.type)) {
239
+ pending.push(...childNodes(node));
240
+ }
241
+ }
242
+ return found;
243
+ }
244
+
245
+ const isIdentifier = (node, name) => node?.type === 'Identifier' && node.name === name;
246
+
247
+ /** `e.code`, `fsFaultOf(e)` or `isPathAbsentError(e)` for the catch parameter `name`. */
248
+ function inspectsErrno(node, name) {
249
+ if (node.type === 'MemberExpression') {
250
+ return (
251
+ isIdentifier(node.object, name) &&
252
+ ((!node.computed && isIdentifier(node.property, 'code')) ||
253
+ (node.computed && node.property.type === 'Literal' && node.property.value === 'code'))
254
+ );
255
+ }
256
+ return (
257
+ node.type === 'CallExpression' &&
258
+ node.callee.type === 'Identifier' &&
259
+ ERRNO_PREDICATES.has(node.callee.name) &&
260
+ isIdentifier(node.arguments[0], name)
261
+ );
262
+ }
263
+
264
+ /** `new CommandRefusalError(...)` / `new VatError(...)` carrying `cause: <name>`. */
265
+ function wrapsCause(node, name) {
266
+ return (
267
+ node.type === 'NewExpression' &&
268
+ node.callee.type === 'Identifier' &&
269
+ REFUSAL_CLASSES.has(node.callee.name) &&
270
+ node.arguments.some(
271
+ (argument) =>
272
+ argument.type === 'ObjectExpression' &&
273
+ argument.properties.some(
274
+ (property) =>
275
+ property.type === 'Property' &&
276
+ !property.computed &&
277
+ (isIdentifier(property.key, 'cause') || property.key.value === 'cause') &&
278
+ isIdentifier(property.value, name),
279
+ ),
280
+ )
281
+ );
282
+ }
283
+
284
+ module.exports = {
285
+ meta: {
286
+ type: 'problem',
287
+ docs: {
288
+ description:
289
+ 'Ban classifying a filesystem failure by hand: an errno name as a string in a comparison, case, Set/const array or .has()/.includes(), and a catch that inspects the errno then wraps it as a refusal cause',
290
+ category: 'Filesystem and process',
291
+ bans: 'an errno name (`ENOENT`, `EACCES`, …) as a string in a classifying position, and a catch that reads `e.code` then builds a refusal with `cause: e`',
292
+ useInstead: '`fsFaultOf(e)` / `classifyFsFault(e, …)`, or a single-errno predicate such as `isPathAbsentError(e)`',
293
+ // Not in `recommended`: the remedy names this repo's own classifier
294
+ // (`fsFaultOf`, `classifyFsFault`) and the refusal classes `CommandRefusalError` /
295
+ // `VatError`, none of which an adopter has. VAT enables it explicitly, naming
296
+ // the files that must spell errnos (the table, the git-stderr parser, the
297
+ // fault-injecting fs).
298
+ recommended: false,
299
+ recommendedSeverity: 'error',
300
+ },
301
+ fixable: null,
302
+ schema: [EXEMPT_FILES_SCHEMA],
303
+ messages: {
304
+ literal:
305
+ "Errno name '{{name}}' written as a string classifies a filesystem failure by hand. " +
306
+ 'Ask the one classifier instead: fsFaultOf(e)?.faultClass, classifyFsFault(e, …), or a ' +
307
+ 'single-errno predicate such as isPathAbsentError(e). A private errno table drifts from its neighbours.',
308
+ adhocRefusal:
309
+ 'This catch inspects the errno ({{param}}.code / fsFaultOf / isPathAbsentError) and then builds a refusal ' +
310
+ 'with cause: {{param}} by hand. Let classifyFsFault({{param}}, …) pick the refusal once.',
311
+ [UNANCHORED_EXEMPT_FILE]: UNANCHORED_EXEMPT_MESSAGE,
312
+ },
313
+ },
314
+
315
+ create(context) {
316
+ const filename = context.getFilename();
317
+
318
+ if (createConfigurableExemptPathMatcher([])(context)(filename)) {
319
+ // Still surface a malformed exemption list: this file may be exempt only
320
+ // BECAUSE an entry is unanchored.
321
+ return {
322
+ Program(node) {
323
+ reportUnanchoredExemptEntries(context, node);
324
+ },
325
+ };
326
+ }
327
+
328
+ if (isTestFile(filename)) {
329
+ return {};
330
+ }
331
+
332
+ return {
333
+ Program(node) {
334
+ reportUnanchoredExemptEntries(context, node);
335
+ },
336
+
337
+ 'Literal, TemplateLiteral'(node) {
338
+ if (isErrnoLiteral(node) && isClassifyingPosition(node)) {
339
+ context.report({ node, messageId: 'literal', data: { name: literalText(node) } });
340
+ }
341
+ },
342
+
343
+ // `const C = 'ENOENT'; e.code === C`: flagged at the use, where the check is.
344
+ Identifier(node) {
345
+ if (isClassifyingPosition(node) && isConstErrnoAlias(context.sourceCode, node)) {
346
+ context.report({ node, messageId: 'literal', data: { name: node.name } });
347
+ }
348
+ },
349
+
350
+ CatchClause(node) {
351
+ if (node.param?.type !== 'Identifier') {
352
+ return;
353
+ }
354
+ const param = node.param.name;
355
+ if (collectNodes(node.body, (child) => inspectsErrno(child, param)).length === 0) {
356
+ return;
357
+ }
358
+ for (const wrapped of collectNodes(node.body, (child) => wrapsCause(child, param))) {
359
+ context.report({ node: wrapped, messageId: 'adhocRefusal', data: { param } });
360
+ }
361
+ },
362
+ };
363
+ },
364
+ };
@@ -11,9 +11,8 @@
11
11
  * production code at it would be worse advice than the bare call. Prefer a
12
12
  * junction for a directory link on win32, or catch the failure and name the
13
13
  * missing privilege. An `eslint-disable` justification is the sanctioned way
14
- * to say "this platform is deliberately out of scope" — see
15
- * `cli/src/commands/agent/install.ts`, where `--dev` is knowingly unavailable
16
- * on an unprivileged Windows and fails saying exactly that.
14
+ * to say "this is the guarded form" — see `dev-tools/src/link-workspace-packages.ts`,
15
+ * which makes a junction on win32 and names the package a failure was for.
17
16
  *
18
17
  * ⚠️ **The name is now narrower than the rule.** It covers shipped code too;
19
18
  * renaming it is a public-API change to `@vibe-agent-toolkit/utils/eslint` and
@@ -23,7 +23,7 @@
23
23
  * The rule is a FLOOR, deliberately syntactic: the clause is fine if either
24
24
  *
25
25
  * 1. the error binding is REFERENCED anywhere in the body — narrowing on it
26
- * (`isFilesystemAccessError(e)`, `e instanceof X`, `e.code === 'ENOENT'`),
26
+ * (`fsFaultOf(e)`, `e instanceof X`, `e.code === 'ENOENT'`),
27
27
  * carrying it into a report (`errors.push(String(e))`, `{ ok: false, error }`),
28
28
  * or logging it — or
29
29
  * 2. the body THROWS (a rethrow, or a translation into a louder error), at the