@vibe-agent-toolkit/utils 0.1.42 → 0.2.0-rc.2
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.
- package/README.md +19 -3
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +88 -2
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs-utils.d.ts +389 -30
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +425 -56
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +2 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +7 -1
- package/dist/fs.js.map +1 -1
- package/dist/git-root-cache.d.ts +44 -0
- package/dist/git-root-cache.d.ts.map +1 -0
- package/dist/git-root-cache.js +68 -0
- package/dist/git-root-cache.js.map +1 -0
- package/dist/git-utils.d.ts +11 -0
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +28 -8
- package/dist/git-utils.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -2
- package/dist/index.js.map +1 -1
- package/dist/numeric-args.d.ts +24 -0
- package/dist/numeric-args.d.ts.map +1 -0
- package/dist/numeric-args.js +37 -0
- package/dist/numeric-args.js.map +1 -0
- package/dist/path-core.d.ts +30 -0
- package/dist/path-core.d.ts.map +1 -1
- package/dist/path-core.js +32 -0
- package/dist/path-core.js.map +1 -1
- package/dist/path.d.ts +1 -1
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +1 -1
- package/dist/path.js.map +1 -1
- package/dist/project-utils.d.ts +7 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +9 -1
- package/dist/project-utils.js.map +1 -1
- package/dist/test-helpers.d.ts +16 -0
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +28 -1
- package/dist/test-helpers.js.map +1 -1
- package/eslint/README.md +1 -1
- package/eslint/rules/dead-import.cjs +61 -11
- package/eslint/rules/eslint-rule-factory.cjs +16 -1
- package/eslint/rules/no-manual-path-normalize.cjs +24 -3
- package/eslint/rules/path-function-rule-factory.cjs +99 -20
- package/eslint/rules/prefer-startswith-over-regex.cjs +24 -1
- package/package.json +2 -2
package/dist/fs-utils.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Filesystem utilities
|
|
3
3
|
*/
|
|
4
|
+
// ⚠️ The sync half is reached through the DEFAULT object (`nodeFs.existsSync`),
|
|
5
|
+
// not as named imports, and that is load-bearing rather than stylistic. Node
|
|
6
|
+
// snapshots a builtin's named ESM exports at import time, so `vi.spyOn(fs,
|
|
7
|
+
// 'existsSync')` cannot see a call made through a named binding — the spy
|
|
8
|
+
// attaches and counts zero, which reads exactly like "this function performs no
|
|
9
|
+
// I/O". `classifyFilenameCaseFrom` is guarded by precisely that assertion (it
|
|
10
|
+
// must reach neither `readdir` nor this pair), so a "tidy-up" back to named
|
|
11
|
+
// imports would silently disarm the guard. The async half below already uses the
|
|
12
|
+
// default object for the same reason.
|
|
13
|
+
import nodeFs from 'node:fs';
|
|
4
14
|
import fs from 'node:fs/promises';
|
|
5
15
|
import path from 'node:path';
|
|
6
|
-
import {
|
|
16
|
+
import { promisify } from 'node:util';
|
|
17
|
+
import { toForwardSlash, toNfc } from './path-core.js';
|
|
7
18
|
import { safePath } from './path-utils.js';
|
|
8
19
|
/**
|
|
9
20
|
* Per-run memo for the two filesystem lookups that validation repeats on values
|
|
@@ -21,11 +32,16 @@ import { safePath } from './path-utils.js';
|
|
|
21
32
|
* arbitrarily long ago. The intended lifetime is one instance per validation run,
|
|
22
33
|
* constructed as a local and collected with the run.
|
|
23
34
|
*
|
|
35
|
+
* Fill first, then judge. The loop holds no `await`, because every listing the
|
|
36
|
+
* loop could have needed was already taken:
|
|
37
|
+
*
|
|
24
38
|
* @example
|
|
25
39
|
* ```typescript
|
|
26
40
|
* const fsCache = new FsLookupCache(); // one per run
|
|
27
|
-
*
|
|
28
|
-
*
|
|
41
|
+
* const targets = links.map((link) => link.target);
|
|
42
|
+
* const siblingNames = await fillSiblingNames(targets, fsCache); // all the I/O, once
|
|
43
|
+
* for (const target of targets) {
|
|
44
|
+
* classifyFilenameCaseFrom(siblingNames, target); // pure — no syscall
|
|
29
45
|
* }
|
|
30
46
|
* ```
|
|
31
47
|
*/
|
|
@@ -34,10 +50,137 @@ export class FsLookupCache {
|
|
|
34
50
|
#listings = new Map();
|
|
35
51
|
/** Path → its canonical path, falling back to the resolved path. */
|
|
36
52
|
#realpaths = new Map();
|
|
53
|
+
/** Path → the existence/kind pair recorded the first time it was probed. */
|
|
54
|
+
#probes = new Map();
|
|
55
|
+
/** Probe calls received, and how many of them reached the filesystem. */
|
|
56
|
+
#probeCount = 0;
|
|
57
|
+
#probeMisses = 0;
|
|
58
|
+
/**
|
|
59
|
+
* Probe counters, for tests and `--debug` output.
|
|
60
|
+
*
|
|
61
|
+
* A memo whose tests never assert its hit count is theatre: every assertion
|
|
62
|
+
* about *values* still passes when the memo is disabled, because an
|
|
63
|
+
* always-miss cache returns the same answers — only more slowly. This is the
|
|
64
|
+
* one observable that dies when the memo does.
|
|
65
|
+
*/
|
|
66
|
+
get probeStats() {
|
|
67
|
+
return { probes: this.#probeCount, misses: this.#probeMisses };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Does this path exist, and is it a directory — asked once per run.
|
|
71
|
+
*
|
|
72
|
+
* **Both syscalls are preserved, in order, exactly as an uncached caller
|
|
73
|
+
* would make them.** `existsSync` then `statSync` is not the same as one
|
|
74
|
+
* `statSync`: the pair distinguishes "absent" from "present but unstattable",
|
|
75
|
+
* and the link walker's classifier branches differently on each. Collapsing
|
|
76
|
+
* them would be a behaviour change wearing the shape of an optimization, so
|
|
77
|
+
* this method deduplicates the pair rather than replacing it.
|
|
78
|
+
*
|
|
79
|
+
* Synchronous, unlike this class's other two lookups, because its caller (the
|
|
80
|
+
* skill link-graph walker) is synchronous throughout. One oracle answering
|
|
81
|
+
* both shapes beats a second class that differs only in colour.
|
|
82
|
+
*
|
|
83
|
+
* @param targetPath - Path to probe
|
|
84
|
+
* @returns The recorded existence/kind pair
|
|
85
|
+
*/
|
|
86
|
+
probe(targetPath) {
|
|
87
|
+
this.#probeCount++;
|
|
88
|
+
const cached = this.#probes.get(targetPath);
|
|
89
|
+
if (cached !== undefined)
|
|
90
|
+
return cached;
|
|
91
|
+
this.#probeMisses++;
|
|
92
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- caller-validated path
|
|
93
|
+
const exists = nodeFs.existsSync(targetPath);
|
|
94
|
+
let isDirectory = null;
|
|
95
|
+
if (exists) {
|
|
96
|
+
try {
|
|
97
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- caller-validated path
|
|
98
|
+
isDirectory = nodeFs.statSync(targetPath).isDirectory();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Present to `existsSync` but unstattable. `null` records "no answer"
|
|
102
|
+
// rather than guessing `false`, which would read as "it is a file".
|
|
103
|
+
isDirectory = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const result = { exists, isDirectory };
|
|
107
|
+
this.#probes.set(targetPath, result);
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
37
110
|
/**
|
|
38
|
-
* Canonical path for `targetPath
|
|
39
|
-
*
|
|
40
|
-
*
|
|
111
|
+
* Canonical path for `targetPath`. A path that cannot be canonicalized is
|
|
112
|
+
* answered from its **deepest existing ancestor** — that ancestor's realpath
|
|
113
|
+
* with the missing remainder re-appended — because a non-existent file has no
|
|
114
|
+
* realpath and callers comparing paths still need an answer.
|
|
115
|
+
*
|
|
116
|
+
* ⚠️ **The fallback must stay in the same NAMESPACE as the success path, which
|
|
117
|
+
* a lexical `safePath.resolve()` is not.** The only consumer of this column
|
|
118
|
+
* compares one canonical path against another (`isWithinProject` /
|
|
119
|
+
* `isWithinProjectFrom`), so an answer resolved lexically is being compared
|
|
120
|
+
* against an answer resolved through symlinks. Where the root traverses a
|
|
121
|
+
* symlink — macOS `/tmp → /private/tmp`, bind mounts, a worktree under a
|
|
122
|
+
* symlinked path — the two spellings differ and the comparison is nonsense.
|
|
123
|
+
* Measured truth table for `isWithinProject(file, root)` under a `link → real`
|
|
124
|
+
* root, before the walk:
|
|
125
|
+
*
|
|
126
|
+
* ```text
|
|
127
|
+
* existing file, symlinked root : true
|
|
128
|
+
* MISSING file, symlinked root : false ← lexical fallback, wrong namespace
|
|
129
|
+
* MISSING file, plain root : true
|
|
130
|
+
* symlink inside pointing out : false (correct either way)
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
* The middle row is user-visible: a merely BROKEN root-absolute markdown link
|
|
134
|
+
* was reported as *escaping the project*. The walk fixes it without widening
|
|
135
|
+
* containment, because the ancestor is exactly where an escaping symlink
|
|
136
|
+
* lives — a missing file behind a directory link that points outside still
|
|
137
|
+
* canonicalizes outside.
|
|
138
|
+
*
|
|
139
|
+
* The recursion goes through `this.realpath(parent)`, not a private helper, so
|
|
140
|
+
* ancestors land in the same memo and share in-flight promises. A missing
|
|
141
|
+
* file's parent directory is almost always already cached, so the common case
|
|
142
|
+
* costs no extra syscall. **The fixpoint guard is mandatory**: `path.dirname`
|
|
143
|
+
* is idempotent at a root (`'/'` on posix, `'C:/'` for a drive, `'//server/share/'`
|
|
144
|
+
* for a UNC share), so without it the walk never terminates.
|
|
145
|
+
*
|
|
146
|
+
* Errno is deliberately not inspected. EACCES on an existing file and ELOOP on
|
|
147
|
+
* a symlink cycle land in the same catch as ENOENT, and for all three the
|
|
148
|
+
* ancestor's namespace is a strictly better answer than the lexical one.
|
|
149
|
+
*
|
|
150
|
+
* ⚠️ **`promisify(nodeFs.realpath)` — NOT `fs/promises.realpath`. Node ships two
|
|
151
|
+
* different realpaths and they do not agree.** `fs.realpathSync` and the
|
|
152
|
+
* `fs.realpath` *callback* form run Node's own JS implementation: an
|
|
153
|
+
* lstat/readlink walk that preserves the casing you asked for. `fs/promises.realpath`
|
|
154
|
+
* and `fs.realpath.native` call `uv_fs_realpath` (`realpath(3)` /
|
|
155
|
+
* `GetFinalPathNameByHandleW`), which reports the casing **on disk**. On a
|
|
156
|
+
* case-insensitive filesystem — macOS and Windows — those are different strings,
|
|
157
|
+
* and this column feeds *synchronous* judges that previously called
|
|
158
|
+
* `fs.realpathSync` themselves. A column that does not match `realpathSync` byte
|
|
159
|
+
* for byte flips containment verdicts and emits findings the un-refactored code
|
|
160
|
+
* does not. Measured, Node v24.13.1 / darwin, disk holding `<B>/Sub/Target.TXT`,
|
|
161
|
+
* asked for `<B>/sub/target.txt`:
|
|
162
|
+
*
|
|
163
|
+
* ```text
|
|
164
|
+
* realpathSync : <B>/sub/target.txt ← the contract
|
|
165
|
+
* promisify(fs.realpath) : <B>/sub/target.txt ✅ matches (this call)
|
|
166
|
+
* fs/promises.realpath : <B>/Sub/Target.TXT ❌ on-disk casing
|
|
167
|
+
* fs.realpath.native : <B>/Sub/Target.TXT ❌ on-disk casing
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* They also disagree on `''`, where the sync form resolves to the cwd and the
|
|
171
|
+
* native form throws `ENOENT`. **Do not "modernize" this back to `fs/promises`** —
|
|
172
|
+
* it reads tidier and silently changes output. `packages/utils/test/fs-utils.test.ts`
|
|
173
|
+
* → *"answers a mis-cased path exactly as realpathSync does, not as the native
|
|
174
|
+
* resolver does"* pins the equivalence (and skips itself on a case-sensitive
|
|
175
|
+
* filesystem, where the two routes cannot be told apart).
|
|
176
|
+
*
|
|
177
|
+
* `promisify` is applied **per call, on the default object**, not once at module
|
|
178
|
+
* scope: an eagerly captured function bypasses any `vi.spyOn(nodeFs, 'realpath')`
|
|
179
|
+
* installed after import, so this method's I/O would count zero — indistinguishable
|
|
180
|
+
* from performing none. (`fs.realpath` carries no `util.promisify.custom`, so this
|
|
181
|
+
* promisification really does get the JS implementation; it is verified, not
|
|
182
|
+
* assumed — see *"routes canonicalization through the node:fs default object"*.)
|
|
183
|
+
* The wrapper is allocated only on a cache MISS, i.e. once per actual syscall.
|
|
41
184
|
*
|
|
42
185
|
* @param targetPath - Path to canonicalize
|
|
43
186
|
* @returns Canonical path with forward slashes on every platform
|
|
@@ -48,14 +191,39 @@ export class FsLookupCache {
|
|
|
48
191
|
return cached;
|
|
49
192
|
// Stored before the first `await` anywhere can run, so concurrent callers
|
|
50
193
|
// reaching this method share the one in-flight promise.
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
194
|
+
//
|
|
195
|
+
// No `security/detect-non-literal-fs-filename` suppression here, unlike the
|
|
196
|
+
// sibling lookups: the rule matches a member call on an fs object, and the
|
|
197
|
+
// path is passed to the promisified wrapper instead. The path is
|
|
198
|
+
// caller-validated all the same.
|
|
199
|
+
const pending = promisify(nodeFs.realpath)(targetPath)
|
|
54
200
|
.then(toForwardSlash)
|
|
55
|
-
.catch(() =>
|
|
201
|
+
.catch(() => this.#canonicalizeViaAncestor(targetPath));
|
|
56
202
|
this.#realpaths.set(targetPath, pending);
|
|
57
203
|
return pending;
|
|
58
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* The ancestor walk behind {@link FsLookupCache.realpath}'s fallback: canonicalize
|
|
207
|
+
* the parent — through the public method, so the memo and in-flight sharing
|
|
208
|
+
* apply — and re-append this path's own basename.
|
|
209
|
+
*
|
|
210
|
+
* Runs inside the already-stored promise's `.catch()`, which is what keeps the
|
|
211
|
+
* store-before-await property intact: the row for `targetPath` is in the map
|
|
212
|
+
* before any of this can start.
|
|
213
|
+
*
|
|
214
|
+
* @param targetPath - Path that could not be canonicalized
|
|
215
|
+
* @returns Canonical ancestor plus the missing remainder, forward-slashed
|
|
216
|
+
*/
|
|
217
|
+
async #canonicalizeViaAncestor(targetPath) {
|
|
218
|
+
const absolutePath = safePath.resolve(targetPath);
|
|
219
|
+
const parent = toForwardSlash(path.dirname(absolutePath));
|
|
220
|
+
// Fixpoint at a filesystem root — `/`, `C:/`, `//server/share/` — where
|
|
221
|
+
// `dirname` returns its own input. Nothing left to walk, and no guard means
|
|
222
|
+
// no termination.
|
|
223
|
+
if (parent === absolutePath)
|
|
224
|
+
return absolutePath;
|
|
225
|
+
return safePath.join(await this.realpath(parent), path.basename(absolutePath));
|
|
226
|
+
}
|
|
59
227
|
/**
|
|
60
228
|
* Entry names of `dirPath`, or `null` when it cannot be read (missing directory,
|
|
61
229
|
* no permission). The unreadable answer is cached too — re-asking is the same
|
|
@@ -100,61 +268,262 @@ export async function copyDirectory(src, dest) {
|
|
|
100
268
|
}
|
|
101
269
|
}
|
|
102
270
|
/**
|
|
103
|
-
*
|
|
271
|
+
* List the parent directory of every path in `filePaths` — the only place I/O is
|
|
272
|
+
* legal for this fact, and the pass that must run *before* any judging.
|
|
104
273
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
274
|
+
* ⚠️ **It takes FILE paths, not directory paths, deliberately.** It derives each
|
|
275
|
+
* parent with `path.dirname` itself, so exactly one function in the system owns
|
|
276
|
+
* the key derivation and a caller cannot construct a key that
|
|
277
|
+
* {@link siblingNamesFrom} then misses. Do not "simplify" this to take
|
|
278
|
+
* directories: that hands the derivation back to every call site and reopens the
|
|
279
|
+
* silent-miss class this shape closes.
|
|
108
280
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
281
|
+
* Distinct parents are listed **concurrently**: the shape this replaced asked one
|
|
282
|
+
* link at a time at judgement time, which serialised every `readdir` behind the
|
|
283
|
+
* previous link's `await`. De-duplication is by parent, so N files in one
|
|
284
|
+
* directory cost one listing; the listing itself goes through
|
|
285
|
+
* {@link FsLookupCache.readdir}, which memoizes and shares in-flight promises
|
|
286
|
+
* across fills.
|
|
112
287
|
*
|
|
113
|
-
*
|
|
114
|
-
* would let an unmigrated call site silently keep the un-memoized behaviour, which
|
|
115
|
-
* is a no-op wearing the shape of a fix. `new FsLookupCache()` per call reproduces
|
|
116
|
-
* the old behaviour exactly, so migrating is mechanical — but it has to be a
|
|
117
|
-
* decision someone made.
|
|
118
|
-
*
|
|
119
|
-
* @param filePath - Absolute path to the file to verify
|
|
288
|
+
* @param filePaths - File paths whose parent directories should be listed
|
|
120
289
|
* @param fsCache - Per-run lookup cache (one instance per validation run)
|
|
121
|
-
* @returns
|
|
290
|
+
* @returns The filled table; empty input yields an empty table with no syscalls
|
|
291
|
+
*/
|
|
292
|
+
export async function fillSiblingNames(filePaths, fsCache) {
|
|
293
|
+
const parentDirs = new Set();
|
|
294
|
+
for (const filePath of filePaths) {
|
|
295
|
+
parentDirs.add(path.dirname(filePath));
|
|
296
|
+
}
|
|
297
|
+
const table = new Map();
|
|
298
|
+
await Promise.all([...parentDirs].map(async (parentDir) => {
|
|
299
|
+
// Stored EXACTLY as `readdir` returned it — no Unicode folding, no copy.
|
|
300
|
+
//
|
|
301
|
+
// The fill used to fold every entry to NFC here (and `siblingNamesFrom`
|
|
302
|
+
// folded `expectedName` to match), which left the judge a pure `===` over
|
|
303
|
+
// pre-reconciled strings. It also destroyed the only evidence that could
|
|
304
|
+
// distinguish "these two spellings are the same bytes" from "these two
|
|
305
|
+
// spellings are equal only after folding" — and those are different facts
|
|
306
|
+
// on a byte-exact filesystem. Comparison semantics now live entirely in
|
|
307
|
+
// {@link classifyFilenameCase}, so the fill has no opinion to disagree
|
|
308
|
+
// with, and a hand-written row is raw `readdir` output rather than a form
|
|
309
|
+
// only the fill knew how to produce.
|
|
310
|
+
//
|
|
311
|
+
// The array is the cache's own and is deliberately not copied: the table
|
|
312
|
+
// type is `readonly string[]`, several tables may share one listing, and
|
|
313
|
+
// copying every listing per fill is exactly the per-run cost this pair
|
|
314
|
+
// exists to avoid. Treat it as immutable.
|
|
315
|
+
table.set(parentDir, await fsCache.readdir(parentDir));
|
|
316
|
+
}));
|
|
317
|
+
return table;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Read the row for `filePath` out of an already-filled table. Pure.
|
|
122
321
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
322
|
+
* **A miss throws rather than degrading to `names: null`.** The fill set is
|
|
323
|
+
* derived from exactly the paths the judge will be asked about, so a missing
|
|
324
|
+
* parent is a programming error — a path judged that nobody filled. The `null`
|
|
325
|
+
* fallback would answer it as "the directory is unreadable", which reports every
|
|
326
|
+
* file under that directory as *missing*: a wrong answer wearing the shape of a
|
|
327
|
+
* graceful degradation, and one no test of the verdict would catch.
|
|
129
328
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
329
|
+
* Internal on purpose — {@link classifyFilenameCaseFrom} is the public judge.
|
|
330
|
+
*
|
|
331
|
+
* @param table - Table filled by {@link fillSiblingNames}
|
|
332
|
+
* @param filePath - Path being asked about
|
|
333
|
+
* @returns The row: the expected basename plus the parent's entries
|
|
334
|
+
* @throws If `table` holds no entry for the path's parent directory
|
|
133
335
|
*/
|
|
134
|
-
export
|
|
135
|
-
// Get parent directory and expected filename
|
|
336
|
+
export function siblingNamesFrom(table, filePath) {
|
|
136
337
|
const parentDir = path.dirname(filePath);
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
338
|
+
// `undefined` can only mean "absent key": a filled entry is an array or an
|
|
339
|
+
// explicit `null`, never `undefined`.
|
|
340
|
+
const names = table.get(parentDir);
|
|
341
|
+
if (names === undefined) {
|
|
342
|
+
throw new Error(`No sibling listing for directory "${parentDir}" (asked about "${filePath}"). ` +
|
|
343
|
+
`Fill it with fillSiblingNames() before judging.`);
|
|
344
|
+
}
|
|
345
|
+
// Raw on this side too. This lookup reads a row; it does not judge, and
|
|
346
|
+
// folding here would be judging — see {@link classifyFilenameCase}, which
|
|
347
|
+
// needs the spelling the caller actually asked for in order to tell a
|
|
348
|
+
// byte-exact hit from a fold-only one.
|
|
349
|
+
return { expectedName: path.basename(filePath), names };
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Decide whether `row.expectedName` names a real entry, and how faithfully.
|
|
353
|
+
*
|
|
354
|
+
* Pure: no filesystem, no cache, no path parsing — it reads only the columns it
|
|
355
|
+
* is handed, which is what makes hand-written listings a legitimate test input.
|
|
356
|
+
* Both columns arrive **raw**, exactly as `readdir` and `path.basename` produced
|
|
357
|
+
* them; this function owns every comparison rule, so nothing upstream can
|
|
358
|
+
* disagree with it about what "the same filename" means.
|
|
359
|
+
*
|
|
360
|
+
* **Three passes, strictly in this order, first match wins — the order IS the
|
|
361
|
+
* contract**, because each pass accepts a strictly weaker notion of sameness and
|
|
362
|
+
* a weaker pass reached first would mislabel a file that is genuinely there:
|
|
363
|
+
*
|
|
364
|
+
* 1. **byte-exact** `entry === expectedName`. On a case-insensitive filesystem a
|
|
365
|
+
* listing can hold both `readme.md` and `README.md`, in either order; asking
|
|
366
|
+
* for `README.md` must report it present regardless of which one `readdir`
|
|
367
|
+
* happened to return first. Same argument, one form weaker, for pass 2.
|
|
368
|
+
* 2. **NFC-folded** `toNfc(entry) === toNfc(expectedName)`. `é` has two encodings
|
|
369
|
+
* (NFC `U+00E9` vs NFD `e` + `U+0301`) that are `!==` and that case-folding
|
|
370
|
+
* does not reconcile, so without this pass an accented file that plainly
|
|
371
|
+
* exists was reported flatly *missing* — not even a case-mismatch hint, since
|
|
372
|
+
* that needs pass 3 to match (ledger entry D7).
|
|
373
|
+
* 3. **case-insensitive, on the folded forms.** Folding first is required, not
|
|
374
|
+
* tidy: `toLowerCase()` does not reconcile NFC against NFD, so a name that
|
|
375
|
+
* differs in *both* case and normalization falls out as `absent` and the
|
|
376
|
+
* author loses the suggestion.
|
|
377
|
+
*
|
|
378
|
+
* ⚠️ **Passes 1 and 2 are not the same verdict, and collapsing them is a
|
|
379
|
+
* silently-wrong answer rather than a lost nicety.** The fix for D7 originally
|
|
380
|
+
* folded both sides *before* comparing, which repaired the false "missing" on
|
|
381
|
+
* macOS/APFS — and over-corrected into the opposite error on Linux/ext4, where
|
|
382
|
+
* the filesystem is byte-exact: a markdown link spelling a filename NFD while
|
|
383
|
+
* disk holds NFC genuinely 404s there, and the folded judge answered "exists,
|
|
384
|
+
* exact match, no issue". `match` is what keeps both facts: the link resolves
|
|
385
|
+
* (so it must not be reported broken), *and* it resolves only by folding (so a
|
|
386
|
+
* caller can warn). {@link classifyFilenameCaseFrom}'s consumer in
|
|
387
|
+
* `@vibe-agent-toolkit/resources` turns `'normalized'` into
|
|
388
|
+
* `LINK_NORMALIZATION_MISMATCH`.
|
|
389
|
+
*
|
|
390
|
+
* **Folding is deferred to the miss path, and that is a real saving.** Pass 1
|
|
391
|
+
* calls `toNfc` zero times, so a corpus whose links all resolve byte-exactly —
|
|
392
|
+
* every pure-ASCII corpus, i.e. nearly all of them — normalizes nothing at all.
|
|
393
|
+
* The older shape folded every entry of every directory in the fill,
|
|
394
|
+
* unconditionally.
|
|
395
|
+
*
|
|
396
|
+
* @param row - The listing row, read out of a filled table by {@link siblingNamesFrom}
|
|
397
|
+
* @returns The verdict: whether it resolves, the entry really on disk, and which pass matched
|
|
398
|
+
*/
|
|
399
|
+
export function classifyFilenameCase(row) {
|
|
400
|
+
const { expectedName, names } = row;
|
|
401
|
+
if (names === null) {
|
|
402
|
+
// Parent directory doesn't exist (or can't be read).
|
|
403
|
+
return { exists: false, actualName: null, match: 'absent' };
|
|
404
|
+
}
|
|
405
|
+
// Pass 1 — byte-exact.
|
|
406
|
+
// Tested against `undefined` rather than for truthiness: `readdir` never
|
|
407
|
+
// yields an empty entry name, but hand-written rows are this function's
|
|
408
|
+
// advertised input now that it is pure, and `''` is falsy — it would fall
|
|
409
|
+
// through to a later pass and come back as `actualName: ''` with the wrong
|
|
410
|
+
// `match`.
|
|
411
|
+
const exactMatch = names.find(entry => entry === expectedName);
|
|
412
|
+
if (exactMatch !== undefined) {
|
|
413
|
+
return { exists: true, actualName: exactMatch, match: 'exact' };
|
|
143
414
|
}
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
415
|
+
// Pass 2 — equal only after NFC folding. Reached only when pass 1 missed, so
|
|
416
|
+
// an all-ASCII corpus never pays for it.
|
|
417
|
+
const foldedExpected = toNfc(expectedName);
|
|
418
|
+
const normalizedMatch = names.find(entry => toNfc(entry) === foldedExpected);
|
|
419
|
+
if (normalizedMatch !== undefined) {
|
|
420
|
+
return { exists: true, actualName: normalizedMatch, match: 'normalized' };
|
|
421
|
+
}
|
|
422
|
+
// Pass 3 — case-insensitive over the folded forms.
|
|
423
|
+
const loweredExpected = foldedExpected.toLowerCase();
|
|
424
|
+
const caseInsensitiveMatch = names.find(entry => toNfc(entry).toLowerCase() === loweredExpected);
|
|
425
|
+
return caseInsensitiveMatch === undefined
|
|
426
|
+
? { exists: false, actualName: null, match: 'absent' }
|
|
427
|
+
: { exists: false, actualName: caseInsensitiveMatch, match: 'case_mismatch' };
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Judge `filePath` against an already-filled {@link SiblingNamesTable}.
|
|
431
|
+
*
|
|
432
|
+
* This is the judging half of the two-pass shape: {@link fillSiblingNames} does
|
|
433
|
+
* every listing first, then this runs over as many paths as you like with no
|
|
434
|
+
* interleaved I/O.
|
|
435
|
+
*
|
|
436
|
+
* **The signature is not what keeps this free of I/O — a test is.** `fs-utils.ts`
|
|
437
|
+
* imports `node:fs` and `node:fs/promises` at module scope, so this function's
|
|
438
|
+
* module reaches the filesystem freely; taking no {@link FsLookupCache} and no
|
|
439
|
+
* `fs` parameter constrains a future edit not at all, which could call
|
|
440
|
+
* `nodeFs.statSync` on the next line and still typecheck. What actually holds the
|
|
441
|
+
* property is `packages/utils/test/fs-utils.test.ts` →
|
|
442
|
+
* *"judges from a filled table, reaching neither readdir nor the sync stat pair"*:
|
|
443
|
+
* it spies `fs.readdir`, `nodeFs.existsSync` and `nodeFs.statSync` on the very
|
|
444
|
+
* default objects this module imports, drives a positive control through each so
|
|
445
|
+
* a zero cannot mean "the instrument never attached", and asserts the counts do
|
|
446
|
+
* not move across judgement. If a future check needs another fact about the parent
|
|
447
|
+
* directory, widen the *table* rather than reaching for `fs` here — and expect
|
|
448
|
+
* that test, not this signature, to be what stops you.
|
|
449
|
+
*
|
|
450
|
+
* @param table - Table filled by {@link fillSiblingNames}
|
|
451
|
+
* @param filePath - Absolute path to judge
|
|
452
|
+
* @returns The verdict — see {@link FilenameCaseVerdict}
|
|
453
|
+
* @throws If `table` holds no entry for the path's parent directory — see
|
|
454
|
+
* {@link siblingNamesFrom}
|
|
455
|
+
*/
|
|
456
|
+
export function classifyFilenameCaseFrom(table, filePath) {
|
|
457
|
+
return classifyFilenameCase(siblingNamesFrom(table, filePath));
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Canonicalize every path in `paths` — the only place I/O is legal for this
|
|
461
|
+
* fact, and the pass that must run *before* any judging.
|
|
462
|
+
*
|
|
463
|
+
* ⚠️ **Rows are keyed by the input path string exactly as given** — not a
|
|
464
|
+
* dirname, not a re-resolved form. {@link realpathFrom} looks that same string
|
|
465
|
+
* up, so any normalization applied here and not there is a silent miss (a loud
|
|
466
|
+
* one, in fact: the judge throws). Contrast {@link fillSiblingNames}, which keys
|
|
467
|
+
* by `path.dirname` *because* many files share one listing; here the answer is
|
|
468
|
+
* per path, so the path is the key.
|
|
469
|
+
*
|
|
470
|
+
* Distinct paths are canonicalized **concurrently**: the shape this replaces
|
|
471
|
+
* asked one path at a time at judgement time, which serialised every `realpath`
|
|
472
|
+
* behind the previous path's `await`. De-duplication is by path, so the same
|
|
473
|
+
* path passed N times costs one syscall; the call itself goes through
|
|
474
|
+
* {@link FsLookupCache.realpath}, which memoizes and shares in-flight promises
|
|
475
|
+
* across fills.
|
|
476
|
+
*
|
|
477
|
+
* @param paths - Paths to canonicalize
|
|
478
|
+
* @param fsCache - Per-run lookup cache (one instance per validation run)
|
|
479
|
+
* @returns The filled table; empty input yields an empty table with no syscalls
|
|
480
|
+
*/
|
|
481
|
+
export async function fillRealpaths(paths, fsCache) {
|
|
482
|
+
const distinctPaths = new Set(paths);
|
|
483
|
+
const table = new Map();
|
|
484
|
+
await Promise.all([...distinctPaths].map(async (filePath) => {
|
|
485
|
+
table.set(filePath, await fsCache.realpath(filePath));
|
|
486
|
+
}));
|
|
487
|
+
return table;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Read the canonical path for `filePath` out of an already-filled table. Pure.
|
|
491
|
+
*
|
|
492
|
+
* **A miss throws rather than degrading to a recomputed realpath.** The fill set
|
|
493
|
+
* is derived from exactly the paths the judge will be asked about, so a missing
|
|
494
|
+
* key is a programming error — a path judged that nobody filled. Recomputing it
|
|
495
|
+
* would answer *correctly* and silently reintroduce the per-path syscall this
|
|
496
|
+
* column exists to remove: a regression no test of the verdict could catch,
|
|
497
|
+
* because the verdict would be identical, only slower.
|
|
498
|
+
*
|
|
499
|
+
* Public, unlike {@link siblingNamesFrom}: a sibling-names row is not yet an
|
|
500
|
+
* answer (it still needs {@link classifyFilenameCase}), whereas here the row IS
|
|
501
|
+
* the answer — so this lookup is itself the judge for this column, and there is
|
|
502
|
+
* nothing left to keep internal.
|
|
503
|
+
*
|
|
504
|
+
* **The signature is not what keeps this free of I/O — a test is.** As with
|
|
505
|
+
* {@link classifyFilenameCaseFrom}, this module imports `node:fs` and
|
|
506
|
+
* `node:fs/promises` at module scope, so withholding a {@link FsLookupCache} from
|
|
507
|
+
* the parameter list prevents nothing. The guard is
|
|
508
|
+
* `packages/utils/test/fs-utils.test.ts` → *"judges from a filled table, reaching
|
|
509
|
+
* neither the async nor the sync realpath"*, which spies `nodeFs.realpath` and
|
|
510
|
+
* `nodeFs.realpathSync` on the module default objects, proves both instruments
|
|
511
|
+
* attached with a positive control, and asserts zero calls across judgement.
|
|
512
|
+
*
|
|
513
|
+
* @param table - Table filled by {@link fillRealpaths}
|
|
514
|
+
* @param filePath - Path being asked about, as it was handed to the fill
|
|
515
|
+
* @returns The canonical path, with forward slashes on every platform
|
|
516
|
+
* @throws If `table` holds no row for `filePath`
|
|
517
|
+
*/
|
|
518
|
+
export function realpathFrom(table, filePath) {
|
|
519
|
+
// `undefined` can only mean "absent key": a filled row is always a string,
|
|
520
|
+
// because `FsLookupCache.realpath` falls back to the deepest existing
|
|
521
|
+
// ancestor's canonical path rather than leaving an unresolvable path without
|
|
522
|
+
// an answer.
|
|
523
|
+
const realPath = table.get(filePath);
|
|
524
|
+
if (realPath === undefined) {
|
|
525
|
+
throw new Error(`No canonical path for "${filePath}". Fill it with fillRealpaths() before judging.`);
|
|
149
526
|
}
|
|
150
|
-
|
|
151
|
-
const caseInsensitiveMatch = entries.find(entry => entry.toLowerCase() === expectedName.toLowerCase());
|
|
152
|
-
// Return result:
|
|
153
|
-
// - If case-insensitive match found: exists=false (wrong case), actualName=<actual>
|
|
154
|
-
// - If no match at all: exists=false, actualName=null
|
|
155
|
-
return {
|
|
156
|
-
exists: false,
|
|
157
|
-
actualName: caseInsensitiveMatch ?? null,
|
|
158
|
-
};
|
|
527
|
+
return realPath;
|
|
159
528
|
}
|
|
160
529
|
//# sourceMappingURL=fs-utils.js.map
|
package/dist/fs-utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs-utils.js","sourceRoot":"","sources":["../src/fs-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"fs-utils.js","sourceRoot":"","sources":["../src/fs-utils.ts"],"names":[],"mappings":"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"}
|
package/dist/fs.d.ts
CHANGED
|
@@ -8,5 +8,6 @@
|
|
|
8
8
|
* `@vibe-agent-toolkit/utils/path` instead — it is dramatically cheaper.
|
|
9
9
|
*/
|
|
10
10
|
export { normalizePath, normalizedTmpdir, mkdirSyncReal, resolveFromImportMeta, dynamicImportPath, } from './path-utils.js';
|
|
11
|
-
export { copyDirectory, FsLookupCache,
|
|
11
|
+
export { classifyFilenameCaseFrom, copyDirectory, fillRealpaths, fillSiblingNames, FsLookupCache, realpathFrom, } from './fs-utils.js';
|
|
12
|
+
export type { FilenameCaseVerdict, FilenameMatch, PathProbe, PathProbeStats, RealpathTable, SiblingNamesTable, } from './fs-utils.js';
|
|
12
13
|
//# sourceMappingURL=fs.d.ts.map
|
package/dist/fs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAQzB,OAAO,EACL,wBAAwB,EACxB,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,YAAY,GACb,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,mBAAmB,EACnB,aAAa,EACb,SAAS,EACT,cAAc,EACd,aAAa,EACb,iBAAiB,GAClB,MAAM,eAAe,CAAC"}
|
package/dist/fs.js
CHANGED
|
@@ -8,5 +8,11 @@
|
|
|
8
8
|
* `@vibe-agent-toolkit/utils/path` instead — it is dramatically cheaper.
|
|
9
9
|
*/
|
|
10
10
|
export { normalizePath, normalizedTmpdir, mkdirSyncReal, resolveFromImportMeta, dynamicImportPath, } from './path-utils.js';
|
|
11
|
-
|
|
11
|
+
// Two materialized columns, each a fill+judge pair and nothing else. For sibling
|
|
12
|
+
// names the judge is `classifyFilenameCaseFrom`, and the internal
|
|
13
|
+
// `classifyFilenameCase`/`siblingNamesFrom` members plus the `SiblingNames` row
|
|
14
|
+
// type they trade in stay module-local — a row there is not yet an answer. For
|
|
15
|
+
// realpaths the row IS the answer, so the row lookup `realpathFrom` is itself the
|
|
16
|
+
// judge and is exported. See `index.ts` for the full reasoning.
|
|
17
|
+
export { classifyFilenameCaseFrom, copyDirectory, fillRealpaths, fillSiblingNames, FsLookupCache, realpathFrom, } from './fs-utils.js';
|
|
12
18
|
//# sourceMappingURL=fs.js.map
|
package/dist/fs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,
|
|
1
|
+
{"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAEzB,iFAAiF;AACjF,kEAAkE;AAClE,gFAAgF;AAChF,+EAA+E;AAC/E,kFAAkF;AAClF,gEAAgE;AAChE,OAAO,EACL,wBAAwB,EACxB,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,YAAY,GACb,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The module-level memo behind `gitFindRoot`, in its own leaf module.
|
|
3
|
+
*
|
|
4
|
+
* Two modules need it and they must not need each other:
|
|
5
|
+
*
|
|
6
|
+
* - `git-utils.ts` reads and writes it (it owns the walk).
|
|
7
|
+
* - `project-utils.ts` clears it from `resetProjectRootCaches()`, so callers
|
|
8
|
+
* have ONE reset to remember rather than one per walk-up cache.
|
|
9
|
+
*
|
|
10
|
+
* Having `project-utils.ts` import `git-utils.ts` for that would drag `which`
|
|
11
|
+
* and `node:child_process` into the `./project` entry, which exists precisely so
|
|
12
|
+
* root discovery costs no third-party packages to reach. This file imports
|
|
13
|
+
* nothing, so both sides can depend on it.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately not re-exported from `index.ts` or any subpath: the memo is an
|
|
16
|
+
* implementation detail of `gitFindRoot`, and a second public reset name is a
|
|
17
|
+
* reset callers forget to call.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Look up a memoized git root.
|
|
21
|
+
*
|
|
22
|
+
* @param dir - Resolved directory to look up
|
|
23
|
+
* @returns The memoized answer (possibly `null`), or `undefined` if unknown
|
|
24
|
+
*/
|
|
25
|
+
export declare function lookupGitRoot(dir: string): string | null | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Record `gitRoot` as the answer for every directory a walk climbed through.
|
|
28
|
+
*
|
|
29
|
+
* @param climbed - Directories visited by the walk, deepest first
|
|
30
|
+
* @param gitRoot - The answer they all share
|
|
31
|
+
* @returns `gitRoot`, so a walk can `return rememberGitRoot(...)`
|
|
32
|
+
*/
|
|
33
|
+
export declare function rememberGitRoot(climbed: readonly string[], gitRoot: string | null): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Drop every memoized git root.
|
|
36
|
+
*
|
|
37
|
+
* Not called directly by application code — `resetProjectRootCaches()` calls it,
|
|
38
|
+
* so one reset invalidates both walk-up caches. Necessary because a `.git`
|
|
39
|
+
* directory can appear or vanish while the process lives (tests build fixtures
|
|
40
|
+
* mid-run; a long-lived host re-enters `vat audit` between edits), and a stale
|
|
41
|
+
* entry would outlive the change.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resetGitRootCache(): void;
|
|
44
|
+
//# sourceMappingURL=git-root-cache.d.ts.map
|