@vibe-agent-toolkit/utils 0.2.0-rc.6 → 0.2.0-rc.8
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 +43 -9
- package/dist/asset-reference.js +3 -11
- package/dist/asset-reference.js.map +1 -1
- package/dist/crawl-timing.d.ts +3 -3
- package/dist/crawl-timing.js +3 -3
- package/dist/dirent-kind.d.ts +83 -0
- package/dist/dirent-kind.d.ts.map +1 -0
- package/dist/dirent-kind.js +129 -0
- package/dist/dirent-kind.js.map +1 -0
- package/dist/entrypoint.d.ts +54 -0
- package/dist/entrypoint.d.ts.map +1 -0
- package/dist/entrypoint.js +71 -0
- package/dist/entrypoint.js.map +1 -0
- package/dist/env-flag.d.ts +46 -0
- package/dist/env-flag.d.ts.map +1 -0
- package/dist/env-flag.js +57 -0
- package/dist/env-flag.js.map +1 -0
- package/dist/errors/errno.d.ts +51 -0
- package/dist/errors/errno.d.ts.map +1 -0
- package/dist/errors/errno.js +112 -0
- package/dist/errors/errno.js.map +1 -0
- package/dist/errors/vat-error.d.ts +60 -0
- package/dist/errors/vat-error.d.ts.map +1 -0
- package/dist/errors/vat-error.js +86 -0
- package/dist/errors/vat-error.js.map +1 -0
- package/dist/file-crawler.d.ts +22 -0
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +83 -22
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs/file-hash.d.ts.map +1 -1
- package/dist/fs/file-hash.js +0 -1
- package/dist/fs/file-hash.js.map +1 -1
- package/dist/fs-utils.d.ts +422 -217
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +535 -283
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +3 -2
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +5 -7
- package/dist/fs.js.map +1 -1
- package/dist/git-run.d.ts +1 -1
- package/dist/git-run.js +1 -1
- package/dist/git-tracker.d.ts +41 -2
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +84 -19
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts +47 -2
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +123 -20
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +1 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +9 -3
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -22
- package/dist/index.js.map +1 -1
- package/dist/listing-refusal.d.ts +112 -0
- package/dist/listing-refusal.d.ts.map +1 -0
- package/dist/listing-refusal.js +105 -0
- package/dist/listing-refusal.js.map +1 -0
- package/dist/path-containment.d.ts +64 -0
- package/dist/path-containment.d.ts.map +1 -0
- package/dist/path-containment.js +111 -0
- package/dist/path-containment.js.map +1 -0
- package/dist/path-core.d.ts +77 -3
- package/dist/path-core.d.ts.map +1 -1
- package/dist/path-core.js +96 -6
- package/dist/path-core.js.map +1 -1
- package/dist/path-utils.d.ts.map +1 -1
- package/dist/path-utils.js +42 -47
- package/dist/path-utils.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/process.d.ts +7 -0
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +7 -0
- package/dist/process.js.map +1 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +9 -6
- package/dist/project-utils.js.map +1 -1
- package/dist/safe-exec.d.ts +2 -1
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +26 -8
- package/dist/safe-exec.js.map +1 -1
- package/dist/skill-targets.d.ts +2 -2
- package/dist/skill-targets.js +2 -2
- package/dist/skill-test/auth-resolver.d.ts +10 -2
- package/dist/skill-test/auth-resolver.d.ts.map +1 -1
- package/dist/skill-test/auth-resolver.js +26 -11
- package/dist/skill-test/auth-resolver.js.map +1 -1
- package/dist/skill-test/spawn-claude.d.ts +2 -1
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +20 -3
- package/dist/skill-test/spawn-claude.js.map +1 -1
- package/dist/skill-test/transcript.d.ts.map +1 -1
- package/dist/skill-test/transcript.js +18 -17
- package/dist/skill-test/transcript.js.map +1 -1
- package/dist/stdio-blocking.js +13 -14
- package/dist/stdio-blocking.js.map +1 -1
- package/dist/test-helpers.d.ts +56 -193
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +115 -284
- package/dist/test-helpers.js.map +1 -1
- package/dist/testing/executables.d.ts +32 -0
- package/dist/testing/executables.d.ts.map +1 -0
- package/dist/testing/executables.js +70 -0
- package/dist/testing/executables.js.map +1 -0
- package/dist/testing/hostile-tree.d.ts +79 -0
- package/dist/testing/hostile-tree.d.ts.map +1 -0
- package/dist/testing/hostile-tree.js +164 -0
- package/dist/testing/hostile-tree.js.map +1 -0
- package/dist/testing/platform-gates.d.ts +31 -0
- package/dist/testing/platform-gates.d.ts.map +1 -0
- package/dist/testing/platform-gates.js +31 -0
- package/dist/testing/platform-gates.js.map +1 -0
- package/dist/testing/temp-dir.d.ts +255 -0
- package/dist/testing/temp-dir.d.ts.map +1 -0
- package/dist/testing/temp-dir.js +363 -0
- package/dist/testing/temp-dir.js.map +1 -0
- package/dist/testing.d.ts +21 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +27 -1
- package/dist/testing.js.map +1 -1
- package/dist/text-content.d.ts.map +1 -1
- package/dist/text-content.js +7 -1
- package/dist/text-content.js.map +1 -1
- package/dist/text-file.d.ts.map +1 -1
- package/dist/text-file.js +0 -2
- package/dist/text-file.js.map +1 -1
- package/dist/timing-dump.d.ts +1 -1
- package/dist/timing-dump.d.ts.map +1 -1
- package/dist/timing-dump.js +1 -2
- package/dist/timing-dump.js.map +1 -1
- package/dist/windows-shell.d.ts +0 -1
- package/dist/windows-shell.d.ts.map +1 -1
- package/dist/windows-shell.js +0 -1
- package/dist/windows-shell.js.map +1 -1
- package/dist/zod-introspection.js +2 -2
- package/dist/zod-introspection.js.map +1 -1
- package/eslint/README.md +132 -60
- package/eslint/index.cjs +111 -147
- package/eslint/index.d.cts +14 -14
- package/eslint/rules/callee-name.cjs +23 -0
- package/eslint/rules/commands-import-boundary.cjs +148 -0
- package/eslint/rules/dead-import.cjs +1 -1
- package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
- package/eslint/rules/eslint-rule-factory.cjs +5 -2
- package/eslint/rules/exempt-path-matcher.cjs +1 -1
- package/eslint/rules/explicit-zod-strictness.cjs +120 -0
- package/eslint/rules/no-bare-dynamic-import-path.cjs +5 -1
- package/eslint/rules/no-bare-executable-spawn.cjs +70 -0
- package/eslint/rules/no-bare-symlink-in-tests.cjs +16 -3
- package/eslint/rules/no-blind-catch.cjs +171 -0
- package/eslint/rules/no-child-process-execSync.cjs +8 -0
- package/eslint/rules/no-decaying-referent.cjs +216 -0
- package/eslint/rules/no-dotdot-containment.cjs +160 -0
- package/eslint/rules/no-file-url-string-concat.cjs +4 -1
- package/eslint/rules/no-fragile-entrypoint-guard.cjs +228 -0
- package/eslint/rules/no-fs-mkdirSync.cjs +8 -0
- package/eslint/rules/no-fs-promises-cp.cjs +7 -0
- package/eslint/rules/no-fs-realpathSync.cjs +8 -0
- package/eslint/rules/no-hardcoded-path-split.cjs +7 -2
- package/eslint/rules/no-io-in-unit-tier.cjs +128 -0
- package/eslint/rules/no-literal-process-exit.cjs +129 -0
- package/eslint/rules/no-manual-path-normalize.cjs +7 -2
- package/eslint/rules/no-os-tmpdir.cjs +8 -0
- package/eslint/rules/no-path-operations-in-comparisons.cjs +7 -2
- package/eslint/rules/no-path-sep-in-strings.cjs +7 -2
- package/eslint/rules/no-path-startswith.cjs +7 -2
- package/eslint/rules/no-process-exit-in-phase.cjs +12 -2
- package/eslint/rules/no-raw-node-path.cjs +563 -0
- package/eslint/rules/no-raw-text-decode.cjs +10 -1
- package/eslint/rules/no-registry-count-pin.cjs +255 -0
- package/eslint/rules/no-self-package-import.cjs +13 -2
- package/eslint/rules/no-test-scoped-functions.cjs +10 -3
- package/eslint/rules/no-unix-shell-commands.cjs +6 -2
- package/eslint/rules/no-unsafe-root-join.cjs +23 -2
- package/eslint/rules/no-url-pathname-for-fs.cjs +5 -1
- package/eslint/rules/no-version-literal.cjs +151 -0
- package/eslint/rules/prefer-startswith-over-regex.cjs +9 -0
- package/eslint/rules/require-justified-skip.cjs +8 -2
- package/eslint/rules/safe-import.cjs +1 -1
- package/package.json +5 -10
- package/eslint/rules/no-path-join.cjs +0 -14
- package/eslint/rules/no-path-relative.cjs +0 -14
- package/eslint/rules/no-path-resolve.cjs +0 -14
- package/eslint/rules/path-function-rule-factory.cjs +0 -496
package/dist/fs-utils.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Filesystem utilities
|
|
3
3
|
*/
|
|
4
|
+
import { VatError } from './errors/vat-error.js';
|
|
4
5
|
/**
|
|
5
6
|
* What one path looked like the first time this run asked.
|
|
6
7
|
*
|
|
@@ -22,6 +23,77 @@ export interface PathProbe {
|
|
|
22
23
|
*/
|
|
23
24
|
readonly isDirectory: boolean | null;
|
|
24
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* What one `readdir` answered: the entries, or which of the two ways it failed.
|
|
28
|
+
*
|
|
29
|
+
* ⛔ **The two failures are NOT one answer, and collapsing them is a wrong
|
|
30
|
+
* verdict rather than a lost nicety.** This used to be `string[] | null`, where
|
|
31
|
+
* one `null` meant both *"there is no such directory"* and *"I was refused"*.
|
|
32
|
+
* Only the first is absence. A POSIX `--x` directory (mode `0111`) is
|
|
33
|
+
* *traversable* — every file below it opens exactly as written — while
|
|
34
|
+
* `readdir` returns `EACCES`; a judge that walks a path component by component
|
|
35
|
+
* then declared a link that opens fine to be a missing file, and said so with
|
|
36
|
+
* the confident wrong diagnosis *"File not found"*. The condition to report is
|
|
37
|
+
* that a **directory could not be listed**, which is the caller's to decide, and
|
|
38
|
+
* it cannot decide what this type will not carry.
|
|
39
|
+
*
|
|
40
|
+
* The sibling proof that the distinction is real: `resources/src/okf/discovery.ts`
|
|
41
|
+
* already reports an unlistable subdirectory as its own `OKF_SUBDIRECTORY_UNREADABLE`
|
|
42
|
+
* finding rather than as a missing one.
|
|
43
|
+
*/
|
|
44
|
+
export type DirectoryListing =
|
|
45
|
+
/** The directory was read. `names` is exactly what `readdir` handed back. */
|
|
46
|
+
{
|
|
47
|
+
readonly outcome: 'listed';
|
|
48
|
+
readonly names: string[];
|
|
49
|
+
}
|
|
50
|
+
/** There is no such directory (`ENOENT`), or a path component is a file (`ENOTDIR`). */
|
|
51
|
+
| {
|
|
52
|
+
readonly outcome: 'absent';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The directory may well hold the entry asked about; the OS refused the
|
|
56
|
+
* question. `code` is the errno, for a caller that reports the reason.
|
|
57
|
+
*/
|
|
58
|
+
| {
|
|
59
|
+
readonly outcome: 'unreadable';
|
|
60
|
+
readonly code: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Turn a `readdir` rejection into the failure it actually is.
|
|
64
|
+
*
|
|
65
|
+
* ⚠️ **`isFilesystemAccessError` is deliberately NOT used here, and that is not
|
|
66
|
+
* an oversight.** It answers a different question — *"is this the environment's
|
|
67
|
+
* fault or a bug in our code?"* — and to answer it, it deliberately groups
|
|
68
|
+
* `ENOENT` together with `EACCES`. That grouping IS the conflation this function
|
|
69
|
+
* exists to undo, so reusing the predicate would reinstate the defect while
|
|
70
|
+
* looking like sharing.
|
|
71
|
+
*
|
|
72
|
+
* Anything that is not a recognised *absence* errno reads as unreadable,
|
|
73
|
+
* including an error carrying no errno at all: "I could not ask" is the answer
|
|
74
|
+
* that fabricates no finding, and an unrecognised failure has not established
|
|
75
|
+
* that the directory is missing.
|
|
76
|
+
*
|
|
77
|
+
* @param error - Whatever `fs.readdir` rejected with
|
|
78
|
+
* @returns The listing outcome that error stands for
|
|
79
|
+
*/
|
|
80
|
+
export declare function listingFailure(error: unknown): DirectoryListing;
|
|
81
|
+
/**
|
|
82
|
+
* The clause a finding prints about a refusal {@link TRANSIENT_LISTING_ERRNOS}
|
|
83
|
+
* calls transient — owned here, beside the list, so it describes every member.
|
|
84
|
+
*
|
|
85
|
+
* 🪤 Both consumers of `AbsenceCause.transient` used to write their own: "`X`
|
|
86
|
+
* is descriptor exhaustion" — true of `EMFILE`/`ENFILE` and false of `EAGAIN`,
|
|
87
|
+
* which is a retryable shortage of some other resource. Two lanes each carrying
|
|
88
|
+
* the wording for a fact this module was made the single owner of is exactly
|
|
89
|
+
* how the lanes come to disagree with it; the errno list and the sentence about
|
|
90
|
+
* it move together only if they live together.
|
|
91
|
+
*
|
|
92
|
+
* @param code - The errno the listing was refused with
|
|
93
|
+
* @returns A clause naming the errno and what kind of condition it is, with no
|
|
94
|
+
* trailing punctuation so a caller can continue the sentence
|
|
95
|
+
*/
|
|
96
|
+
export declare function transientRefusalClause(code: string): string;
|
|
25
97
|
/** How many probes a {@link FsLookupCache} answered, and how many cost syscalls. */
|
|
26
98
|
export interface PathProbeStats {
|
|
27
99
|
/** Probe calls received. */
|
|
@@ -51,15 +123,30 @@ export interface PathProbeStats {
|
|
|
51
123
|
* @example
|
|
52
124
|
* ```typescript
|
|
53
125
|
* const fsCache = new FsLookupCache(); // one per run
|
|
54
|
-
* const
|
|
55
|
-
* const
|
|
56
|
-
* for (const target of
|
|
57
|
-
*
|
|
126
|
+
* const requests = links.map((link) => ({ referrer: link.from, target: link.target }));
|
|
127
|
+
* const spellings = await fillPathSpellings(requests, fsCache); // all the I/O, once
|
|
128
|
+
* for (const { referrer, target } of requests) {
|
|
129
|
+
* pathSpellingFrom(spellings, referrer, target); // pure — no syscall
|
|
58
130
|
* }
|
|
59
131
|
* ```
|
|
60
132
|
*/
|
|
61
133
|
export declare class FsLookupCache {
|
|
62
134
|
#private;
|
|
135
|
+
/**
|
|
136
|
+
* The three-way spelling index over this cache's listings — one per run, for
|
|
137
|
+
* the same reason the listings themselves are.
|
|
138
|
+
*
|
|
139
|
+
* ⚠️ **It hangs off the cache rather than off a fill, and that is what makes
|
|
140
|
+
* the index pay.** A caller that judges its paths in one `fillPathSpellings`
|
|
141
|
+
* would be fine either way; a caller that judges them one at a time — which
|
|
142
|
+
* `validateLink` exists to serve — would otherwise re-index the same listing
|
|
143
|
+
* per path, and the cost would go straight back to
|
|
144
|
+
* O(paths × entries-in-that-directory) with the listing memo hiding the
|
|
145
|
+
* syscalls but not the work.
|
|
146
|
+
*
|
|
147
|
+
* Lazily built: a run that never judges a path allocates nothing.
|
|
148
|
+
*/
|
|
149
|
+
get spellingIndex(): DirectorySpellingIndex;
|
|
63
150
|
/**
|
|
64
151
|
* Probe counters, for tests and `--debug` output.
|
|
65
152
|
*
|
|
@@ -167,17 +254,49 @@ export declare class FsLookupCache {
|
|
|
167
254
|
*/
|
|
168
255
|
realpath(targetPath: string): Promise<string>;
|
|
169
256
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
257
|
+
* What `dirPath` holds, or which of the two ways the question went unanswered.
|
|
258
|
+
*
|
|
259
|
+
* A *stable* failure is cached like a success: re-asking a directory whose
|
|
260
|
+
* mode bits refuse us, or whose path is a symlink cycle, is the same failed
|
|
261
|
+
* syscall. A **transient** one is not — see {@link TRANSIENT_LISTING_ERRNOS}.
|
|
262
|
+
*
|
|
263
|
+
* ⚠️ **The transient entry is dropped only once the promise has SETTLED, and
|
|
264
|
+
* that timing is the whole design.** Deleting the row up front, or refusing to
|
|
265
|
+
* store it, would make every concurrent caller start its own `readdir` —
|
|
266
|
+
* turning the descriptor shortage `EMFILE` reports into a descriptor storm,
|
|
267
|
+
* i.e. answering the failure with more of its cause. Storing the in-flight
|
|
268
|
+
* promise keeps the collapse-N-callers-to-one-syscall property intact through
|
|
269
|
+
* the failure; evicting after it settles is what stops the *next* wave from
|
|
270
|
+
* inheriting a verdict about a moment that has passed.
|
|
271
|
+
*
|
|
272
|
+
* The alternative considered and rejected was a bounded retry inside this
|
|
273
|
+
* method. It re-issues the syscall *while the shortage is still in progress*
|
|
274
|
+
* (which is the storm again, only self-inflicted), it needs a backoff timer to
|
|
275
|
+
* be worth anything, and it hides latency inside a call every caller reads as
|
|
276
|
+
* a memo lookup. Letting the next ask pay one syscall is the same cost the
|
|
277
|
+
* cache already bounds: one per directory, per wave.
|
|
173
278
|
*
|
|
174
279
|
* @param dirPath - Directory to list
|
|
175
|
-
* @returns
|
|
280
|
+
* @returns The entry names, or why there are none to hand back
|
|
176
281
|
*/
|
|
177
|
-
readdir(dirPath: string): Promise<
|
|
282
|
+
readdir(dirPath: string): Promise<DirectoryListing>;
|
|
283
|
+
}
|
|
284
|
+
/** Thrown when a link inside the tree being copied points outside it. */
|
|
285
|
+
export declare class CopyLinkEscapesSourceError extends VatError {
|
|
286
|
+
constructor(link: string, src: string);
|
|
178
287
|
}
|
|
179
288
|
/**
|
|
180
|
-
* Recursively copy a directory
|
|
289
|
+
* Recursively copy a directory, following symlinks — contained to `src`.
|
|
290
|
+
*
|
|
291
|
+
* A link is copied as what it points at (a linked directory as its tree, a
|
|
292
|
+
* linked file as its bytes; a dangling link fails loudly in `stat`). Two
|
|
293
|
+
* refusals bound that: a link whose target is not under `src` throws
|
|
294
|
+
* {@link CopyLinkEscapesSourceError} — `scripts/etc -> /etc` used to copy
|
|
295
|
+
* `/etc` into `dist` — and a link that leads the walk back into a directory
|
|
296
|
+
* it already entered throws `DirectoryWalkRevisitedError` (`scripts/loop -> .`
|
|
297
|
+
* used to create `dest/loop/loop/…` until `ENAMETOOLONG`, writing every file
|
|
298
|
+
* at every level first). Adopter-authored trees reach this through
|
|
299
|
+
* `vat agent build`, so neither shape is exotic.
|
|
181
300
|
*
|
|
182
301
|
* @param src - Source directory path
|
|
183
302
|
* @param dest - Destination directory path
|
|
@@ -187,215 +306,320 @@ export declare class FsLookupCache {
|
|
|
187
306
|
*/
|
|
188
307
|
export declare function copyDirectory(src: string, dest: string): Promise<void>;
|
|
189
308
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
309
|
+
* Which spelling rule matched a name, and therefore how faithfully the
|
|
310
|
+
* asked-for spelling matches disk.
|
|
311
|
+
*
|
|
312
|
+
* The three that are not `absent` are ordered from most to least faithful, and
|
|
313
|
+
* every consumer that reports to a human needs the distinction: only `exact`
|
|
314
|
+
* opens on every filesystem.
|
|
315
|
+
*
|
|
316
|
+
* **The three rules are tried strictly in the order below, first match wins —
|
|
317
|
+
* the order IS the contract**, because each accepts a strictly weaker notion of
|
|
318
|
+
* sameness and a weaker rule reached first would mislabel a file that is
|
|
319
|
+
* genuinely there. {@link DirectorySpellingIndex} implements them as three
|
|
320
|
+
* lookups over one pre-built index (`indexEntry`/`lookupIn`); it is the only
|
|
321
|
+
* judge, so nothing upstream can disagree with it about what "the same
|
|
322
|
+
* filename" means.
|
|
323
|
+
*
|
|
324
|
+
* ⚠️ **`exact` and `normalized` are not the same verdict, and collapsing them
|
|
325
|
+
* is a silently-wrong answer rather than a lost nicety.** Folding both sides
|
|
326
|
+
* *before* comparing repairs the false "missing" on macOS/APFS — `é` has two
|
|
327
|
+
* encodings (NFC `U+00E9` vs NFD `e` + `U+0301`) that are `!==` and that
|
|
328
|
+
* case-folding does not reconcile, so an accented file that plainly exists was
|
|
329
|
+
* once reported flatly *missing* — and over-corrects into the opposite error on
|
|
330
|
+
* Linux/ext4, where the filesystem is byte-exact: a markdown link spelling a
|
|
331
|
+
* filename NFD while disk holds NFC genuinely 404s there, and a folded judge
|
|
332
|
+
* answers "exists, exact match, no issue". Keeping both facts is the point —
|
|
333
|
+
* the link resolves (so it must not be reported broken), *and* it resolves only
|
|
334
|
+
* by folding (so a caller can warn). `@vibe-agent-toolkit/resources` turns
|
|
335
|
+
* `'normalized'` into `LINK_NORMALIZATION_MISMATCH`. This is one of three sites
|
|
336
|
+
* on that seam; the class is collected in
|
|
337
|
+
* `docs/architecture/resource-scanning-and-caching.md` §3.6 (ledger entry D7).
|
|
338
|
+
*
|
|
339
|
+
* ⚠️ **Case-folding is applied to the NFC-folded form, not to raw bytes.**
|
|
340
|
+
* `toLowerCase()` does not reconcile NFC against NFD, so a name that differs in
|
|
341
|
+
* *both* case and normalization would fall out as `absent` and the author would
|
|
342
|
+
* lose the suggestion. The prohibition that bounds every fold — it yields a
|
|
343
|
+
* comparison key, never a path to open — is stated once at {@link toNfc}, which
|
|
344
|
+
* is also where the reason it is not folded into `safePath.resolve` lives.
|
|
345
|
+
*/
|
|
346
|
+
export type FilenameMatch =
|
|
347
|
+
/** The asked-for name and a directory entry are the same bytes. Opens anywhere. */
|
|
348
|
+
'exact'
|
|
349
|
+
/**
|
|
350
|
+
* They are different bytes that are equal after Unicode NFC folding — the same
|
|
351
|
+
* visible filename in two normalization forms. Opens on macOS/APFS and
|
|
352
|
+
* Windows; **does not open on a byte-exact filesystem** (Linux/ext4, i.e. CI
|
|
353
|
+
* and most deploy targets), where the two forms simply name different files.
|
|
196
354
|
*/
|
|
197
|
-
|
|
355
|
+
| 'normalized'
|
|
356
|
+
/** They differ by letter case (after folding). Opens only on a case-insensitive filesystem. */
|
|
357
|
+
| 'case_mismatch'
|
|
358
|
+
/** Nothing in the listing matches, or the directory could not be read. */
|
|
359
|
+
| 'absent';
|
|
360
|
+
/**
|
|
361
|
+
* Why a name is not in a listing — the two are a different fact about the tree
|
|
362
|
+
* and a different thing to tell a human.
|
|
363
|
+
*
|
|
364
|
+
* ⛔ **Kept off {@link FilenameMatch} deliberately.** That union names the
|
|
365
|
+
* *spelling rules* a name can match under, and "the directory refused to be
|
|
366
|
+
* listed" is not a spelling rule — it is the reason no rule could be tried. It
|
|
367
|
+
* carries no rank in {@link SPELLING_RANK} and no corrected spelling, and
|
|
368
|
+
* folding it in as a fifth verdict would silently un-exhaust every switch over
|
|
369
|
+
* a spelling (the OKF cross-link lane has one) without moving what those
|
|
370
|
+
* switches actually decide.
|
|
371
|
+
*/
|
|
372
|
+
export type AbsenceCause =
|
|
373
|
+
/** The directory was listed and holds nothing matching, under any rule. */
|
|
374
|
+
{
|
|
375
|
+
readonly kind: 'no_such_entry';
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* A directory on the path could not be listed, so the question was never
|
|
379
|
+
* asked. ⚠️ **This is not evidence of absence** — a `--x` directory is
|
|
380
|
+
* traversable, so the target may well open. A caller reporting it as a
|
|
381
|
+
* missing file is asserting something it has not learned.
|
|
382
|
+
*
|
|
383
|
+
* 🔑 **It carries WHICH directory and WHICH errno because the alternative was
|
|
384
|
+
* a remedy nobody can aim.** Collapsing every refusal to the bare word left
|
|
385
|
+
* each consumer able to say only "a directory on that path refused" — useless
|
|
386
|
+
* to a reader staring at a five-segment path, and identical whether the cause
|
|
387
|
+
* was a mode bit they can fix or a descriptor shortage they should just
|
|
388
|
+
* re-run past.
|
|
389
|
+
*/
|
|
390
|
+
| {
|
|
391
|
+
readonly kind: 'directory_unreadable';
|
|
392
|
+
/** The errno `readdir` refused with: `EACCES`, `EMFILE`, `ENFILE`, `ELOOP`, … */
|
|
393
|
+
readonly code: string;
|
|
198
394
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
395
|
+
* The directory that refused — **absolute**, forward-slashed.
|
|
396
|
+
*
|
|
397
|
+
* 🔒 **Sanitize before quoting it to a human.** An absolute path in a
|
|
398
|
+
* finding is the developer's `$HOME` in every CI log, and both consumers
|
|
399
|
+
* of this field re-express it against a root they own
|
|
400
|
+
* (`issueLocation(dir, projectRoot)` in the link lane,
|
|
401
|
+
* `safePath.relative(bundleRoot, dir)` in the OKF lane) before it reaches
|
|
402
|
+
* a message. It is absolute *here* because those two roots differ and the
|
|
403
|
+
* walk root this was found under is neither of them.
|
|
202
404
|
*/
|
|
203
|
-
readonly
|
|
405
|
+
readonly directory: string;
|
|
204
406
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* makes "this link only resolves after normalization" a question the judge can
|
|
208
|
-
* still answer. See {@link classifyFilenameCase}.
|
|
407
|
+
* Whether re-running could get a different answer — see
|
|
408
|
+
* {@link TRANSIENT_LISTING_ERRNOS}.
|
|
209
409
|
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* a *fact*, and the judge that wants it — a check that says "the directory
|
|
214
|
-
* itself is missing" rather than "the file is missing" — cannot recover it
|
|
215
|
-
* once the fill has thrown it away.
|
|
410
|
+
* Derived once, here, rather than by each consumer: two lanes write a
|
|
411
|
+
* "re-run before investigating" remedy off this fact, and a second errno
|
|
412
|
+
* list is exactly how those two come to disagree about it.
|
|
216
413
|
*/
|
|
217
|
-
readonly
|
|
218
|
-
}
|
|
414
|
+
readonly transient: boolean;
|
|
415
|
+
};
|
|
416
|
+
/** The refusal half of {@link AbsenceCause}: a directory that would not be listed. */
|
|
417
|
+
export type DirectoryRefusal = Extract<AbsenceCause, {
|
|
418
|
+
kind: 'directory_unreadable';
|
|
419
|
+
}>;
|
|
219
420
|
/**
|
|
220
|
-
* The
|
|
221
|
-
*
|
|
421
|
+
* The refusal a `readdir` that was refused stands for — errno, directory and
|
|
422
|
+
* whether a re-ask could answer differently, derived ONCE beside the errno list.
|
|
222
423
|
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* into one verdict.
|
|
227
|
-
*
|
|
228
|
-
* A *missing key* is a third thing again, and never a legal input to judgement:
|
|
229
|
-
* see {@link siblingNamesFrom}.
|
|
230
|
-
*/
|
|
231
|
-
export type SiblingNamesTable = ReadonlyMap<string, readonly string[] | null>;
|
|
232
|
-
/**
|
|
233
|
-
* List the parent directory of every path in `filePaths` — the only place I/O is
|
|
234
|
-
* legal for this fact, and the pass that must run *before* any judging.
|
|
235
|
-
*
|
|
236
|
-
* ⚠️ **It takes FILE paths, not directory paths, deliberately.** It derives each
|
|
237
|
-
* parent with `path.dirname` itself, so exactly one function in the system owns
|
|
238
|
-
* the key derivation and a caller cannot construct a key that
|
|
239
|
-
* {@link siblingNamesFrom} then misses. Do not "simplify" this to take
|
|
240
|
-
* directories: that hands the derivation back to every call site and reopens the
|
|
241
|
-
* silent-miss class this shape closes.
|
|
242
|
-
*
|
|
243
|
-
* Distinct parents are listed **concurrently**: the shape this replaced asked one
|
|
244
|
-
* link at a time at judgement time, which serialised every `readdir` behind the
|
|
245
|
-
* previous link's `await`. De-duplication is by parent, so N files in one
|
|
246
|
-
* directory cost one listing; the listing itself goes through
|
|
247
|
-
* {@link FsLookupCache.readdir}, which memoizes and shares in-flight promises
|
|
248
|
-
* across fills.
|
|
424
|
+
* Shared by the spelling judge (through {@link absenceCauseFor}) and the crawl
|
|
425
|
+
* that defines the population (`file-crawler.ts`), so the two lanes cannot
|
|
426
|
+
* disagree about which refusals are transient.
|
|
249
427
|
*
|
|
250
|
-
* @param
|
|
251
|
-
* @param
|
|
252
|
-
* @returns The
|
|
428
|
+
* @param listing - A `readdir` outcome that was refused
|
|
429
|
+
* @param directory - The directory that was asked about
|
|
430
|
+
* @returns The refusal, with `directory` forward-slashed
|
|
253
431
|
*/
|
|
254
|
-
export declare function
|
|
432
|
+
export declare function directoryRefusalFor(listing: Extract<DirectoryListing, {
|
|
433
|
+
outcome: 'unreadable';
|
|
434
|
+
}>, directory: string): DirectoryRefusal;
|
|
435
|
+
/** What one directory entry name matched, and how the directory spells it. */
|
|
436
|
+
export type ComponentMatch = {
|
|
437
|
+
match: Exclude<FilenameMatch, 'absent'>;
|
|
438
|
+
actualName: string;
|
|
439
|
+
} | {
|
|
440
|
+
match: 'absent';
|
|
441
|
+
because: AbsenceCause;
|
|
442
|
+
};
|
|
255
443
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* **A miss throws rather than degrading to `names: null`.** The fill set is
|
|
259
|
-
* derived from exactly the paths the judge will be asked about, so a missing
|
|
260
|
-
* parent is a programming error — a path judged that nobody filled. The `null`
|
|
261
|
-
* fallback would answer it as "the directory is unreadable", which reports every
|
|
262
|
-
* file under that directory as *missing*: a wrong answer wearing the shape of a
|
|
263
|
-
* graceful degradation, and one no test of the verdict would catch.
|
|
264
|
-
*
|
|
265
|
-
* Internal on purpose — {@link classifyFilenameCaseFrom} is the public judge.
|
|
444
|
+
* What judging a whole path said, and the two spellings a message quotes.
|
|
266
445
|
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
446
|
+
* A union rather than one interface with an optional field: {@link AbsenceCause}
|
|
447
|
+
* is required exactly when the verdict is `absent` and unreachable otherwise, so
|
|
448
|
+
* a caller cannot report a path as missing without having read *which* absence
|
|
449
|
+
* it is.
|
|
271
450
|
*/
|
|
272
|
-
export
|
|
451
|
+
export type PathSpelling = {
|
|
452
|
+
/** The worst spelling defect on the path. */
|
|
453
|
+
match: Exclude<FilenameMatch, 'absent'>;
|
|
454
|
+
/** The path relative to the walk root, spelled as the caller asked for it. */
|
|
455
|
+
askedPath: string;
|
|
456
|
+
/** The same path as disk spells it. */
|
|
457
|
+
actualPath: string;
|
|
458
|
+
} | {
|
|
459
|
+
/** No component matched — see `because` before calling anything missing. */
|
|
460
|
+
match: 'absent';
|
|
461
|
+
/** The path relative to the walk root, spelled as the caller asked for it. */
|
|
462
|
+
askedPath: string;
|
|
463
|
+
/** Empty: nothing matched, so there is no disk spelling to quote. */
|
|
464
|
+
actualPath: string;
|
|
465
|
+
/** Whether the entry is really gone, or the listing was refused. */
|
|
466
|
+
because: AbsenceCause;
|
|
467
|
+
/**
|
|
468
|
+
* What the walk DID establish before it stopped: the components above
|
|
469
|
+
* the one it could not find or could not ask about.
|
|
470
|
+
*
|
|
471
|
+
* 🪤 Carried because dropping it discarded a verdict. `Locked/t.md`
|
|
472
|
+
* against a disk `locked/` that then refuses to list: component 1 was
|
|
473
|
+
* judged and found a case mismatch — a defect that 404s on a
|
|
474
|
+
* case-sensitive filesystem whatever the mode bit below says — and a
|
|
475
|
+
* bare `absent` threw it away, so the report called the spelling
|
|
476
|
+
* "unverified" about a component VAT had verified and found wrong.
|
|
477
|
+
*/
|
|
478
|
+
verified: VerifiedPrefix;
|
|
479
|
+
};
|
|
273
480
|
/**
|
|
274
|
-
*
|
|
481
|
+
* The components of a path a walk judged before it stopped, and their verdict.
|
|
275
482
|
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
483
|
+
* Both paths are `/`-joined and relative to the walk root, like the
|
|
484
|
+
* {@link PathSpelling} they ride on; both are empty when the FIRST component
|
|
485
|
+
* is the one that could not be judged.
|
|
279
486
|
*/
|
|
280
|
-
export
|
|
281
|
-
/** The
|
|
282
|
-
'
|
|
487
|
+
export interface VerifiedPrefix {
|
|
488
|
+
/** The worst spelling defect among the judged components. */
|
|
489
|
+
readonly match: Exclude<FilenameMatch, 'absent'>;
|
|
490
|
+
/** The judged components as the caller spelled them. */
|
|
491
|
+
readonly askedPath: string;
|
|
492
|
+
/** The same components as disk spells them. */
|
|
493
|
+
readonly actualPath: string;
|
|
494
|
+
}
|
|
283
495
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
496
|
+
* Every directory a run asks about, listed once and indexed once.
|
|
497
|
+
*
|
|
498
|
+
* ⚠️ **It owns the listings and never hands one out.** That is deliberate: the
|
|
499
|
+
* defect it replaced was a per-path scan over a shared raw array, and an
|
|
500
|
+
* implementation that cannot reach the array cannot scan it. The only ways to
|
|
501
|
+
* ask a question are {@link DirectorySpellingIndex.lookup} and
|
|
502
|
+
* {@link DirectorySpellingIndex.judgePath}, both `Map.get` over an index built
|
|
503
|
+
* at most once per directory — {@link DirectorySpellingIndex.directoriesIndexed}
|
|
504
|
+
* and {@link DirectorySpellingIndex.entriesIndexed} are what a test counts to
|
|
505
|
+
* prove the work did not go back to being per-path.
|
|
506
|
+
*
|
|
507
|
+
* **Instance-per-run, like the {@link FsLookupCache} it borrows** — it holds a
|
|
508
|
+
* snapshot of directory contents and must not outlive the run that took it.
|
|
288
509
|
*/
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
/** Nothing in the listing matches, or the directory could not be read. */
|
|
293
|
-
| 'absent';
|
|
294
|
-
/** What {@link classifyFilenameCase} decided about one asked-for filename. */
|
|
295
|
-
export interface FilenameCaseVerdict {
|
|
510
|
+
export declare class DirectorySpellingIndex {
|
|
511
|
+
#private;
|
|
512
|
+
constructor(fsCache: FsLookupCache);
|
|
296
513
|
/**
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
514
|
+
* How many times a listing was turned into an index.
|
|
515
|
+
*
|
|
516
|
+
* Counted at the BUILD, not as `#indexes.size`: the size is the number of
|
|
517
|
+
* distinct directories asked about, which stays put even if every lookup
|
|
518
|
+
* rebuilds — the exact regression this number exists to catch.
|
|
301
519
|
*/
|
|
302
|
-
|
|
520
|
+
get directoriesIndexed(): number;
|
|
521
|
+
/** How many directory entries were examined, across every index built. */
|
|
522
|
+
get entriesIndexed(): number;
|
|
523
|
+
/** Every directory that has been listed, for never-reached-above-the-root pins. */
|
|
524
|
+
get indexedDirectories(): string[];
|
|
303
525
|
/**
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
526
|
+
* Ask what `directory` really calls `name`.
|
|
527
|
+
*
|
|
528
|
+
* @param directory - Absolute path of the directory to ask about
|
|
529
|
+
* @param name - One path component, spelled as the caller asked for it
|
|
530
|
+
* @returns Which rule matched and the entry's own spelling, or `absent`
|
|
308
531
|
*/
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
|
|
532
|
+
lookup(directory: string, name: string): Promise<ComponentMatch>;
|
|
533
|
+
/**
|
|
534
|
+
* Judge every component of `resolvedPath`, from `root` down.
|
|
535
|
+
*
|
|
536
|
+
* Each component is judged against the directory that actually holds it —
|
|
537
|
+
* which is the corrected spelling of the previous component, not the
|
|
538
|
+
* asked-for one, so a wrong directory name does not hide a wrong filename
|
|
539
|
+
* beneath it.
|
|
540
|
+
*
|
|
541
|
+
* ⛔ **It never looks above `root`.** The walk starts there and only
|
|
542
|
+
* descends, and a path that does not live under `root` is refused outright
|
|
543
|
+
* rather than walked from somewhere else: a verdict that depends on a
|
|
544
|
+
* directory above the root is a verdict that changes when the tree is moved.
|
|
545
|
+
* Pick a root the caller has already enumerated, and every component below it
|
|
546
|
+
* is one the *reference text* contributed — exactly the ones worth judging.
|
|
547
|
+
*
|
|
548
|
+
* @param root - Absolute path of a directory known to exist, and an ancestor
|
|
549
|
+
* of `resolvedPath` (or `resolvedPath` itself)
|
|
550
|
+
* @param resolvedPath - Absolute path to judge
|
|
551
|
+
* @returns The worst spelling defect on the path, plus both spellings of it
|
|
552
|
+
* @throws If `resolvedPath` does not live at or under `root`
|
|
553
|
+
*/
|
|
554
|
+
judgePath(root: string, resolvedPath: string): Promise<PathSpelling>;
|
|
555
|
+
}
|
|
556
|
+
/** One path to judge, paired with the file whose text asked for it. */
|
|
557
|
+
export interface PathSpellingRequest {
|
|
558
|
+
/** The referring file, whose own path was enumerated and is therefore trusted. */
|
|
559
|
+
referrer: string;
|
|
560
|
+
/** The absolute path the reference resolved to. */
|
|
561
|
+
target: string;
|
|
312
562
|
}
|
|
313
563
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* different kinds of value and routinely disagree about one file: `entry` is
|
|
332
|
-
* an *enumerated* path (`readdir` hands back whatever is on disk, commonly
|
|
333
|
-
* decomposed) while `expectedName` is a path *derived from markdown link
|
|
334
|
-
* text* (composed, as an editor writes it). `é` has two encodings (NFC
|
|
335
|
-
* `U+00E9` vs NFD `e` + `U+0301`) that are `!==` and that case-folding does
|
|
336
|
-
* not reconcile, so without this pass an accented file that plainly exists
|
|
337
|
-
* was reported flatly *missing* — not even a case-mismatch hint, since that
|
|
338
|
-
* needs pass 3 to match. This is one of three sites on that seam; the class
|
|
339
|
-
* is collected in `docs/architecture/resource-scanning-and-caching.md` §3.6
|
|
340
|
-
* (ledger entry D7).
|
|
341
|
-
* 3. **case-insensitive, on the folded forms.** Folding first is required, not
|
|
342
|
-
* tidy: `toLowerCase()` does not reconcile NFC against NFD, so a name that
|
|
343
|
-
* differs in *both* case and normalization falls out as `absent` and the
|
|
344
|
-
* author loses the suggestion.
|
|
345
|
-
*
|
|
346
|
-
* ⚠️ **Passes 1 and 2 are not the same verdict, and collapsing them is a
|
|
347
|
-
* silently-wrong answer rather than a lost nicety.** The fix for D7 originally
|
|
348
|
-
* folded both sides *before* comparing, which repaired the false "missing" on
|
|
349
|
-
* macOS/APFS — and over-corrected into the opposite error on Linux/ext4, where
|
|
350
|
-
* the filesystem is byte-exact: a markdown link spelling a filename NFD while
|
|
351
|
-
* disk holds NFC genuinely 404s there, and the folded judge answered "exists,
|
|
352
|
-
* exact match, no issue". `match` is what keeps both facts: the link resolves
|
|
353
|
-
* (so it must not be reported broken), *and* it resolves only by folding (so a
|
|
354
|
-
* caller can warn). {@link classifyFilenameCaseFrom}'s consumer in
|
|
355
|
-
* `@vibe-agent-toolkit/resources` turns `'normalized'` into
|
|
356
|
-
* `LINK_NORMALIZATION_MISMATCH`. The prohibition that bounds every fold reached
|
|
357
|
-
* from here — it yields a comparison key, never a path to open — is stated once
|
|
358
|
-
* at {@link toNfc}, which is also where the reason it is not folded into
|
|
359
|
-
* `safePath.resolve` lives.
|
|
360
|
-
*
|
|
361
|
-
* **Folding is deferred to the miss path, and that is a real saving.** Pass 1
|
|
362
|
-
* calls `toNfc` zero times, so a corpus whose links all resolve byte-exactly —
|
|
363
|
-
* every pure-ASCII corpus, i.e. nearly all of them — normalizes nothing at all.
|
|
364
|
-
* The older shape folded every entry of every directory in the fill,
|
|
365
|
-
* unconditionally.
|
|
366
|
-
*
|
|
367
|
-
* @param row - The listing row, read out of a filled table by {@link siblingNamesFrom}
|
|
368
|
-
* @returns The verdict: whether it resolves, the entry really on disk, and which pass matched
|
|
564
|
+
* Where to start judging `target`, given that `referrer`'s own path came off
|
|
565
|
+
* the filesystem rather than out of a document.
|
|
566
|
+
*
|
|
567
|
+
* ⚠️ **The root is the deepest directory the two paths share, and that choice
|
|
568
|
+
* is doing real work in both directions.** Everything *above* it was enumerated
|
|
569
|
+
* (so judging it would compare disk against disk, and on a macOS crawl that
|
|
570
|
+
* routinely means reporting an NFD component nobody wrote); everything *below*
|
|
571
|
+
* it is what the reference text contributed, and is precisely what a
|
|
572
|
+
* misspelling can hide in.
|
|
573
|
+
*
|
|
574
|
+
* Falls back to the target's own parent — i.e. judging the basename alone, the
|
|
575
|
+
* weakest useful answer — when the two paths share no meaningful ancestor
|
|
576
|
+
* (different drives on Windows, or a relative path).
|
|
577
|
+
*
|
|
578
|
+
* @param referrer - Path of the file holding the reference
|
|
579
|
+
* @param target - Absolute path the reference resolved to
|
|
580
|
+
* @returns The directory to walk down from
|
|
369
581
|
*/
|
|
370
|
-
export declare function
|
|
582
|
+
export declare function spellingWalkRoot(referrer: string, target: string): string;
|
|
371
583
|
/**
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
* **The signature is not what keeps this free of I/O — a test is.** `fs-utils.ts`
|
|
379
|
-
* imports `node:fs` and `node:fs/promises` at module scope, so this function's
|
|
380
|
-
* module reaches the filesystem freely; taking no {@link FsLookupCache} and no
|
|
381
|
-
* `fs` parameter constrains a future edit not at all, which could call
|
|
382
|
-
* `nodeFs.statSync` on the next line and still typecheck. What actually holds the
|
|
383
|
-
* property is `packages/utils/test/fs-utils.test.ts` →
|
|
384
|
-
* *"judges from a filled table, reaching neither readdir nor the sync stat pair"*:
|
|
385
|
-
* it spies `fs.readdir`, `nodeFs.existsSync` and `nodeFs.statSync` on the very
|
|
386
|
-
* default objects this module imports, drives a positive control through each so
|
|
387
|
-
* a zero cannot mean "the instrument never attached", and asserts the counts do
|
|
388
|
-
* not move across judgement. If a future check needs another fact about the parent
|
|
389
|
-
* directory, widen the *table* rather than reaching for `fs` here — and expect
|
|
390
|
-
* that test, not this signature, to be what stops you.
|
|
391
|
-
*
|
|
392
|
-
* @param table - Table filled by {@link fillSiblingNames}
|
|
393
|
-
* @param filePath - Absolute path to judge
|
|
394
|
-
* @returns The verdict — see {@link FilenameCaseVerdict}
|
|
395
|
-
* @throws If `table` holds no entry for the path's parent directory — see
|
|
396
|
-
* {@link siblingNamesFrom}
|
|
584
|
+
* The materialized spelling column: one judged path per distinct
|
|
585
|
+
* (walk root, target) pair.
|
|
586
|
+
*
|
|
587
|
+
* A *missing key* is never a legal input to judgement: see
|
|
588
|
+
* {@link pathSpellingFrom}.
|
|
397
589
|
*/
|
|
398
|
-
export
|
|
590
|
+
export type PathSpellingTable = ReadonlyMap<string, PathSpelling>;
|
|
591
|
+
/**
|
|
592
|
+
* Judge every request's whole path — the only place I/O is legal for this fact,
|
|
593
|
+
* and the pass that must run *before* any judging.
|
|
594
|
+
*
|
|
595
|
+
* Distinct (root, target) pairs are walked **concurrently**, and every listing
|
|
596
|
+
* they need goes through the cache's own {@link DirectorySpellingIndex}
|
|
597
|
+
* ({@link FsLookupCache.spellingIndex}), so a directory holding N referenced
|
|
598
|
+
* targets is listed once, not N times, a directory on the path to M of them is
|
|
599
|
+
* listed once, not M times, and a caller that fills once per path still indexes
|
|
600
|
+
* each directory only once for the whole run.
|
|
601
|
+
*
|
|
602
|
+
* @param requests - Targets to judge, each paired with its referring file
|
|
603
|
+
* @param fsCache - Per-run lookup cache (one instance per validation run)
|
|
604
|
+
* @returns The filled table; empty input yields an empty table with no syscalls
|
|
605
|
+
*/
|
|
606
|
+
export declare function fillPathSpellings(requests: Iterable<PathSpellingRequest>, fsCache: FsLookupCache): Promise<PathSpellingTable>;
|
|
607
|
+
/**
|
|
608
|
+
* Read the verdict for one reference out of an already-filled table. Pure.
|
|
609
|
+
*
|
|
610
|
+
* **A miss throws rather than degrading to `absent`.** The fill set is derived
|
|
611
|
+
* from exactly the references the judge will be asked about, so a missing row
|
|
612
|
+
* is a programming error — a path judged that nobody filled. Degrading would
|
|
613
|
+
* report every such reference as *missing*: a wrong answer wearing the shape of
|
|
614
|
+
* a graceful degradation, and one no test of the verdict would catch.
|
|
615
|
+
*
|
|
616
|
+
* @param table - Table filled by {@link fillPathSpellings}
|
|
617
|
+
* @param referrer - The file holding the reference
|
|
618
|
+
* @param target - The absolute path it resolved to
|
|
619
|
+
* @returns How faithfully the whole path is spelled
|
|
620
|
+
* @throws If `table` holds no row for this (referrer, target) pair
|
|
621
|
+
*/
|
|
622
|
+
export declare function pathSpellingFrom(table: PathSpellingTable, referrer: string, target: string): PathSpelling;
|
|
399
623
|
/**
|
|
400
624
|
* The materialized realpath column: path → its canonical path.
|
|
401
625
|
*
|
|
@@ -414,9 +638,9 @@ export type RealpathTable = ReadonlyMap<string, string>;
|
|
|
414
638
|
* ⚠️ **Rows are keyed by the input path string exactly as given** — not a
|
|
415
639
|
* dirname, not a re-resolved form. {@link realpathFrom} looks that same string
|
|
416
640
|
* up, so any normalization applied here and not there is a silent miss (a loud
|
|
417
|
-
* one, in fact: the judge throws). Contrast {@link
|
|
418
|
-
* by
|
|
419
|
-
* per path, so the path is the key.
|
|
641
|
+
* one, in fact: the judge throws). Contrast {@link fillPathSpellings}, which
|
|
642
|
+
* keys by (walk root, target) *because* many references share one walk; here the
|
|
643
|
+
* answer is per path, so the path is the key.
|
|
420
644
|
*
|
|
421
645
|
* Distinct paths are canonicalized **concurrently**: the shape this replaces
|
|
422
646
|
* asked one path at a time at judgement time, which serialised every `realpath`
|
|
@@ -440,13 +664,11 @@ export declare function fillRealpaths(paths: Iterable<string>, fsCache: FsLookup
|
|
|
440
664
|
* column exists to remove: a regression no test of the verdict could catch,
|
|
441
665
|
* because the verdict would be identical, only slower.
|
|
442
666
|
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
* the answer — so this lookup is itself the judge for this column, and there is
|
|
446
|
-
* nothing left to keep internal.
|
|
667
|
+
* The row IS the answer here — nothing further has to judge it — so this lookup
|
|
668
|
+
* is itself the judge for this column.
|
|
447
669
|
*
|
|
448
670
|
* **The signature is not what keeps this free of I/O — a test is.** As with
|
|
449
|
-
* {@link
|
|
671
|
+
* {@link pathSpellingFrom}, this module imports `node:fs` and
|
|
450
672
|
* `node:fs/promises` at module scope, so withholding a {@link FsLookupCache} from
|
|
451
673
|
* the parameter list prevents nothing. The guard is
|
|
452
674
|
* `packages/utils/test/fs-utils.test.ts` → *"judges from a filled table, reaching
|
|
@@ -460,21 +682,4 @@ export declare function fillRealpaths(paths: Iterable<string>, fsCache: FsLookup
|
|
|
460
682
|
* @throws If `table` holds no row for `filePath`
|
|
461
683
|
*/
|
|
462
684
|
export declare function realpathFrom(table: RealpathTable, filePath: string): string;
|
|
463
|
-
/**
|
|
464
|
-
* Whether `error` is the filesystem refusing a path rather than a bug.
|
|
465
|
-
*
|
|
466
|
-
* Deliberately NOT `error instanceof Error`: the point of every caller is to
|
|
467
|
-
* degrade on a hostile tree, and a `TypeError` from a validator is not that.
|
|
468
|
-
* Treating one as environmental turns a real defect into a warning about
|
|
469
|
-
* whichever file it happened on — which makes a tool quietest exactly when it is
|
|
470
|
-
* most wrong.
|
|
471
|
-
*
|
|
472
|
-
* Walks `cause`, because the errno is routinely re-wrapped on its way up. The CLI
|
|
473
|
-
* config loader turns a read failure into `new Error('Failed to load config: …')`;
|
|
474
|
-
* without following the chain the predicate answered "not a filesystem error" for
|
|
475
|
-
* a plain `EACCES`, and an unreadable config aborted a whole `vat audit` run. Any
|
|
476
|
-
* layer that adds context to an OS error defeats a `code`-only check, so the check
|
|
477
|
-
* cannot be `code`-only.
|
|
478
|
-
*/
|
|
479
|
-
export declare function isFilesystemAccessError(error: unknown): boolean;
|
|
480
685
|
//# sourceMappingURL=fs-utils.d.ts.map
|