@trackunit/repo-checks 0.0.0-stage → 0.0.3

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 (46) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE.txt +191 -0
  3. package/README.md +319 -2
  4. package/bin/index.d.ts +2 -0
  5. package/bin/index.js +34 -0
  6. package/migrations/entry.js +3 -0
  7. package/migrations.json +3 -0
  8. package/package.json +34 -3
  9. package/src/cli.d.ts +27 -0
  10. package/src/cli.js +109 -0
  11. package/src/index.d.ts +8 -0
  12. package/src/index.js +21 -0
  13. package/src/lib/config-error.d.ts +8 -0
  14. package/src/lib/config-error.js +16 -0
  15. package/src/lib/config.d.ts +22 -0
  16. package/src/lib/config.js +61 -0
  17. package/src/lib/doc-references/case-fix.d.ts +21 -0
  18. package/src/lib/doc-references/case-fix.js +66 -0
  19. package/src/lib/doc-references/check-doc-references.d.ts +28 -0
  20. package/src/lib/doc-references/check-doc-references.js +147 -0
  21. package/src/lib/doc-references/classify.d.ts +49 -0
  22. package/src/lib/doc-references/classify.js +59 -0
  23. package/src/lib/doc-references/discover.d.ts +6 -0
  24. package/src/lib/doc-references/discover.js +84 -0
  25. package/src/lib/doc-references/extract.d.ts +29 -0
  26. package/src/lib/doc-references/extract.js +46 -0
  27. package/src/lib/doc-references/finding.d.ts +24 -0
  28. package/src/lib/doc-references/finding.js +11 -0
  29. package/src/lib/doc-references/fs-errors.d.ts +14 -0
  30. package/src/lib/doc-references/fs-errors.js +25 -0
  31. package/src/lib/doc-references/headings.d.ts +79 -0
  32. package/src/lib/doc-references/headings.js +219 -0
  33. package/src/lib/doc-references/parse.d.ts +72 -0
  34. package/src/lib/doc-references/parse.js +72 -0
  35. package/src/lib/doc-references/placeholders.d.ts +74 -0
  36. package/src/lib/doc-references/placeholders.js +176 -0
  37. package/src/lib/doc-references/repo-root.d.ts +18 -0
  38. package/src/lib/doc-references/repo-root.js +36 -0
  39. package/src/lib/doc-references/resolver.d.ts +96 -0
  40. package/src/lib/doc-references/resolver.js +430 -0
  41. package/src/lib/doc-references/settings.d.ts +163 -0
  42. package/src/lib/doc-references/settings.js +167 -0
  43. package/src/lib/doc-references/suppressions.d.ts +8 -0
  44. package/src/lib/doc-references/suppressions.js +24 -0
  45. package/src/lib/format.d.ts +13 -0
  46. package/src/lib/format.js +33 -0
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findRepoRoot = exports.findRepoRootFromDir = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ /**
8
+ * Find the repository root for a directory: `rootDir` when configured, otherwise `startDir` or its
9
+ * nearest ancestor containing a `.git` directory or a `.git` file (a git worktree or submodule).
10
+ * Returns `undefined` when no repository encloses `startDir`.
11
+ */
12
+ const findRepoRootFromDir = (startDir, { rootDir, cwd } = {}) => {
13
+ if (rootDir !== undefined) {
14
+ return path.resolve(cwd ?? process.cwd(), rootDir);
15
+ }
16
+ let directory = path.resolve(startDir);
17
+ for (;;) {
18
+ if (fs.existsSync(path.join(directory, ".git"))) {
19
+ return directory;
20
+ }
21
+ const parent = path.dirname(directory);
22
+ if (parent === directory) {
23
+ return undefined;
24
+ }
25
+ directory = parent;
26
+ }
27
+ };
28
+ exports.findRepoRootFromDir = findRepoRootFromDir;
29
+ /**
30
+ * Find the repository root for a checked document, as {@link findRepoRootFromDir} does from the
31
+ * document's directory. Walks up from the document rather than from `process.cwd()`, so checking
32
+ * from a subdirectory or a worktree resolves the same root.
33
+ */
34
+ const findRepoRoot = (documentPath, options = {}) => (0, exports.findRepoRootFromDir)(path.dirname(path.resolve(documentPath)), options);
35
+ exports.findRepoRoot = findRepoRoot;
36
+ //# sourceMappingURL=repo-root.js.map
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Case-exact resolution of markdown file references.
3
+ *
4
+ * Existence is decided by listing directories and comparing names exactly, never with
5
+ * `fs.existsSync`, which is case-insensitive on macOS. A reference that only matches
6
+ * case-insensitively is a `caseMismatch` on every host, so macOS and Linux CI agree on case.
7
+ *
8
+ * Existence is checked against the working tree, not the git index. A gitignored or uncommitted
9
+ * file counts as present, so a checkout that has one (e.g. generated by a setup script) can pass
10
+ * where a fresh CI checkout reports it missing, and a link to a new file passes before it is staged.
11
+ */
12
+ export type ReferenceKind = "url" | "inline";
13
+ export type Resolution = {
14
+ readonly kind: "exact";
15
+ readonly path: string;
16
+ /** Set when the reference has placeholders, so `path` is one arbitrary match of it. */
17
+ readonly placeholder?: true;
18
+ } | {
19
+ readonly kind: "caseMismatch";
20
+ readonly path: string;
21
+ readonly actualPath: string;
22
+ /** The directory the reference's path was resolved against: the document's directory or the repo root. */
23
+ readonly base: string;
24
+ /** A recased segment had several siblings matching it case-insensitively, so `actualPath` is one guess of several. */
25
+ readonly ambiguous: boolean;
26
+ /** The reference has placeholders, so `actualPath` is one instance of it rather than its corrected form. */
27
+ readonly placeholder: boolean;
28
+ } | {
29
+ readonly kind: "ignored";
30
+ } | {
31
+ readonly kind: "missing";
32
+ };
33
+ export interface ResolveInput {
34
+ /** The reference as written: a link/image/definition URL or an inline-code span. */
35
+ readonly reference: string;
36
+ /** URLs resolve against the document (or the repo root with a leading `/`); inline code also tries the repo root. */
37
+ readonly referenceKind: ReferenceKind;
38
+ readonly documentDir: string;
39
+ readonly repoRoot: string;
40
+ /** Picomatch globs matched against the repo-relative path. */
41
+ readonly ignore: ReadonlyArray<string>;
42
+ /** Placeholder conventions that turn the reference into a wildcard pattern (see `placeholders.ts`). */
43
+ readonly placeholders: ReadonlyArray<string>;
44
+ /** Directory listings one placeholder reference may make before it is skipped. */
45
+ readonly maxVisitedDirectories?: number;
46
+ }
47
+ /**
48
+ * A listing (or parsed file) read within this long of its mtime is never reused: a change in the
49
+ * same timestamp tick leaves the mtime unchanged. Covers the coarsest common granularity (FAT, 2s).
50
+ */
51
+ export declare const RACY_WINDOW_MS = 2000;
52
+ /**
53
+ * The on-disk casing of an absolute path, without following symlinks. Segments from the first one
54
+ * that doesn't exist, or whose parent can't be listed, onward are returned as given. Callers can be
55
+ * handed a differently cased path on case-insensitive filesystems; canonicalising the document's own
56
+ * path keeps its relative references from all reading as case mismatches.
57
+ */
58
+ export declare const canonicalCase: (absolutePath: string) => string;
59
+ /** Whether a repo-relative path (either separator) matches any of the ignore globs. */
60
+ export declare const isIgnoredPath: (repoRelativePath: string, ignore: ReadonlyArray<string>) => boolean;
61
+ export interface SplitReference {
62
+ /** The reference as written up to its suffix, still percent-encoded for URLs. */
63
+ readonly pathPart: string;
64
+ /** Everything from the first `?` (URLs only) or `#` on, e.g. `?plain=1#L3`; `""` when there is none. */
65
+ readonly suffix: string;
66
+ }
67
+ /**
68
+ * Split a reference into its path part and suffix. A URL's suffix starts at its `?query` or
69
+ * `#fragment`, whichever comes first. Inline code only drops a `#fragment`, since a `?` in a code
70
+ * span may be a literal part of the path.
71
+ */
72
+ export declare const splitReference: (reference: string, referenceKind: ReferenceKind) => SplitReference;
73
+ /** A reference's `#fragment` without the `#`, still percent-encoded for URLs; `""` when there is none. */
74
+ export declare const referenceFragment: (reference: string) => string;
75
+ /** Whether a reference target ends in a file extension. One ending in `/` never does. */
76
+ export declare const hasFileExtension: (target: string) => boolean;
77
+ /** The path a reference names: its path part (see `splitReference`), percent-decoded for URLs. */
78
+ export declare const referenceTarget: (reference: string, referenceKind: ReferenceKind) => string;
79
+ /** Hard cap on directory listings per placeholder reference; hitting it skips the reference. */
80
+ export declare const MAX_VISITED_DIRECTORIES = 10000;
81
+ /**
82
+ * Resolve a reference to `exact`, `caseMismatch`, `ignored`, or `missing`. Candidates inside an
83
+ * ignored location are never looked up. Across candidates, an exact match wins, then an ignored
84
+ * candidate, then the first case mismatch. A reference with placeholders (under
85
+ * `input.placeholders`) matches wildcards against the tree; see `lookupWildcard`. An illustrative
86
+ * `path/to/` reference, or one with adjacent placeholders, is `ignored` without any lookup.
87
+ *
88
+ * The reference as written says what it names: a trailing `/` must resolve to a directory, a file
89
+ * extension to a file (so a directory named `foo.md` is `missing`), and anything else to either. An
90
+ * entry of the wrong kind is `missing`, even when its name differs only in case.
91
+ *
92
+ * Only the repository is looked at: a candidate outside `repoRoot` is `missing` whether or not it
93
+ * exists on this machine, and a leading `..` in a root-relative reference stays at the root, as on
94
+ * GitHub (`/../x` is `/x`).
95
+ */
96
+ export declare const resolveReference: (input: ResolveInput) => Resolution;
@@ -0,0 +1,430 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveReference = exports.MAX_VISITED_DIRECTORIES = exports.referenceTarget = exports.hasFileExtension = exports.referenceFragment = exports.splitReference = exports.isIgnoredPath = exports.canonicalCase = exports.RACY_WINDOW_MS = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ const picomatch_1 = tslib_1.__importDefault(require("picomatch"));
8
+ const fs_errors_1 = require("./fs-errors");
9
+ const placeholders_1 = require("./placeholders");
10
+ /** Per-process listing cache, so each directory is listed once per run; revalidated by directory mtime. */
11
+ const listingCache = new Map();
12
+ /**
13
+ * A listing (or parsed file) read within this long of its mtime is never reused: a change in the
14
+ * same timestamp tick leaves the mtime unchanged. Covers the coarsest common granularity (FAT, 2s).
15
+ */
16
+ exports.RACY_WINDOW_MS = 2000;
17
+ /**
18
+ * The listing of `directory`, or `undefined` when it doesn't exist or isn't a directory.
19
+ *
20
+ * @throws {ReadError} when `directory` exists but can't be stat'ed or listed
21
+ */
22
+ const listDirectory = (directory) => {
23
+ let mtimeMs;
24
+ try {
25
+ const stats = fs.statSync(directory);
26
+ if (!stats.isDirectory()) {
27
+ listingCache.delete(directory);
28
+ return undefined;
29
+ }
30
+ mtimeMs = stats.mtimeMs;
31
+ }
32
+ catch (error) {
33
+ listingCache.delete(directory);
34
+ if ((0, fs_errors_1.isNotFoundError)(error))
35
+ return undefined;
36
+ throw new fs_errors_1.ReadError(`Cannot read directory ${directory}: ${(0, fs_errors_1.errorMessage)(error)}`);
37
+ }
38
+ const cached = listingCache.get(directory);
39
+ if (cached?.mtimeMs === mtimeMs && cached.listedAtMs - mtimeMs > exports.RACY_WINDOW_MS) {
40
+ return cached;
41
+ }
42
+ try {
43
+ const listedAtMs = Date.now();
44
+ const names = fs.readdirSync(directory);
45
+ const byLowerCase = new Map();
46
+ const ambiguousLowerCase = new Set();
47
+ for (const name of names) {
48
+ const lowerCase = name.toLowerCase();
49
+ if (byLowerCase.has(lowerCase)) {
50
+ ambiguousLowerCase.add(lowerCase);
51
+ }
52
+ else {
53
+ byLowerCase.set(lowerCase, name);
54
+ }
55
+ }
56
+ const listing = {
57
+ mtimeMs,
58
+ listedAtMs,
59
+ names: new Set(names),
60
+ sortedNames: [...names].sort(),
61
+ byLowerCase,
62
+ ambiguousLowerCase,
63
+ };
64
+ listingCache.set(directory, listing);
65
+ return listing;
66
+ }
67
+ catch (error) {
68
+ listingCache.delete(directory);
69
+ if ((0, fs_errors_1.isNotFoundError)(error))
70
+ return undefined;
71
+ throw new fs_errors_1.ReadError(`Cannot read directory ${directory}: ${(0, fs_errors_1.errorMessage)(error)}`);
72
+ }
73
+ };
74
+ /** The listed entry named `segment`, exactly if possible, otherwise case-insensitively. */
75
+ const findListedEntry = (listing, segment) => {
76
+ if (listing.names.has(segment)) {
77
+ return { name: segment, ambiguous: false };
78
+ }
79
+ const lowerCase = segment.toLowerCase();
80
+ const name = listing.byLowerCase.get(lowerCase);
81
+ return name === undefined ? undefined : { name, ambiguous: listing.ambiguousLowerCase.has(lowerCase) };
82
+ };
83
+ const findEntry = (directory, segment) => {
84
+ const listing = listDirectory(directory);
85
+ return listing && findListedEntry(listing, segment);
86
+ };
87
+ const isWithin = (absolutePath, directory) => absolutePath === directory || absolutePath.startsWith(directory + path.sep);
88
+ const segmentsBelow = (base, absolutePath) => path
89
+ .relative(base, absolutePath)
90
+ .split(path.sep)
91
+ .filter(segment => segment !== "");
92
+ /**
93
+ * What a reference's target may name: a trailing `/` needs a directory, a file extension a file,
94
+ * and anything else either. A stemless dot-name (`.github`, `.gitignore`) has no extension here,
95
+ * since it is as often a directory as a file. A multi-dot name (`apps/socket.io`) does, so it must
96
+ * name a file.
97
+ */
98
+ const expectedKind = (target) => {
99
+ if (target.endsWith("/"))
100
+ return "directory";
101
+ const lastSegment = target.slice(target.lastIndexOf("/") + 1);
102
+ if (/^\.[^.]+$/.test(lastSegment))
103
+ return "any";
104
+ return (0, exports.hasFileExtension)(target) ? "file" : "any";
105
+ };
106
+ /**
107
+ * Whether `absolutePath` exists, following symlinks, and is of the `expected` kind.
108
+ *
109
+ * @throws {ReadError} when `absolutePath` exists but can't be stat'ed
110
+ */
111
+ const isExpectedKind = (absolutePath, expected) => {
112
+ let stats;
113
+ try {
114
+ stats = fs.statSync(absolutePath);
115
+ }
116
+ catch (error) {
117
+ if ((0, fs_errors_1.isNotFoundError)(error))
118
+ return false; // e.g. a dangling symlink, which a directory listing still names
119
+ throw new fs_errors_1.ReadError(`Cannot read ${absolutePath}: ${(0, fs_errors_1.errorMessage)(error)}`);
120
+ }
121
+ return expected === "any" || stats.isDirectory() === (expected === "directory");
122
+ };
123
+ /**
124
+ * Walk `absolutePath` segment by segment below `trustedBase` (whose casing the caller has already
125
+ * canonicalised), comparing each name exactly against its parent's listing. The entry it ends on
126
+ * must be of the `expected` kind, or the reference is `missing` whatever its casing.
127
+ */
128
+ const lookupCaseExact = (absolutePath, trustedBase, expected) => {
129
+ const start = isWithin(absolutePath, trustedBase) ? trustedBase : path.parse(absolutePath).root;
130
+ let actualPath = start;
131
+ let caseDiffers = false;
132
+ let ambiguous = false;
133
+ for (const segment of segmentsBelow(start, absolutePath)) {
134
+ const entry = findEntry(actualPath, segment);
135
+ if (entry === undefined) {
136
+ return { kind: "missing" };
137
+ }
138
+ caseDiffers || (caseDiffers = entry.name !== segment);
139
+ ambiguous || (ambiguous = entry.ambiguous);
140
+ actualPath = path.join(actualPath, entry.name);
141
+ }
142
+ if (!isExpectedKind(actualPath, expected)) {
143
+ return { kind: "missing" };
144
+ }
145
+ return caseDiffers ? { kind: "caseMismatch", actualPath, ambiguous } : { kind: "exact", path: absolutePath };
146
+ };
147
+ /**
148
+ * The on-disk casing of an absolute path, without following symlinks. Segments from the first one
149
+ * that doesn't exist, or whose parent can't be listed, onward are returned as given. Callers can be
150
+ * handed a differently cased path on case-insensitive filesystems; canonicalising the document's own
151
+ * path keeps its relative references from all reading as case mismatches.
152
+ */
153
+ const canonicalCase = (absolutePath) => {
154
+ const root = path.parse(absolutePath).root;
155
+ const segments = segmentsBelow(root, absolutePath);
156
+ let actualPath = root;
157
+ for (const [index, segment] of segments.entries()) {
158
+ let entry;
159
+ try {
160
+ entry = findEntry(actualPath, segment);
161
+ }
162
+ catch {
163
+ // Ancestors outside the repository may be searchable but not listable (e.g. a `--x` home directory).
164
+ entry = undefined;
165
+ }
166
+ if (entry === undefined) {
167
+ return path.join(actualPath, ...segments.slice(index));
168
+ }
169
+ actualPath = path.join(actualPath, entry.name);
170
+ }
171
+ return actualPath;
172
+ };
173
+ exports.canonicalCase = canonicalCase;
174
+ const compiledIgnores = new WeakMap();
175
+ /** Whether a repo-relative path (either separator) matches any of the ignore globs. */
176
+ const isIgnoredPath = (repoRelativePath, ignore) => {
177
+ let matchers = compiledIgnores.get(ignore);
178
+ if (!matchers) {
179
+ matchers = ignore.map(glob => (0, picomatch_1.default)(glob, { dot: true }));
180
+ compiledIgnores.set(ignore, matchers);
181
+ }
182
+ const normalized = repoRelativePath.split(path.sep).join("/");
183
+ return matchers.some(matcher => matcher(normalized));
184
+ };
185
+ exports.isIgnoredPath = isIgnoredPath;
186
+ /**
187
+ * Split a reference into its path part and suffix. A URL's suffix starts at its `?query` or
188
+ * `#fragment`, whichever comes first. Inline code only drops a `#fragment`, since a `?` in a code
189
+ * span may be a literal part of the path.
190
+ */
191
+ const splitReference = (reference, referenceKind) => {
192
+ const index = reference.search(referenceKind === "url" ? /[?#]/ : /#/);
193
+ return index === -1
194
+ ? { pathPart: reference, suffix: "" }
195
+ : { pathPart: reference.slice(0, index), suffix: reference.slice(index) };
196
+ };
197
+ exports.splitReference = splitReference;
198
+ /** A reference's `#fragment` without the `#`, still percent-encoded for URLs; `""` when there is none. */
199
+ const referenceFragment = (reference) => {
200
+ const index = reference.indexOf("#");
201
+ return index === -1 ? "" : reference.slice(index + 1);
202
+ };
203
+ exports.referenceFragment = referenceFragment;
204
+ const EXTENSION_RE = /\.[A-Za-z0-9]{1,8}$/;
205
+ /** Whether a reference target ends in a file extension. One ending in `/` never does. */
206
+ const hasFileExtension = (target) => EXTENSION_RE.test(target);
207
+ exports.hasFileExtension = hasFileExtension;
208
+ /** mdast keeps `url` percent-encoded. A malformed escape is checked as written. */
209
+ const decodeUrlPath = (value) => {
210
+ try {
211
+ return decodeURIComponent(value);
212
+ }
213
+ catch {
214
+ return value;
215
+ }
216
+ };
217
+ /** The path a reference names: its path part (see `splitReference`), percent-decoded for URLs. */
218
+ const referenceTarget = (reference, referenceKind) => {
219
+ const { pathPart } = (0, exports.splitReference)(reference, referenceKind);
220
+ return referenceKind === "url" ? decodeUrlPath(pathPart) : pathPart;
221
+ };
222
+ exports.referenceTarget = referenceTarget;
223
+ const candidatePaths = ({ referenceKind, documentDir, repoRoot }, target) => {
224
+ if (target.startsWith("/")) {
225
+ return [{ base: repoRoot, path: path.resolve(repoRoot, target.replace(/^\/+/, "")) }];
226
+ }
227
+ const relativeToDocument = { base: documentDir, path: path.resolve(documentDir, target) };
228
+ if (referenceKind === "url") {
229
+ return [relativeToDocument];
230
+ }
231
+ const relativeToRoot = { base: repoRoot, path: path.resolve(repoRoot, target) };
232
+ return relativeToRoot.path === relativeToDocument.path ? [relativeToDocument] : [relativeToDocument, relativeToRoot];
233
+ };
234
+ /** Hard cap on directory listings per placeholder reference; hitting it skips the reference. */
235
+ exports.MAX_VISITED_DIRECTORIES = 10000;
236
+ const compiledDirectoryIgnores = new WeakMap();
237
+ /**
238
+ * Whether every descendant of a directory is ignored, so a wildcard walk must not list it. That
239
+ * holds exactly when some glob is `P/**` and the directory matches `P`: `**\/dist/**` prunes
240
+ * `libs/dist`, while `dist/*` or `**\/*.zip` never prune anything.
241
+ */
242
+ const isIgnoredDirectory = (directory, { repoRoot, ignore }) => {
243
+ let matchers = compiledDirectoryIgnores.get(ignore);
244
+ if (!matchers) {
245
+ matchers = ignore.flatMap(glob => {
246
+ const prefix = glob.endsWith("/**") ? glob.slice(0, -"/**".length) : "";
247
+ return prefix === "" || prefix.startsWith("!") ? [] : [(0, picomatch_1.default)(prefix, { dot: true })];
248
+ });
249
+ compiledDirectoryIgnores.set(ignore, matchers);
250
+ }
251
+ const normalized = path.relative(repoRoot, directory).split(path.sep).join("/");
252
+ return normalized !== "" && matchers.some(matcher => matcher(normalized));
253
+ };
254
+ /**
255
+ * Whether a resolved path is ignored: it matches an ignore glob, or the reference may name a
256
+ * directory and this one is ignored entirely, so `dist/` counts as ignored generated output even
257
+ * before a build creates it.
258
+ */
259
+ const isIgnoredTarget = (absolutePath, expected, input) => (0, exports.isIgnoredPath)(path.relative(input.repoRoot, absolutePath), input.ignore) ||
260
+ (expected !== "file" && isIgnoredDirectory(absolutePath, input));
261
+ const lookupLiteral = (candidate, expected, input) => isIgnoredTarget(candidate.path, expected, input)
262
+ ? { kind: "ignored" }
263
+ : lookupCaseExact(candidate.path, input.repoRoot, expected);
264
+ const CASE_EXACT = { caseDiffers: false, ambiguous: false };
265
+ /** List `directory` for the walk, unless it is ignored or the budget is spent. */
266
+ const visit = ({ input, budget }, directory) => {
267
+ if (budget.exhausted || isIgnoredDirectory(directory, input))
268
+ return undefined;
269
+ if (budget.remaining <= 0) {
270
+ budget.exhausted = true;
271
+ return undefined;
272
+ }
273
+ const listing = listDirectory(directory);
274
+ if (listing)
275
+ budget.remaining--;
276
+ return listing;
277
+ };
278
+ /** Entry names a wildcard step may take; `.git` is never entered. */
279
+ const wildcardNames = (listing) => listing.sortedNames.filter(name => name !== ".git");
280
+ /**
281
+ * Depth-first, backtracking walk of `walk.steps` from `directory` (already matching the first
282
+ * `index` steps).
283
+ *
284
+ * @yields every existing, non-ignored match of the expected kind, in sorted entry-name order
285
+ */
286
+ function* walkMatches(walk, directory, index, bindings, matchCase) {
287
+ const step = walk.steps[index];
288
+ if (step === undefined) {
289
+ if (isExpectedKind(directory, walk.expected) && !isIgnoredTarget(directory, walk.expected, walk.input)) {
290
+ yield { ...matchCase, path: directory };
291
+ }
292
+ return;
293
+ }
294
+ const listing = visit(walk, directory);
295
+ if (!listing)
296
+ return;
297
+ if (step.kind === "literal") {
298
+ const entry = findListedEntry(listing, step.value);
299
+ if (entry) {
300
+ yield* walkMatches(walk, path.join(directory, entry.name), index + 1, bindings, {
301
+ caseDiffers: matchCase.caseDiffers || entry.name !== step.value,
302
+ ambiguous: matchCase.ambiguous || entry.ambiguous,
303
+ });
304
+ }
305
+ }
306
+ else {
307
+ const matchName = (0, placeholders_1.createSegmentMatcher)(step, bindings);
308
+ for (const name of wildcardNames(listing)) {
309
+ const bound = matchName(name);
310
+ if (bound)
311
+ yield* walkMatches(walk, path.join(directory, name), index + 1, bound, matchCase);
312
+ }
313
+ }
314
+ }
315
+ /**
316
+ * The walk's steps: the candidate base's segments as literals, then the compiled reference, with
317
+ * `.`, `..`, and empty segments applied lexically exactly as `path.resolve` would.
318
+ */
319
+ const wildcardSteps = (base, compiled) => {
320
+ const steps = segmentsBelow(path.parse(base).root, base).map(value => ({
321
+ kind: "literal",
322
+ value,
323
+ }));
324
+ for (const segment of compiled.segments) {
325
+ if (segment.kind === "literal" && (segment.value === "" || segment.value === "."))
326
+ continue;
327
+ if (segment.kind === "literal" && segment.value === "..") {
328
+ steps.pop();
329
+ continue;
330
+ }
331
+ steps.push(segment);
332
+ }
333
+ return steps;
334
+ };
335
+ /**
336
+ * Resolve a placeholder reference against one candidate base. A reference whose path as written
337
+ * matches an ignore glob (`{name}/app.zip`) is `ignored`, and so is one whose literal prefix before
338
+ * the first placeholder is an ignored location, as a literal reference there would be. Beyond it the walk never lists an ignored directory or `.git`,
339
+ * so matches only there count as none. The first case-exact match is `exact`; otherwise a match
340
+ * whose literal steps differ only in case is a `caseMismatch`. A match must be of the `expected`
341
+ * kind, exactly as for a literal reference. Hitting the visit cap is `ignored`.
342
+ */
343
+ const lookupWildcard = (candidate, compiled, { expected, budget }, input) => {
344
+ if (isIgnoredTarget(candidate.path, expected, input))
345
+ return { kind: "ignored" };
346
+ const steps = wildcardSteps(candidate.base, compiled);
347
+ const root = path.parse(candidate.base).root;
348
+ const literalCount = steps.findIndex(step => step.kind !== "literal");
349
+ if (literalCount === -1)
350
+ return lookupLiteral(candidate, expected, input);
351
+ const prefixSegments = steps.slice(0, literalCount).flatMap(step => (step.kind === "literal" ? [step.value] : []));
352
+ const prefix = path.join(root, ...prefixSegments);
353
+ if (isIgnoredDirectory(prefix, input))
354
+ return { kind: "ignored" };
355
+ const start = isWithin(prefix, input.repoRoot) ? input.repoRoot : root;
356
+ const walk = { input, steps, expected, budget };
357
+ const matches = walkMatches(walk, start, segmentsBelow(root, start).length, new Map(), CASE_EXACT);
358
+ let caseMismatch;
359
+ for (const match of matches) {
360
+ if (!match.caseDiffers)
361
+ return { kind: "exact", path: match.path };
362
+ caseMismatch ?? (caseMismatch = match);
363
+ }
364
+ if (budget.exhausted)
365
+ return { kind: "ignored" };
366
+ return caseMismatch
367
+ ? { kind: "caseMismatch", actualPath: caseMismatch.path, ambiguous: caseMismatch.ambiguous }
368
+ : { kind: "missing" };
369
+ };
370
+ /**
371
+ * Resolve a reference to `exact`, `caseMismatch`, `ignored`, or `missing`. Candidates inside an
372
+ * ignored location are never looked up. Across candidates, an exact match wins, then an ignored
373
+ * candidate, then the first case mismatch. A reference with placeholders (under
374
+ * `input.placeholders`) matches wildcards against the tree; see `lookupWildcard`. An illustrative
375
+ * `path/to/` reference, or one with adjacent placeholders, is `ignored` without any lookup.
376
+ *
377
+ * The reference as written says what it names: a trailing `/` must resolve to a directory, a file
378
+ * extension to a file (so a directory named `foo.md` is `missing`), and anything else to either. An
379
+ * entry of the wrong kind is `missing`, even when its name differs only in case.
380
+ *
381
+ * Only the repository is looked at: a candidate outside `repoRoot` is `missing` whether or not it
382
+ * exists on this machine, and a leading `..` in a root-relative reference stays at the root, as on
383
+ * GitHub (`/../x` is `/x`).
384
+ */
385
+ const resolveReference = (input) => {
386
+ const written = (0, exports.referenceTarget)(input.reference, input.referenceKind);
387
+ const target = written.startsWith("/") ? path.posix.normalize(written) : written;
388
+ const pathPart = target.replace(/^\/+/, "");
389
+ const expected = expectedKind(written);
390
+ if ((0, placeholders_1.isIllustrativeReference)(pathPart, input.placeholders)) {
391
+ return { kind: "ignored" };
392
+ }
393
+ const compiled = (0, placeholders_1.compileReference)(pathPart, input.placeholders);
394
+ if (compiled && (0, placeholders_1.hasAdjacentSlots)(compiled)) {
395
+ return { kind: "ignored" };
396
+ }
397
+ const budget = { remaining: input.maxVisitedDirectories ?? exports.MAX_VISITED_DIRECTORIES, exhausted: false };
398
+ let ignored = false;
399
+ let caseMismatch;
400
+ for (const candidate of candidatePaths(input, target)) {
401
+ if (!isWithin(candidate.path, input.repoRoot))
402
+ continue;
403
+ const lookup = compiled
404
+ ? lookupWildcard(candidate, compiled, { expected, budget }, input)
405
+ : lookupLiteral(candidate, expected, input);
406
+ if (lookup.kind === "ignored") {
407
+ ignored = true;
408
+ continue;
409
+ }
410
+ if (lookup.kind === "exact") {
411
+ return compiled ? { kind: "exact", path: lookup.path, placeholder: true } : { kind: "exact", path: lookup.path };
412
+ }
413
+ if (lookup.kind === "caseMismatch" && !caseMismatch) {
414
+ caseMismatch = {
415
+ kind: "caseMismatch",
416
+ path: candidate.path,
417
+ actualPath: lookup.actualPath,
418
+ base: candidate.base,
419
+ ambiguous: lookup.ambiguous,
420
+ placeholder: compiled !== undefined,
421
+ };
422
+ }
423
+ }
424
+ if (ignored) {
425
+ return { kind: "ignored" };
426
+ }
427
+ return caseMismatch ?? { kind: "missing" };
428
+ };
429
+ exports.resolveReference = resolveReference;
430
+ //# sourceMappingURL=resolver.js.map