@descent-vtt/spec-graph 0.7.0 → 0.9.0

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 (106) hide show
  1. package/CHANGELOG.md +477 -0
  2. package/README.md +109 -17
  3. package/dist/cli.d.ts +2 -2
  4. package/dist/cli.js +38 -19
  5. package/dist/cli.js.map +1 -1
  6. package/dist/directives.d.ts +9 -2
  7. package/dist/directives.js +15 -4
  8. package/dist/directives.js.map +1 -1
  9. package/dist/extract.d.ts +1 -1
  10. package/dist/extract.js +50 -16
  11. package/dist/extract.js.map +1 -1
  12. package/dist/glob.d.ts +44 -44
  13. package/dist/glob.js +152 -116
  14. package/dist/glob.js.map +1 -1
  15. package/dist/identity.d.ts +12 -4
  16. package/dist/identity.js +32 -8
  17. package/dist/identity.js.map +1 -1
  18. package/dist/index.d.ts +3 -3
  19. package/dist/index.js +4 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/lifecycle.js +10 -2
  22. package/dist/lifecycle.js.map +1 -1
  23. package/dist/markdown.d.ts +53 -161
  24. package/dist/markdown.js +42 -864
  25. package/dist/markdown.js.map +1 -1
  26. package/dist/report.d.ts +23 -0
  27. package/dist/report.js +80 -0
  28. package/dist/report.js.map +1 -1
  29. package/dist/rules.js +48 -14
  30. package/dist/rules.js.map +1 -1
  31. package/dist/sections.d.ts +1 -1
  32. package/dist/sections.js +18 -4
  33. package/dist/sections.js.map +1 -1
  34. package/dist/select.d.ts +2 -2
  35. package/dist/select.js +3 -3
  36. package/dist/select.js.map +1 -1
  37. package/dist/source.d.ts +6 -19
  38. package/dist/source.js +4 -70
  39. package/dist/source.js.map +1 -1
  40. package/dist/types.d.ts +23 -3
  41. package/dist/types.js +1 -1
  42. package/dist/types.js.map +1 -1
  43. package/dist/vendor/spec-core/markdown/frontmatter.d.ts +103 -0
  44. package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
  45. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
  46. package/dist/vendor/spec-core/markdown/headings.d.ts +45 -0
  47. package/dist/vendor/spec-core/markdown/headings.js +199 -0
  48. package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
  49. package/dist/vendor/spec-core/markdown/index.d.ts +5 -0
  50. package/dist/vendor/spec-core/markdown/index.js +4 -0
  51. package/dist/vendor/spec-core/markdown/index.js.map +1 -0
  52. package/dist/vendor/spec-core/markdown/layout.d.ts +39 -0
  53. package/dist/vendor/spec-core/markdown/layout.js +28 -0
  54. package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
  55. package/dist/vendor/spec-core/markdown/links.d.ts +22 -0
  56. package/dist/vendor/spec-core/markdown/links.js +364 -0
  57. package/dist/vendor/spec-core/markdown/links.js.map +1 -0
  58. package/dist/vendor/spec-core/markdown/lists.d.ts +22 -0
  59. package/dist/vendor/spec-core/markdown/lists.js +127 -0
  60. package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
  61. package/dist/vendor/spec-core/markdown/scan.d.ts +36 -0
  62. package/dist/vendor/spec-core/markdown/scan.js +439 -0
  63. package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
  64. package/dist/vendor/spec-core/markdown/syntax.d.ts +75 -0
  65. package/dist/vendor/spec-core/markdown/syntax.js +181 -0
  66. package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
  67. package/dist/vendor/spec-core/markdown/tables.d.ts +18 -0
  68. package/dist/vendor/spec-core/markdown/tables.js +86 -0
  69. package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
  70. package/dist/vendor/spec-core/markdown/types.d.ts +287 -0
  71. package/dist/vendor/spec-core/markdown/types.js +9 -0
  72. package/dist/vendor/spec-core/markdown/types.js.map +1 -0
  73. package/dist/vendor/spec-core/path/index.d.ts +1 -0
  74. package/dist/vendor/spec-core/path/index.js +2 -0
  75. package/dist/vendor/spec-core/path/index.js.map +1 -0
  76. package/dist/vendor/spec-core/path/posix.d.ts +83 -0
  77. package/dist/vendor/spec-core/path/posix.js +150 -0
  78. package/dist/vendor/spec-core/path/posix.js.map +1 -0
  79. package/dist/vendor/spec-core/pattern/automaton.d.ts +129 -0
  80. package/dist/vendor/spec-core/pattern/automaton.js +340 -0
  81. package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
  82. package/dist/vendor/spec-core/pattern/charset.d.ts +58 -0
  83. package/dist/vendor/spec-core/pattern/charset.js +116 -0
  84. package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
  85. package/dist/vendor/spec-core/pattern/glob.d.ts +121 -0
  86. package/dist/vendor/spec-core/pattern/glob.js +481 -0
  87. package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
  88. package/dist/vendor/spec-core/pattern/index.d.ts +6 -0
  89. package/dist/vendor/spec-core/pattern/index.js +4 -0
  90. package/dist/vendor/spec-core/pattern/index.js.map +1 -0
  91. package/dist/{regex.d.ts → vendor/spec-core/pattern/regex.d.ts} +20 -29
  92. package/dist/{regex.js → vendor/spec-core/pattern/regex.js} +40 -49
  93. package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
  94. package/dist/vendor/spec-core/text/index.d.ts +2 -0
  95. package/dist/vendor/spec-core/text/index.js +2 -0
  96. package/dist/vendor/spec-core/text/index.js.map +1 -0
  97. package/dist/vendor/spec-core/text/lines.d.ts +72 -0
  98. package/dist/vendor/spec-core/text/lines.js +156 -0
  99. package/dist/vendor/spec-core/text/lines.js.map +1 -0
  100. package/dist/yaml.d.ts +44 -12
  101. package/dist/yaml.js +115 -206
  102. package/dist/yaml.js.map +1 -1
  103. package/package.json +3 -2
  104. package/src/vendor/spec-core/LICENSE +21 -0
  105. package/src/vendor/spec-core/README.md +8 -0
  106. package/dist/regex.js.map +0 -1
package/dist/glob.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Pattern matching and directory walking.
3
3
  *
4
- * A dependency-free glob, because pulling in a matcher for `docs/**\/*.md` would
5
- * be the single largest thing in the package. The supported syntax is the
6
- * portion people actually type - `**`, `*`, `?`, `{a,b}`, `[abc]`, and a leading
7
- * `!` for negation - and everything is matched against repository-relative
8
- * POSIX paths so results do not depend on the host platform.
4
+ * The patterns are the `path` dialect every spec-* tool reads, from spec-core:
5
+ * `**`, `*`, `?`, `{a,b}`, `[abc]` and `[!a-z]`, matched against whole
6
+ * repository-relative POSIX paths by an automaton that cannot backtrack, with
7
+ * case respected on every host (ADR-0022). What stays here is spec-graph's own:
8
+ * how a list with `!` entries in it reads, which directories a walk never
9
+ * enters, and how a reference target - which is not a path - is matched.
9
10
  */
10
- import { type Matcher } from './regex.js';
11
+ import { type RegexMatcher } from './vendor/spec-core/pattern/index.js';
11
12
  /** Directories skipped unless a pattern explicitly names them. */
12
13
  export declare const DEFAULT_IGNORED_DIRECTORIES: readonly string[];
13
14
  /** Files larger than this are skipped: a specification is not a binary. */
@@ -30,41 +31,32 @@ export interface WalkOptions {
30
31
  /** True when the string contains glob syntax rather than being a literal path. */
31
32
  export declare function isGlob(pattern: string): boolean;
32
33
  /**
33
- * Compiles a glob to an anchored regular expression.
34
+ * Compiles a glob to an anchored regular expression, in the dialect spec-graph
35
+ * read before it adopted spec-core's.
34
36
  *
35
- * `**` crosses directory separators; `*` and `?` do not. A trailing `/` or a
36
- * bare directory name matches everything beneath it, which is what people mean
37
- * when they write `--ignore drafts`.
38
- *
39
- * Nothing in the pipeline matches with this any more - see {@link compileGlob},
40
- * which gives the same answers without the backtracking. It stays for callers
41
- * who want a `RegExp`, and as the oracle the tests hold the automaton to.
37
+ * @deprecated Nothing matches with this. It stays for callers who want a
38
+ * `RegExp`, and as the record of the old reading that `tests/glob.test.ts`
39
+ * holds every difference against, each one named in ADR-0022. It no longer
40
+ * folds case on Windows alone: an answer must not depend on the host.
42
41
  */
43
42
  export declare function globToRegExp(pattern: string): RegExp;
44
43
  export interface GlobOptions {
45
- /** Fold case. Defaults to the host's convention: on for Windows, off elsewhere. */
44
+ /** Fold case. Off unless asked for, on every host. */
46
45
  readonly ignoreCase?: boolean | undefined;
47
46
  }
48
47
  /**
49
- * Compiles a glob to the automaton every matcher here uses.
50
- *
51
- * A glob has no nested quantifiers, and ADR-0017 took that to mean there was no
52
- * backtracking hazard in handing one to `RegExp`. Nobody had timed it. Each
53
- * `*` is a `[^/]*`, and against a subject that fails to match, a backtracking
54
- * engine tries every way of dividing it between them - the subject's length to
55
- * the power of the stars:
48
+ * Compiles one glob to a matcher over whole repository-relative paths.
56
49
  *
57
- * ```text
58
- * RegExp here
59
- * **\/*-*-*-*.md a 643-character hyphenated file name 2.3s 0.2ms
60
- * *-*-*-x a 10,000-character reference target 120s 0.9ms
61
- * ```
50
+ * A pattern with no glob syntax names that path and nothing beneath it; a bare
51
+ * directory meaning its contents is a reading of {@link createGlobMatcher}'s.
52
+ * Throws a `GlobError` that names the glob as it was written.
62
53
  *
63
- * The first is bounded by the filesystem, which caps a name at 255 bytes. The
64
- * second is not: `--ignore-ref` patterns are matched against targets read out
65
- * of documents, and a document is whatever somebody wrote.
54
+ * Matching is linear in the path whatever the pattern. It was not always: a
55
+ * glob's stars are not nested, but each is a `[^/]*` to `RegExp`, and a subject
56
+ * that fails is divided between them every way there is - `*-*-*-x` took two
57
+ * minutes over a 10,000-character reference target (ADR-0017).
66
58
  */
67
- export declare function compileGlob(pattern: string, options?: GlobOptions): Matcher;
59
+ export declare function compileGlob(pattern: string, options?: GlobOptions): RegexMatcher;
68
60
  export interface GlobMatcher {
69
61
  (path: string): boolean;
70
62
  }
@@ -72,29 +64,37 @@ export interface GlobMatcher {
72
64
  * Builds a matcher from include patterns, honouring `!` negations.
73
65
  *
74
66
  * Later patterns win, so `docs/**\/*.md` followed by `!docs/drafts/**` reads the
75
- * way a `.gitignore` does.
67
+ * way a `.gitignore` does. A pattern with no glob syntax in it names a file, or
68
+ * a directory and everything beneath it; `docs/` names only what is beneath.
76
69
  */
77
70
  export declare function createGlobMatcher(patterns: readonly string[]): GlobMatcher;
78
71
  /**
79
72
  * Builds a predicate over reference *targets*, not paths.
80
73
  *
81
74
  * Separate from {@link createGlobMatcher} on purpose. That one is
82
- * path-oriented: it expands a bare name to `name/**` because a directory means
83
- * everything under it. A reference target has no such structure - `trap 55` is
84
- * a name, not a location - so a bare pattern must match it literally and
85
- * nothing else.
75
+ * path-oriented: a bare name there is a directory and everything under it. A
76
+ * reference target has no such structure - `trap 55` is a name, not a location
77
+ * - so a bare pattern matches it literally and nothing else.
86
78
  *
87
- * Matching is case-insensitive on every platform. Path matching inherits the
88
- * host filesystem's case rules, which is right for paths and wrong here: a
89
- * repository's findings must not depend on which machine ran the check.
79
+ * Case is ignored, on every platform, because a citation's spelling is the
80
+ * author's and not the filesystem's. The fold is simple case mapping, one code
81
+ * point at a time and without a locale, so every host gives the same answer:
82
+ * U+00B5 (micro sign) and U+03BC (mu) stay two targets, as they have been since
83
+ * a Windows-only `i` flag made them one there (ADR-0017).
90
84
  *
91
- * Both sides are lower-cased, and that is the whole of the case rule. Folding
92
- * as well used to happen on Windows only, where it made U+00B5 (micro sign)
93
- * and U+03BC (mu) the same target - one machine's answer, which is what this
94
- * function exists not to give.
85
+ * A target is text a document holds rather than a path on this host, and two
86
+ * things follow. A `\` escapes the character after it: there is no Windows
87
+ * separator to allow for. And a `.` or `..` segment is text: in a path the
88
+ * dialect drops the one and refuses the other as climbing out of the root, but
89
+ * `../../notes/gone.md` is a link somebody wrote and may want left alone.
95
90
  */
96
91
  export declare function createReferenceFilter(patterns: readonly string[]): (target: string) => boolean;
97
- /** The literal directory prefix of a pattern, used to avoid walking the world. */
92
+ /**
93
+ * The literal directory a pattern's matches all sit under, or `''` for the root.
94
+ *
95
+ * `{docs,specs}/**` has two, and this is the directory they share; the walk
96
+ * itself starts at each.
97
+ */
98
98
  export declare function globBase(pattern: string): string;
99
99
  /**
100
100
  * Walks the tree and returns every file matching the patterns.
package/dist/glob.js CHANGED
@@ -1,15 +1,16 @@
1
1
  /**
2
2
  * Pattern matching and directory walking.
3
3
  *
4
- * A dependency-free glob, because pulling in a matcher for `docs/**\/*.md` would
5
- * be the single largest thing in the package. The supported syntax is the
6
- * portion people actually type - `**`, `*`, `?`, `{a,b}`, `[abc]`, and a leading
7
- * `!` for negation - and everything is matched against repository-relative
8
- * POSIX paths so results do not depend on the host platform.
4
+ * The patterns are the `path` dialect every spec-* tool reads, from spec-core:
5
+ * `**`, `*`, `?`, `{a,b}`, `[abc]` and `[!a-z]`, matched against whole
6
+ * repository-relative POSIX paths by an automaton that cannot backtrack, with
7
+ * case respected on every host (ADR-0022). What stays here is spec-graph's own:
8
+ * how a list with `!` entries in it reads, which directories a walk never
9
+ * enters, and how a reference target - which is not a path - is matched.
9
10
  */
10
11
  import { readdir, stat } from 'node:fs/promises';
11
- import { isAbsolutePath, joinPosix, normalisePosix, toPosix } from './paths.js';
12
- import { compilePattern } from './regex.js';
12
+ import { isAbsolutePath, joinPosix, toPosix } from './paths.js';
13
+ import { compileGlob as compileFamilyGlob, GlobError, isGlobSyntax, parseGlob, parseGlobList, } from './vendor/spec-core/pattern/index.js';
13
14
  /** Directories skipped unless a pattern explicitly names them. */
14
15
  export const DEFAULT_IGNORED_DIRECTORIES = Object.freeze([
15
16
  '.git',
@@ -32,55 +33,51 @@ export const DEFAULT_IGNORED_DIRECTORIES = Object.freeze([
32
33
  ]);
33
34
  /** Files larger than this are skipped: a specification is not a binary. */
34
35
  export const MAX_FILE_SIZE = 4 * 1024 * 1024;
36
+ /**
37
+ * How a path pattern is read everywhere in spec-graph.
38
+ *
39
+ * A literal names a file or a directory and everything beneath it, which is
40
+ * what `--ignore docs/drafts` has always meant. A `\` is a separator, because a
41
+ * pattern typed on a Windows shell arrives with them and spec-graph has always
42
+ * read them that way; it cannot also be an escape.
43
+ */
44
+ const PATH = { dialect: 'path', caseSensitive: true, literal: 'either', backslash: 'separator' };
35
45
  /** True when the string contains glob syntax rather than being a literal path. */
36
46
  export function isGlob(pattern) {
37
- return /[*?[\]{}]/.test(pattern);
47
+ return isGlobSyntax(pattern);
38
48
  }
39
49
  /**
40
- * Compiles a glob to an anchored regular expression.
50
+ * Compiles a glob to an anchored regular expression, in the dialect spec-graph
51
+ * read before it adopted spec-core's.
41
52
  *
42
- * `**` crosses directory separators; `*` and `?` do not. A trailing `/` or a
43
- * bare directory name matches everything beneath it, which is what people mean
44
- * when they write `--ignore drafts`.
45
- *
46
- * Nothing in the pipeline matches with this any more - see {@link compileGlob},
47
- * which gives the same answers without the backtracking. It stays for callers
48
- * who want a `RegExp`, and as the oracle the tests hold the automaton to.
53
+ * @deprecated Nothing matches with this. It stays for callers who want a
54
+ * `RegExp`, and as the record of the old reading that `tests/glob.test.ts`
55
+ * holds every difference against, each one named in ADR-0022. It no longer
56
+ * folds case on Windows alone: an answer must not depend on the host.
49
57
  */
50
58
  export function globToRegExp(pattern) {
51
- return new RegExp(`^${globSource(pattern)}$`, process.platform === 'win32' ? 'i' : '');
59
+ return new RegExp(`^${globSource(pattern)}$`);
52
60
  }
53
61
  /**
54
- * Compiles a glob to the automaton every matcher here uses.
55
- *
56
- * A glob has no nested quantifiers, and ADR-0017 took that to mean there was no
57
- * backtracking hazard in handing one to `RegExp`. Nobody had timed it. Each
58
- * `*` is a `[^/]*`, and against a subject that fails to match, a backtracking
59
- * engine tries every way of dividing it between them - the subject's length to
60
- * the power of the stars:
62
+ * Compiles one glob to a matcher over whole repository-relative paths.
61
63
  *
62
- * ```text
63
- * RegExp here
64
- * **\/*-*-*-*.md a 643-character hyphenated file name 2.3s 0.2ms
65
- * *-*-*-x a 10,000-character reference target 120s 0.9ms
66
- * ```
64
+ * A pattern with no glob syntax names that path and nothing beneath it; a bare
65
+ * directory meaning its contents is a reading of {@link createGlobMatcher}'s.
66
+ * Throws a `GlobError` that names the glob as it was written.
67
67
  *
68
- * The first is bounded by the filesystem, which caps a name at 255 bytes. The
69
- * second is not: `--ignore-ref` patterns are matched against targets read out
70
- * of documents, and a document is whatever somebody wrote.
68
+ * Matching is linear in the path whatever the pattern. It was not always: a
69
+ * glob's stars are not nested, but each is a `[^/]*` to `RegExp`, and a subject
70
+ * that fails is divided between them every way there is - `*-*-*-x` took two
71
+ * minutes over a 10,000-character reference target (ADR-0017).
71
72
  */
72
73
  export function compileGlob(pattern, options = {}) {
73
- const source = globSource(pattern);
74
- try {
75
- return compilePattern(`^${source}$`, { ignoreCase: options.ignoreCase ?? process.platform === 'win32' });
76
- }
77
- catch (error) {
78
- // The message describes an expression the user never wrote, so it is told
79
- // against the glob they did.
80
- throw new Error(`invalid glob "${pattern}": ${error.message}`);
81
- }
74
+ // `literal: 'file'` is the dialect's reading of any value but `directory`
75
+ // and `either`, so a mutant that blanks it reads the same. It is spelled out
76
+ // because what an unknown value means is spec-core's to change.
77
+ const glob = compileFamilyGlob(pattern, { ...PATH, literal: 'file', caseSensitive: options.ignoreCase !== true });
78
+ return { test: (path) => glob.match(path), size: glob.automaton.kinds.length };
82
79
  }
83
- /** The expression a glob stands for, before anchors and flags. */
80
+ /** The expression a glob stood for before 0.9.0, before anchors and flags. */
84
81
  function globSource(pattern) {
85
82
  let source = '';
86
83
  let i = 0;
@@ -146,83 +143,118 @@ function globSource(pattern) {
146
143
  function escapeClass(body) {
147
144
  return body.replace(/[\\^\]]/g, '\\$&');
148
145
  }
146
+ /** Compiles a list of path patterns, or throws naming the one that is not a glob. */
147
+ function pathList(patterns) {
148
+ const parsed = parseGlobList(patterns, PATH);
149
+ if (!parsed.ok)
150
+ throw new Error(`invalid glob ${parsed.error}`);
151
+ return parsed.list;
152
+ }
149
153
  /**
150
154
  * Builds a matcher from include patterns, honouring `!` negations.
151
155
  *
152
156
  * Later patterns win, so `docs/**\/*.md` followed by `!docs/drafts/**` reads the
153
- * way a `.gitignore` does.
157
+ * way a `.gitignore` does. A pattern with no glob syntax in it names a file, or
158
+ * a directory and everything beneath it; `docs/` names only what is beneath.
154
159
  */
155
160
  export function createGlobMatcher(patterns) {
156
- const compiled = patterns.map((pattern) => {
157
- const negated = pattern.startsWith('!');
158
- const body = negated ? pattern.slice(1) : pattern;
159
- const normalised = normalisePosix(toPosix(body));
160
- // A bare directory means everything under it.
161
- const expanded = isGlob(normalised) ? normalised : `${normalised}/**`;
162
- return {
163
- negated,
164
- exact: isGlob(normalised) ? null : normalised.toLowerCase(),
165
- expression: compileGlob(expanded),
166
- direct: compileGlob(normalised),
167
- };
168
- });
169
- return (path) => {
170
- let included = false;
171
- for (const entry of compiled) {
172
- const hit = entry.direct.test(path) ||
173
- entry.expression.test(path) ||
174
- (entry.exact !== null && entry.exact === path.toLowerCase());
175
- if (hit)
176
- included = !entry.negated;
177
- }
178
- return included;
179
- };
161
+ const list = pathList(patterns);
162
+ return (path) => list.match(path);
180
163
  }
181
164
  /**
182
165
  * Builds a predicate over reference *targets*, not paths.
183
166
  *
184
167
  * Separate from {@link createGlobMatcher} on purpose. That one is
185
- * path-oriented: it expands a bare name to `name/**` because a directory means
186
- * everything under it. A reference target has no such structure - `trap 55` is
187
- * a name, not a location - so a bare pattern must match it literally and
188
- * nothing else.
168
+ * path-oriented: a bare name there is a directory and everything under it. A
169
+ * reference target has no such structure - `trap 55` is a name, not a location
170
+ * - so a bare pattern matches it literally and nothing else.
189
171
  *
190
- * Matching is case-insensitive on every platform. Path matching inherits the
191
- * host filesystem's case rules, which is right for paths and wrong here: a
192
- * repository's findings must not depend on which machine ran the check.
172
+ * Case is ignored, on every platform, because a citation's spelling is the
173
+ * author's and not the filesystem's. The fold is simple case mapping, one code
174
+ * point at a time and without a locale, so every host gives the same answer:
175
+ * U+00B5 (micro sign) and U+03BC (mu) stay two targets, as they have been since
176
+ * a Windows-only `i` flag made them one there (ADR-0017).
193
177
  *
194
- * Both sides are lower-cased, and that is the whole of the case rule. Folding
195
- * as well used to happen on Windows only, where it made U+00B5 (micro sign)
196
- * and U+03BC (mu) the same target - one machine's answer, which is what this
197
- * function exists not to give.
178
+ * A target is text a document holds rather than a path on this host, and two
179
+ * things follow. A `\` escapes the character after it: there is no Windows
180
+ * separator to allow for. And a `.` or `..` segment is text: in a path the
181
+ * dialect drops the one and refuses the other as climbing out of the root, but
182
+ * `../../notes/gone.md` is a link somebody wrote and may want left alone.
198
183
  */
199
184
  export function createReferenceFilter(patterns) {
200
- if (patterns.length === 0)
201
- return () => false;
202
- const compiled = patterns
203
- .filter((pattern) => pattern.trim().length > 0)
204
- .map((pattern) => compileGlob(pattern.trim().toLowerCase(), { ignoreCase: false }));
205
- if (compiled.length === 0)
206
- return () => false;
185
+ // A blank pattern reaches the dialect, which refuses it, as every pattern
186
+ // spec-graph reads is refused when it names nothing. It used to be dropped,
187
+ // so an unset variable in `--ignore-ref "$TAGS"` went unnoticed.
188
+ const globs = patterns.map((pattern) => {
189
+ const literalDots = pattern
190
+ .trim()
191
+ .split('/')
192
+ .map((segment) => (segment === '.' ? '\\.' : segment === '..' ? '\\.\\.' : segment))
193
+ .join('/');
194
+ // Every option spelled out, though the dialect reads `{}` the same way
195
+ // today: its defaults are spec-core's to change, and this reading is
196
+ // spec-graph's to keep. A mutant that drops one is equivalent until then.
197
+ const parsed = parseGlob(literalDots, { dialect: 'path', caseSensitive: false, literal: 'file' });
198
+ if (!parsed.ok)
199
+ throw new GlobError(pattern, parsed.error);
200
+ return parsed.glob;
201
+ });
207
202
  return (target) => {
208
- const value = target.trim().toLowerCase();
209
- return compiled.some((expression) => expression.test(value));
203
+ const value = target.trim();
204
+ return globs.some((glob) => glob.match(value));
210
205
  };
211
206
  }
212
- /** The literal directory prefix of a pattern, used to avoid walking the world. */
207
+ /**
208
+ * The literal directory a pattern's matches all sit under, or `''` for the root.
209
+ *
210
+ * `{docs,specs}/**` has two, and this is the directory they share; the walk
211
+ * itself starts at each.
212
+ */
213
213
  export function globBase(pattern) {
214
- const normalised = normalisePosix(toPosix(pattern.startsWith('!') ? pattern.slice(1) : pattern));
215
- const segments = normalised.split('/');
216
- const literal = [];
217
- for (const segment of segments) {
218
- if (isGlob(segment))
219
- break;
220
- literal.push(segment);
214
+ const { glob } = pathList([pattern]).entries[0];
215
+ const bases = glob.bases.map((base) => base.split('/'));
216
+ const shared = bases[0];
217
+ let length = shared.length;
218
+ for (const base of bases) {
219
+ let same = 0;
220
+ // `<=` would read one past the end, where both sides are undefined and
221
+ // equal, and slice the same prefix: an equivalent mutant, left untested.
222
+ while (same < length && base[same] === shared[same])
223
+ same += 1;
224
+ length = same;
225
+ }
226
+ return shared.slice(0, length).join('/');
227
+ }
228
+ /**
229
+ * Whether a base names directories that exist with exactly that spelling.
230
+ *
231
+ * A walk starts where a pattern's literal prefix points rather than at the
232
+ * root, and on a filesystem that ignores case, `readdir('Docs')` lists `docs`.
233
+ * Every file under it would come back spelled `Docs/...` - a node id, a
234
+ * baseline key - and `Docs/**` would find it on Windows and macOS and nothing
235
+ * on Linux. Asking each parent for the name gives git's answer on every host. A
236
+ * base with an empty segment is rooted at `/`, which is never inside the root.
237
+ */
238
+ async function spelledAsOnDisk(root, base) {
239
+ if (base.length === 0)
240
+ return true;
241
+ let directory = root;
242
+ for (const segment of base.split('/')) {
243
+ let names;
244
+ try {
245
+ names = await readdir(directory);
246
+ }
247
+ catch {
248
+ // Answering `true` here would change nothing a test can see: the walk
249
+ // would then read a directory beneath this one, fail the same way, and
250
+ // find nothing.
251
+ return false;
252
+ }
253
+ if (!names.includes(segment))
254
+ return false;
255
+ directory = `${directory}/${segment}`;
221
256
  }
222
- // The last literal segment may be the file itself rather than a directory.
223
- if (literal.length === segments.length && literal.length > 0)
224
- literal.pop();
225
- return literal.join('/');
257
+ return true;
226
258
  }
227
259
  /**
228
260
  * Walks the tree and returns every file matching the patterns.
@@ -232,7 +264,7 @@ export function globBase(pattern) {
232
264
  */
233
265
  export async function walkFiles(options) {
234
266
  const root = toPosix(options.root).replace(/\/+$/, '');
235
- const matcher = createGlobMatcher(options.patterns);
267
+ const patterns = pathList(options.patterns);
236
268
  const maxSize = options.maxFileSize ?? MAX_FILE_SIZE;
237
269
  // Ignores come in two shapes and both are documented. A bare name prunes any
238
270
  // directory called that, at any depth, the way a `.gitignore` line does.
@@ -240,22 +272,25 @@ export async function walkFiles(options) {
240
272
  // repository-relative path, which is what `--ignore "docs/drafts/**"` means -
241
273
  // and treating that as a directory name silently excluded nothing at all.
242
274
  const ignores = options.ignore ?? [];
243
- const ignoredNames = new Set([
244
- ...DEFAULT_IGNORED_DIRECTORIES,
245
- ...ignores.filter((pattern) => !isGlob(pattern) && !pattern.includes('/')),
246
- ]);
247
- const pathIgnores = ignores.filter((pattern) => isGlob(pattern) || pattern.includes('/'));
248
- const ignoreMatcher = pathIgnores.length > 0 ? createGlobMatcher(pathIgnores) : null;
249
- const excluded = (path) => ignoreMatcher !== null && ignoreMatcher(path);
250
- // Only walk the directories the patterns can possibly reach.
275
+ // The empty pattern is no directory's name, so it goes to the dialect, which
276
+ // refuses it as it refuses an empty include: a pattern that names nothing is
277
+ // a typo or an unset variable, and was an ignore that ignored nothing.
278
+ const bare = (pattern) => pattern.length > 0 && !isGlob(pattern) && !pattern.includes('/');
279
+ const ignoredNames = new Set([...DEFAULT_IGNORED_DIRECTORIES, ...ignores.filter(bare)]);
280
+ const ignoreList = pathList(ignores.filter((pattern) => !bare(pattern)));
281
+ const excluded = (path) => ignoreList.match(path);
282
+ // Only walk the directories the patterns can possibly reach: each pattern's
283
+ // literal prefix, one per brace alternative. A negated pattern adds none,
284
+ // and adding its would change no answer - whatever lies beneath it that no
285
+ // other pattern reaches, no other pattern matches either - so that mutant is
286
+ // equivalent. No positive pattern at all walks nothing, which is the answer.
251
287
  const bases = new Set();
252
- for (const pattern of options.patterns) {
253
- if (pattern.startsWith('!'))
288
+ for (const entry of patterns.entries) {
289
+ if (entry.negated)
254
290
  continue;
255
- bases.add(globBase(pattern));
291
+ for (const base of entry.glob.bases)
292
+ bases.add(base);
256
293
  }
257
- if (bases.size === 0)
258
- bases.add('');
259
294
  // Drop any base already contained in another: walking it again would only
260
295
  // repeat work. The empty base is the repository root, which contains
261
296
  // everything, so when it is present it is the only root worth walking.
@@ -296,7 +331,7 @@ export async function walkFiles(options) {
296
331
  await walk(child);
297
332
  continue;
298
333
  }
299
- if (!matcher(child) || excluded(child) || info.size > maxSize)
334
+ if (!patterns.match(child) || excluded(child) || info.size > maxSize)
300
335
  continue;
301
336
  out.set(child, { path: child, absolute: `${root}/${child}`, size: info.size });
302
337
  }
@@ -307,7 +342,7 @@ export async function walkFiles(options) {
307
342
  }
308
343
  if (!entry.isFile())
309
344
  continue;
310
- if (!matcher(child) || excluded(child))
345
+ if (!patterns.match(child) || excluded(child))
311
346
  continue;
312
347
  try {
313
348
  const info = await stat(`${root}/${child}`);
@@ -321,7 +356,8 @@ export async function walkFiles(options) {
321
356
  }
322
357
  };
323
358
  for (const base of roots)
324
- await walk(base);
359
+ if (await spelledAsOnDisk(root, base))
360
+ await walk(base);
325
361
  return [...out.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
326
362
  }
327
363
  /**
package/dist/glob.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,EAAE,cAAc,EAAgB,MAAM,YAAY,CAAC;AAE1D,kEAAkE;AAClE,MAAM,CAAC,MAAM,2BAA2B,GAAsB,MAAM,CAAC,MAAM,CAAC;IAC1E,MAAM;IACN,KAAK;IACL,MAAM;IACN,QAAQ;IACR,OAAO;IACP,OAAO;IACP,QAAQ;IACR,OAAO;IACP,cAAc;IACd,kBAAkB;IAClB,QAAQ;IACR,MAAM;IACN,OAAO;IACP,KAAK;IACL,QAAQ;IACR,UAAU;IACV,aAAa;CACd,CAAC,CAAC;AAEH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAmB7C,kFAAkF;AAClF,MAAM,UAAU,MAAM,CAAC,OAAe;IACpC,OAAO,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,OAAO,IAAI,MAAM,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACzF,CAAC;AAOD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,OAAO,GAAgB,EAAE;IACpE,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,CAAC;QACH,OAAO,cAAc,CAAC,IAAI,MAAM,GAAG,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC,CAAC;IAC3G,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,0EAA0E;QAC1E,6BAA6B;QAC7B,MAAM,IAAI,KAAK,CAAC,iBAAiB,OAAO,MAAO,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;IAC5E,CAAC;AACH,CAAC;AAED,kEAAkE;AAClE,SAAS,UAAU,CAAC,OAAe;IACjC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;QAC1B,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAW,CAAC;QAEhC,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC;YACvC,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC;gBAC1C,iEAAiE;gBACjE,MAAM,IAAI,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;gBACzC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1B,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,OAAO,CAAC;gBAClB,CAAC,IAAI,CAAC,CAAC;YACT,CAAC;YACD,SAAS;QACX,CAAC;QAED,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,MAAM,IAAI,MAAM,CAAC;YACjB,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QAED,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1C,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC;gBAChB,CAAC,IAAI,CAAC,CAAC;gBACP,SAAS;YACX,CAAC;YACD,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;YACzC,MAAM,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC;YAC7F,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;YACd,SAAS;QACX,CAAC;QAED,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC;YAChB,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpC,MAAM,CAAC,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,GAAG,CAAC;YACd,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,GAAG,CAAC;YACd,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QAED,MAAM,IAAI,EAAE,CAAC,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;QAClD,CAAC,IAAI,CAAC,CAAC;IACT,CAAC;IAED,2EAA2E;IAC3E,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,OAAO,iBAAiB,CAAC,CAAC;IAClF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AAC1C,CAAC;AAMD;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAA2B;IAC3D,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QACxC,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACxC,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;QAClD,MAAM,UAAU,GAAG,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACjD,8CAA8C;QAC9C,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,KAAK,CAAC;QACtE,OAAO;YACL,OAAO;YACP,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,EAAE;YAC3D,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC;YACjC,MAAM,EAAE,WAAW,CAAC,UAAU,CAAC;SAChC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,IAAY,EAAW,EAAE;QAC/B,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,MAAM,GAAG,GACP,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;gBACvB,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;gBAC3B,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;YAC/D,IAAI,GAAG;gBAAE,QAAQ,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC;QACrC,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAA2B;IAC/D,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC;IAC9C,MAAM,QAAQ,GAAG,QAAQ;SACtB,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;SAC9C,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACtF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC;IAC9C,OAAO,CAAC,MAAc,EAAW,EAAE;QACjC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC1C,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC;AACJ,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,MAAM,UAAU,GAAG,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACjG,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,MAAM,CAAC,OAAO,CAAC;YAAE,MAAM;QAC3B,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC;IACD,2EAA2E;IAC3E,IAAI,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,CAAC,GAAG,EAAE,CAAC;IAC5E,OAAO,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAoB;IAClD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACvD,MAAM,OAAO,GAAG,iBAAiB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpD,MAAM,OAAO,GAAG,OAAO,CAAC,WAAW,IAAI,aAAa,CAAC;IAErD,6EAA6E;IAC7E,yEAAyE;IACzE,sEAAsE;IACtE,8EAA8E;IAC9E,0EAA0E;IAC1E,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;IACrC,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC;QAC3B,GAAG,2BAA2B;QAC9B,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;KAC3E,CAAC,CAAC;IACH,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1F,MAAM,aAAa,GAAG,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACrF,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,aAAa,KAAK,IAAI,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC;IAE1F,6DAA6D;IAC7D,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;QACvC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QACtC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/B,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;QAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACpC,0EAA0E;IAC1E,qEAAqE;IACrE,uEAAuE;IACvE,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,CAAC,CAAC,CAAC,EAAE,CAAC;QACN,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;IAE7G,MAAM,GAAG,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC1C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAElC,MAAM,IAAI,GAAG,KAAK,EAAE,QAAgB,EAAiB,EAAE;QACrD,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,QAAQ,EAAE,CAAC;QACtE,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO;QAClC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAEtB,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,CAAC;QAAC,MAAM,CAAC;YACP,iEAAiE;YACjE,OAAO;QACT,CAAC;QAED,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;YAE/E,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,kEAAkE;gBAClE,wEAAwE;gBACxE,IAAI,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC;oBAAE,SAAS;gBAC9D,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC;gBAClB,SAAS;YACX,CAAC;YAED,IAAI,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;gBAC3B,IAAI,CAAC,OAAO,CAAC,cAAc;oBAAE,SAAS;gBACtC,IAAI,CAAC;oBACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC;oBAC5C,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;wBACvB,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC;wBAClB,SAAS;oBACX,CAAC;oBACD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,GAAG,OAAO;wBAAE,SAAS;oBACxE,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,IAAI,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;gBACjF,CAAC;gBAAC,MAAM,CAAC;oBACP,SAAS;gBACX,CAAC;gBACD,SAAS;YACX,CAAC;YAED,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;gBAAE,SAAS;YAC9B,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC;gBAAE,SAAS;YAEjD,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC;gBAC5C,IAAI,IAAI,CAAC,IAAI,GAAG,OAAO;oBAAE,SAAS;gBAClC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,IAAI,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YACjF,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;IAE3C,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5F,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,QAAgB;IACtD,IAAI,cAAc,CAAC,QAAQ,CAAC;QAAE,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvD,OAAO,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC;AAC5C,CAAC","sourcesContent":["/**\n * Pattern matching and directory walking.\n *\n * A dependency-free glob, because pulling in a matcher for `docs/**\\/*.md` would\n * be the single largest thing in the package. The supported syntax is the\n * portion people actually type - `**`, `*`, `?`, `{a,b}`, `[abc]`, and a leading\n * `!` for negation - and everything is matched against repository-relative\n * POSIX paths so results do not depend on the host platform.\n */\n\nimport { readdir, stat } from 'node:fs/promises';\n\nimport { isAbsolutePath, joinPosix, normalisePosix, toPosix } from './paths.js';\nimport { compilePattern, type Matcher } from './regex.js';\n\n/** Directories skipped unless a pattern explicitly names them. */\nexport const DEFAULT_IGNORED_DIRECTORIES: readonly string[] = Object.freeze([\n '.git',\n '.hg',\n '.svn',\n '.cache',\n '.next',\n '.nuxt',\n '.turbo',\n '.venv',\n 'node_modules',\n 'bower_components',\n 'vendor',\n 'dist',\n 'build',\n 'out',\n 'target',\n 'coverage',\n '__pycache__',\n]);\n\n/** Files larger than this are skipped: a specification is not a binary. */\nexport const MAX_FILE_SIZE = 4 * 1024 * 1024;\n\nexport interface WalkedFile {\n /** Repository-relative POSIX path. */\n readonly path: string;\n /** Absolute host path, for reading. */\n readonly absolute: string;\n readonly size: number;\n}\n\nexport interface WalkOptions {\n readonly root: string;\n readonly patterns: readonly string[];\n readonly ignore?: readonly string[] | undefined;\n readonly maxFileSize?: number | undefined;\n /** Follow directory symlinks. Off by default: cycles are real. */\n readonly followSymlinks?: boolean | undefined;\n}\n\n/** True when the string contains glob syntax rather than being a literal path. */\nexport function isGlob(pattern: string): boolean {\n return /[*?[\\]{}]/.test(pattern);\n}\n\n/**\n * Compiles a glob to an anchored regular expression.\n *\n * `**` crosses directory separators; `*` and `?` do not. A trailing `/` or a\n * bare directory name matches everything beneath it, which is what people mean\n * when they write `--ignore drafts`.\n *\n * Nothing in the pipeline matches with this any more - see {@link compileGlob},\n * which gives the same answers without the backtracking. It stays for callers\n * who want a `RegExp`, and as the oracle the tests hold the automaton to.\n */\nexport function globToRegExp(pattern: string): RegExp {\n return new RegExp(`^${globSource(pattern)}$`, process.platform === 'win32' ? 'i' : '');\n}\n\nexport interface GlobOptions {\n /** Fold case. Defaults to the host's convention: on for Windows, off elsewhere. */\n readonly ignoreCase?: boolean | undefined;\n}\n\n/**\n * Compiles a glob to the automaton every matcher here uses.\n *\n * A glob has no nested quantifiers, and ADR-0017 took that to mean there was no\n * backtracking hazard in handing one to `RegExp`. Nobody had timed it. Each\n * `*` is a `[^/]*`, and against a subject that fails to match, a backtracking\n * engine tries every way of dividing it between them - the subject's length to\n * the power of the stars:\n *\n * ```text\n * RegExp here\n * **\\/*-*-*-*.md a 643-character hyphenated file name 2.3s 0.2ms\n * *-*-*-x a 10,000-character reference target 120s 0.9ms\n * ```\n *\n * The first is bounded by the filesystem, which caps a name at 255 bytes. The\n * second is not: `--ignore-ref` patterns are matched against targets read out\n * of documents, and a document is whatever somebody wrote.\n */\nexport function compileGlob(pattern: string, options: GlobOptions = {}): Matcher {\n const source = globSource(pattern);\n try {\n return compilePattern(`^${source}$`, { ignoreCase: options.ignoreCase ?? process.platform === 'win32' });\n } catch (error) {\n // The message describes an expression the user never wrote, so it is told\n // against the glob they did.\n throw new Error(`invalid glob \"${pattern}\": ${(error as Error).message}`);\n }\n}\n\n/** The expression a glob stands for, before anchors and flags. */\nfunction globSource(pattern: string): string {\n let source = '';\n let i = 0;\n const braces: number[] = [];\n\n while (i < pattern.length) {\n const ch = pattern[i] as string;\n\n if (ch === '*') {\n const doubled = pattern[i + 1] === '*';\n if (doubled) {\n const slashAfter = pattern[i + 2] === '/';\n // `a/**/b` must also match `a/b`, so the separator is folded in.\n source += slashAfter ? '(?:.*/)?' : '.*';\n i += slashAfter ? 3 : 2;\n } else {\n source += '[^/]*';\n i += 1;\n }\n continue;\n }\n\n if (ch === '?') {\n source += '[^/]';\n i += 1;\n continue;\n }\n\n if (ch === '[') {\n const close = pattern.indexOf(']', i + 1);\n if (close === -1) {\n source += '\\\\[';\n i += 1;\n continue;\n }\n const body = pattern.slice(i + 1, close);\n source += `[${body.startsWith('!') ? `^${escapeClass(body.slice(1))}` : escapeClass(body)}]`;\n i = close + 1;\n continue;\n }\n\n if (ch === '{') {\n braces.push(source.length);\n source += '(?:';\n i += 1;\n continue;\n }\n if (ch === '}' && braces.length > 0) {\n braces.pop();\n source += ')';\n i += 1;\n continue;\n }\n if (ch === ',' && braces.length > 0) {\n source += '|';\n i += 1;\n continue;\n }\n\n source += ch.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&');\n i += 1;\n }\n\n // Otherwise reported as an unmatched parenthesis, in a glob that has none.\n if (braces.length > 0) throw new Error(`invalid glob \"${pattern}\": unclosed \"{\"`);\n return source;\n}\n\nfunction escapeClass(body: string): string {\n return body.replace(/[\\\\^\\]]/g, '\\\\$&');\n}\n\nexport interface GlobMatcher {\n (path: string): boolean;\n}\n\n/**\n * Builds a matcher from include patterns, honouring `!` negations.\n *\n * Later patterns win, so `docs/**\\/*.md` followed by `!docs/drafts/**` reads the\n * way a `.gitignore` does.\n */\nexport function createGlobMatcher(patterns: readonly string[]): GlobMatcher {\n const compiled = patterns.map((pattern) => {\n const negated = pattern.startsWith('!');\n const body = negated ? pattern.slice(1) : pattern;\n const normalised = normalisePosix(toPosix(body));\n // A bare directory means everything under it.\n const expanded = isGlob(normalised) ? normalised : `${normalised}/**`;\n return {\n negated,\n exact: isGlob(normalised) ? null : normalised.toLowerCase(),\n expression: compileGlob(expanded),\n direct: compileGlob(normalised),\n };\n });\n\n return (path: string): boolean => {\n let included = false;\n for (const entry of compiled) {\n const hit =\n entry.direct.test(path) ||\n entry.expression.test(path) ||\n (entry.exact !== null && entry.exact === path.toLowerCase());\n if (hit) included = !entry.negated;\n }\n return included;\n };\n}\n\n/**\n * Builds a predicate over reference *targets*, not paths.\n *\n * Separate from {@link createGlobMatcher} on purpose. That one is\n * path-oriented: it expands a bare name to `name/**` because a directory means\n * everything under it. A reference target has no such structure - `trap 55` is\n * a name, not a location - so a bare pattern must match it literally and\n * nothing else.\n *\n * Matching is case-insensitive on every platform. Path matching inherits the\n * host filesystem's case rules, which is right for paths and wrong here: a\n * repository's findings must not depend on which machine ran the check.\n *\n * Both sides are lower-cased, and that is the whole of the case rule. Folding\n * as well used to happen on Windows only, where it made U+00B5 (micro sign)\n * and U+03BC (mu) the same target - one machine's answer, which is what this\n * function exists not to give.\n */\nexport function createReferenceFilter(patterns: readonly string[]): (target: string) => boolean {\n if (patterns.length === 0) return () => false;\n const compiled = patterns\n .filter((pattern) => pattern.trim().length > 0)\n .map((pattern) => compileGlob(pattern.trim().toLowerCase(), { ignoreCase: false }));\n if (compiled.length === 0) return () => false;\n return (target: string): boolean => {\n const value = target.trim().toLowerCase();\n return compiled.some((expression) => expression.test(value));\n };\n}\n\n/** The literal directory prefix of a pattern, used to avoid walking the world. */\nexport function globBase(pattern: string): string {\n const normalised = normalisePosix(toPosix(pattern.startsWith('!') ? pattern.slice(1) : pattern));\n const segments = normalised.split('/');\n const literal: string[] = [];\n for (const segment of segments) {\n if (isGlob(segment)) break;\n literal.push(segment);\n }\n // The last literal segment may be the file itself rather than a directory.\n if (literal.length === segments.length && literal.length > 0) literal.pop();\n return literal.join('/');\n}\n\n/**\n * Walks the tree and returns every file matching the patterns.\n *\n * Directories are pruned as early as possible: a repository with a 2 GB\n * `node_modules` should cost nothing to scan.\n */\nexport async function walkFiles(options: WalkOptions): Promise<WalkedFile[]> {\n const root = toPosix(options.root).replace(/\\/+$/, '');\n const matcher = createGlobMatcher(options.patterns);\n const maxSize = options.maxFileSize ?? MAX_FILE_SIZE;\n\n // Ignores come in two shapes and both are documented. A bare name prunes any\n // directory called that, at any depth, the way a `.gitignore` line does.\n // Anything carrying a separator or glob syntax is matched against the\n // repository-relative path, which is what `--ignore \"docs/drafts/**\"` means -\n // and treating that as a directory name silently excluded nothing at all.\n const ignores = options.ignore ?? [];\n const ignoredNames = new Set([\n ...DEFAULT_IGNORED_DIRECTORIES,\n ...ignores.filter((pattern) => !isGlob(pattern) && !pattern.includes('/')),\n ]);\n const pathIgnores = ignores.filter((pattern) => isGlob(pattern) || pattern.includes('/'));\n const ignoreMatcher = pathIgnores.length > 0 ? createGlobMatcher(pathIgnores) : null;\n const excluded = (path: string): boolean => ignoreMatcher !== null && ignoreMatcher(path);\n\n // Only walk the directories the patterns can possibly reach.\n const bases = new Set<string>();\n for (const pattern of options.patterns) {\n if (pattern.startsWith('!')) continue;\n bases.add(globBase(pattern));\n }\n if (bases.size === 0) bases.add('');\n // Drop any base already contained in another: walking it again would only\n // repeat work. The empty base is the repository root, which contains\n // everything, so when it is present it is the only root worth walking.\n const roots = bases.has('')\n ? ['']\n : [...bases].filter((base) => ![...bases].some((other) => other !== base && base.startsWith(`${other}/`)));\n\n const out = new Map<string, WalkedFile>();\n const visited = new Set<string>();\n\n const walk = async (relative: string): Promise<void> => {\n const absolute = relative.length === 0 ? root : `${root}/${relative}`;\n if (visited.has(absolute)) return;\n visited.add(absolute);\n\n let entries;\n try {\n entries = await readdir(absolute, { withFileTypes: true });\n } catch {\n // An unreadable directory is not a reason to fail the whole run.\n return;\n }\n\n for (const entry of entries) {\n const child = relative.length === 0 ? entry.name : `${relative}/${entry.name}`;\n\n if (entry.isDirectory()) {\n // Pruning a whole subtree is an optimisation; a pattern that only\n // matches the files inside it is still honoured when they are filtered.\n if (ignoredNames.has(entry.name) || excluded(child)) continue;\n await walk(child);\n continue;\n }\n\n if (entry.isSymbolicLink()) {\n if (!options.followSymlinks) continue;\n try {\n const info = await stat(`${root}/${child}`);\n if (info.isDirectory()) {\n await walk(child);\n continue;\n }\n if (!matcher(child) || excluded(child) || info.size > maxSize) continue;\n out.set(child, { path: child, absolute: `${root}/${child}`, size: info.size });\n } catch {\n continue;\n }\n continue;\n }\n\n if (!entry.isFile()) continue;\n if (!matcher(child) || excluded(child)) continue;\n\n try {\n const info = await stat(`${root}/${child}`);\n if (info.size > maxSize) continue;\n out.set(child, { path: child, absolute: `${root}/${child}`, size: info.size });\n } catch {\n continue;\n }\n }\n };\n\n for (const base of roots) await walk(base);\n\n return [...out.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n}\n\n/**\n * Resolves a repository-relative path against the root, in POSIX form.\n *\n * An absolute path already names one place, and joining it under the root names\n * another: `--baseline /tmp/b.json` read `<root>/tmp/b.json`, found nothing\n * there, and accepted nothing without saying so.\n */\nexport function underRoot(root: string, relative: string): string {\n if (isAbsolutePath(relative)) return toPosix(relative);\n return joinPosix(toPosix(root), relative);\n}\n"]}
1
+ {"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pattern matching and directory walking.\n *\n * The patterns are the `path` dialect every spec-* tool reads, from spec-core:\n * `**`, `*`, `?`, `{a,b}`, `[abc]` and `[!a-z]`, matched against whole\n * repository-relative POSIX paths by an automaton that cannot backtrack, with\n * case respected on every host (ADR-0022). What stays here is spec-graph's own:\n * how a list with `!` entries in it reads, which directories a walk never\n * enters, and how a reference target - which is not a path - is matched.\n */\n\nimport { readdir, stat } from 'node:fs/promises';\n\nimport { isAbsolutePath, joinPosix, toPosix } from './paths.js';\nimport {\n compileGlob as compileFamilyGlob,\n GlobError,\n isGlobSyntax,\n parseGlob,\n parseGlobList,\n type Glob,\n type GlobList,\n type GlobOptions as FamilyGlobOptions,\n type RegexMatcher,\n} from './vendor/spec-core/pattern/index.js';\n\n/** Directories skipped unless a pattern explicitly names them. */\nexport const DEFAULT_IGNORED_DIRECTORIES: readonly string[] = Object.freeze([\n '.git',\n '.hg',\n '.svn',\n '.cache',\n '.next',\n '.nuxt',\n '.turbo',\n '.venv',\n 'node_modules',\n 'bower_components',\n 'vendor',\n 'dist',\n 'build',\n 'out',\n 'target',\n 'coverage',\n '__pycache__',\n]);\n\n/** Files larger than this are skipped: a specification is not a binary. */\nexport const MAX_FILE_SIZE = 4 * 1024 * 1024;\n\nexport interface WalkedFile {\n /** Repository-relative POSIX path. */\n readonly path: string;\n /** Absolute host path, for reading. */\n readonly absolute: string;\n readonly size: number;\n}\n\nexport interface WalkOptions {\n readonly root: string;\n readonly patterns: readonly string[];\n readonly ignore?: readonly string[] | undefined;\n readonly maxFileSize?: number | undefined;\n /** Follow directory symlinks. Off by default: cycles are real. */\n readonly followSymlinks?: boolean | undefined;\n}\n\n/**\n * How a path pattern is read everywhere in spec-graph.\n *\n * A literal names a file or a directory and everything beneath it, which is\n * what `--ignore docs/drafts` has always meant. A `\\` is a separator, because a\n * pattern typed on a Windows shell arrives with them and spec-graph has always\n * read them that way; it cannot also be an escape.\n */\nconst PATH: FamilyGlobOptions = { dialect: 'path', caseSensitive: true, literal: 'either', backslash: 'separator' };\n\n/** True when the string contains glob syntax rather than being a literal path. */\nexport function isGlob(pattern: string): boolean {\n return isGlobSyntax(pattern);\n}\n\n/**\n * Compiles a glob to an anchored regular expression, in the dialect spec-graph\n * read before it adopted spec-core's.\n *\n * @deprecated Nothing matches with this. It stays for callers who want a\n * `RegExp`, and as the record of the old reading that `tests/glob.test.ts`\n * holds every difference against, each one named in ADR-0022. It no longer\n * folds case on Windows alone: an answer must not depend on the host.\n */\nexport function globToRegExp(pattern: string): RegExp {\n return new RegExp(`^${globSource(pattern)}$`);\n}\n\nexport interface GlobOptions {\n /** Fold case. Off unless asked for, on every host. */\n readonly ignoreCase?: boolean | undefined;\n}\n\n/**\n * Compiles one glob to a matcher over whole repository-relative paths.\n *\n * A pattern with no glob syntax names that path and nothing beneath it; a bare\n * directory meaning its contents is a reading of {@link createGlobMatcher}'s.\n * Throws a `GlobError` that names the glob as it was written.\n *\n * Matching is linear in the path whatever the pattern. It was not always: a\n * glob's stars are not nested, but each is a `[^/]*` to `RegExp`, and a subject\n * that fails is divided between them every way there is - `*-*-*-x` took two\n * minutes over a 10,000-character reference target (ADR-0017).\n */\nexport function compileGlob(pattern: string, options: GlobOptions = {}): RegexMatcher {\n // `literal: 'file'` is the dialect's reading of any value but `directory`\n // and `either`, so a mutant that blanks it reads the same. It is spelled out\n // because what an unknown value means is spec-core's to change.\n const glob = compileFamilyGlob(pattern, { ...PATH, literal: 'file', caseSensitive: options.ignoreCase !== true });\n return { test: (path) => glob.match(path), size: glob.automaton.kinds.length };\n}\n\n/** The expression a glob stood for before 0.9.0, before anchors and flags. */\nfunction globSource(pattern: string): string {\n let source = '';\n let i = 0;\n const braces: number[] = [];\n\n while (i < pattern.length) {\n const ch = pattern[i] as string;\n\n if (ch === '*') {\n const doubled = pattern[i + 1] === '*';\n if (doubled) {\n const slashAfter = pattern[i + 2] === '/';\n // `a/**/b` must also match `a/b`, so the separator is folded in.\n source += slashAfter ? '(?:.*/)?' : '.*';\n i += slashAfter ? 3 : 2;\n } else {\n source += '[^/]*';\n i += 1;\n }\n continue;\n }\n\n if (ch === '?') {\n source += '[^/]';\n i += 1;\n continue;\n }\n\n if (ch === '[') {\n const close = pattern.indexOf(']', i + 1);\n if (close === -1) {\n source += '\\\\[';\n i += 1;\n continue;\n }\n const body = pattern.slice(i + 1, close);\n source += `[${body.startsWith('!') ? `^${escapeClass(body.slice(1))}` : escapeClass(body)}]`;\n i = close + 1;\n continue;\n }\n\n if (ch === '{') {\n braces.push(source.length);\n source += '(?:';\n i += 1;\n continue;\n }\n if (ch === '}' && braces.length > 0) {\n braces.pop();\n source += ')';\n i += 1;\n continue;\n }\n if (ch === ',' && braces.length > 0) {\n source += '|';\n i += 1;\n continue;\n }\n\n source += ch.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&');\n i += 1;\n }\n\n // Otherwise reported as an unmatched parenthesis, in a glob that has none.\n if (braces.length > 0) throw new Error(`invalid glob \"${pattern}\": unclosed \"{\"`);\n return source;\n}\n\nfunction escapeClass(body: string): string {\n return body.replace(/[\\\\^\\]]/g, '\\\\$&');\n}\n\nexport interface GlobMatcher {\n (path: string): boolean;\n}\n\n/** Compiles a list of path patterns, or throws naming the one that is not a glob. */\nfunction pathList(patterns: readonly string[]): GlobList {\n const parsed = parseGlobList(patterns, PATH);\n if (!parsed.ok) throw new Error(`invalid glob ${parsed.error}`);\n return parsed.list;\n}\n\n/**\n * Builds a matcher from include patterns, honouring `!` negations.\n *\n * Later patterns win, so `docs/**\\/*.md` followed by `!docs/drafts/**` reads the\n * way a `.gitignore` does. A pattern with no glob syntax in it names a file, or\n * a directory and everything beneath it; `docs/` names only what is beneath.\n */\nexport function createGlobMatcher(patterns: readonly string[]): GlobMatcher {\n const list = pathList(patterns);\n return (path: string): boolean => list.match(path);\n}\n\n/**\n * Builds a predicate over reference *targets*, not paths.\n *\n * Separate from {@link createGlobMatcher} on purpose. That one is\n * path-oriented: a bare name there is a directory and everything under it. A\n * reference target has no such structure - `trap 55` is a name, not a location\n * - so a bare pattern matches it literally and nothing else.\n *\n * Case is ignored, on every platform, because a citation's spelling is the\n * author's and not the filesystem's. The fold is simple case mapping, one code\n * point at a time and without a locale, so every host gives the same answer:\n * U+00B5 (micro sign) and U+03BC (mu) stay two targets, as they have been since\n * a Windows-only `i` flag made them one there (ADR-0017).\n *\n * A target is text a document holds rather than a path on this host, and two\n * things follow. A `\\` escapes the character after it: there is no Windows\n * separator to allow for. And a `.` or `..` segment is text: in a path the\n * dialect drops the one and refuses the other as climbing out of the root, but\n * `../../notes/gone.md` is a link somebody wrote and may want left alone.\n */\nexport function createReferenceFilter(patterns: readonly string[]): (target: string) => boolean {\n // A blank pattern reaches the dialect, which refuses it, as every pattern\n // spec-graph reads is refused when it names nothing. It used to be dropped,\n // so an unset variable in `--ignore-ref \"$TAGS\"` went unnoticed.\n const globs: Glob[] = patterns.map((pattern) => {\n const literalDots = pattern\n .trim()\n .split('/')\n .map((segment) => (segment === '.' ? '\\\\.' : segment === '..' ? '\\\\.\\\\.' : segment))\n .join('/');\n // Every option spelled out, though the dialect reads `{}` the same way\n // today: its defaults are spec-core's to change, and this reading is\n // spec-graph's to keep. A mutant that drops one is equivalent until then.\n const parsed = parseGlob(literalDots, { dialect: 'path', caseSensitive: false, literal: 'file' });\n if (!parsed.ok) throw new GlobError(pattern, parsed.error);\n return parsed.glob;\n });\n return (target: string): boolean => {\n const value = target.trim();\n return globs.some((glob) => glob.match(value));\n };\n}\n\n/**\n * The literal directory a pattern's matches all sit under, or `''` for the root.\n *\n * `{docs,specs}/**` has two, and this is the directory they share; the walk\n * itself starts at each.\n */\nexport function globBase(pattern: string): string {\n const { glob } = pathList([pattern]).entries[0] as { readonly glob: Glob };\n const bases = glob.bases.map((base) => base.split('/'));\n const shared = bases[0] as string[];\n let length = shared.length;\n for (const base of bases) {\n let same = 0;\n // `<=` would read one past the end, where both sides are undefined and\n // equal, and slice the same prefix: an equivalent mutant, left untested.\n while (same < length && base[same] === shared[same]) same += 1;\n length = same;\n }\n return shared.slice(0, length).join('/');\n}\n\n/**\n * Whether a base names directories that exist with exactly that spelling.\n *\n * A walk starts where a pattern's literal prefix points rather than at the\n * root, and on a filesystem that ignores case, `readdir('Docs')` lists `docs`.\n * Every file under it would come back spelled `Docs/...` - a node id, a\n * baseline key - and `Docs/**` would find it on Windows and macOS and nothing\n * on Linux. Asking each parent for the name gives git's answer on every host. A\n * base with an empty segment is rooted at `/`, which is never inside the root.\n */\nasync function spelledAsOnDisk(root: string, base: string): Promise<boolean> {\n if (base.length === 0) return true;\n let directory = root;\n for (const segment of base.split('/')) {\n let names: string[];\n try {\n names = await readdir(directory);\n } catch {\n // Answering `true` here would change nothing a test can see: the walk\n // would then read a directory beneath this one, fail the same way, and\n // find nothing.\n return false;\n }\n if (!names.includes(segment)) return false;\n directory = `${directory}/${segment}`;\n }\n return true;\n}\n\n/**\n * Walks the tree and returns every file matching the patterns.\n *\n * Directories are pruned as early as possible: a repository with a 2 GB\n * `node_modules` should cost nothing to scan.\n */\nexport async function walkFiles(options: WalkOptions): Promise<WalkedFile[]> {\n const root = toPosix(options.root).replace(/\\/+$/, '');\n const patterns = pathList(options.patterns);\n const maxSize = options.maxFileSize ?? MAX_FILE_SIZE;\n\n // Ignores come in two shapes and both are documented. A bare name prunes any\n // directory called that, at any depth, the way a `.gitignore` line does.\n // Anything carrying a separator or glob syntax is matched against the\n // repository-relative path, which is what `--ignore \"docs/drafts/**\"` means -\n // and treating that as a directory name silently excluded nothing at all.\n const ignores = options.ignore ?? [];\n // The empty pattern is no directory's name, so it goes to the dialect, which\n // refuses it as it refuses an empty include: a pattern that names nothing is\n // a typo or an unset variable, and was an ignore that ignored nothing.\n const bare = (pattern: string): boolean => pattern.length > 0 && !isGlob(pattern) && !pattern.includes('/');\n const ignoredNames = new Set([...DEFAULT_IGNORED_DIRECTORIES, ...ignores.filter(bare)]);\n const ignoreList = pathList(ignores.filter((pattern) => !bare(pattern)));\n const excluded = (path: string): boolean => ignoreList.match(path);\n\n // Only walk the directories the patterns can possibly reach: each pattern's\n // literal prefix, one per brace alternative. A negated pattern adds none,\n // and adding its would change no answer - whatever lies beneath it that no\n // other pattern reaches, no other pattern matches either - so that mutant is\n // equivalent. No positive pattern at all walks nothing, which is the answer.\n const bases = new Set<string>();\n for (const entry of patterns.entries) {\n if (entry.negated) continue;\n for (const base of entry.glob.bases) bases.add(base);\n }\n // Drop any base already contained in another: walking it again would only\n // repeat work. The empty base is the repository root, which contains\n // everything, so when it is present it is the only root worth walking.\n const roots = bases.has('')\n ? ['']\n : [...bases].filter((base) => ![...bases].some((other) => other !== base && base.startsWith(`${other}/`)));\n\n const out = new Map<string, WalkedFile>();\n const visited = new Set<string>();\n\n const walk = async (relative: string): Promise<void> => {\n const absolute = relative.length === 0 ? root : `${root}/${relative}`;\n if (visited.has(absolute)) return;\n visited.add(absolute);\n\n let entries;\n try {\n entries = await readdir(absolute, { withFileTypes: true });\n } catch {\n // An unreadable directory is not a reason to fail the whole run.\n return;\n }\n\n for (const entry of entries) {\n const child = relative.length === 0 ? entry.name : `${relative}/${entry.name}`;\n\n if (entry.isDirectory()) {\n // Pruning a whole subtree is an optimisation; a pattern that only\n // matches the files inside it is still honoured when they are filtered.\n if (ignoredNames.has(entry.name) || excluded(child)) continue;\n await walk(child);\n continue;\n }\n\n if (entry.isSymbolicLink()) {\n if (!options.followSymlinks) continue;\n try {\n const info = await stat(`${root}/${child}`);\n if (info.isDirectory()) {\n await walk(child);\n continue;\n }\n if (!patterns.match(child) || excluded(child) || info.size > maxSize) continue;\n out.set(child, { path: child, absolute: `${root}/${child}`, size: info.size });\n } catch {\n continue;\n }\n continue;\n }\n\n if (!entry.isFile()) continue;\n if (!patterns.match(child) || excluded(child)) continue;\n\n try {\n const info = await stat(`${root}/${child}`);\n if (info.size > maxSize) continue;\n out.set(child, { path: child, absolute: `${root}/${child}`, size: info.size });\n } catch {\n continue;\n }\n }\n };\n\n for (const base of roots) if (await spelledAsOnDisk(root, base)) await walk(base);\n\n return [...out.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n}\n\n/**\n * Resolves a repository-relative path against the root, in POSIX form.\n *\n * An absolute path already names one place, and joining it under the root names\n * another: `--baseline /tmp/b.json` read `<root>/tmp/b.json`, found nothing\n * there, and accepted nothing without saying so.\n */\nexport function underRoot(root: string, relative: string): string {\n if (isAbsolutePath(relative)) return toPosix(relative);\n return joinPosix(toPosix(root), relative);\n}\n"]}