@descent-vtt/spec-graph 0.9.8 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,67 @@
3
3
  Notable changes, newest first. Versions follow [semver](https://semver.org):
4
4
  a patch fixes behaviour without asking anything of a repository that upgrades.
5
5
 
6
+ ## 0.9.9
7
+
8
+ Where a walk starts is judged as every directory it passes through: a path
9
+ `--ignore` takes out a pattern's starting point, no walk starts behind a link
10
+ the walk does not follow, and a followed link is pruned as a directory is.
11
+ `--ignore-ref` reads a `.` or `..` in a brace alternative as text. A
12
+ repository with no directory link and no such pattern sees nothing change.
13
+
14
+ ### Fixed
15
+
16
+ - **A path `--ignore` takes out the directory a pattern starts in, as it
17
+ takes out one the walk passes through.** Under `--ignore "docs/*"`,
18
+ `docs/**/*.md` read nothing in `docs/drafts`, but `docs/drafts/**/*.md`,
19
+ `docs/drafts/deep/*.md`, `docs/drafts/d.md` and `docs/vendor/` read what
20
+ is there: a walk that starts below the root never passed through the
21
+ directories above its starting point, and only a bare `--ignore` name was
22
+ asked of them. Each directory from the root to a starting point is now
23
+ matched against the path ignores, the starting point included, so the
24
+ answer no longer depends on where a pattern's walk starts; a later
25
+ `!docs/drafts` gives the directory back to every pattern alike, and the
26
+ root is matched against none (ADR-0022). No configuration in this
27
+ repository, its fixtures or the rest of the family combines such a pattern
28
+ and ignore, and over them every `check` format and every graph export is
29
+ byte for byte what 0.9.8 wrote.
30
+ - **A pattern that starts beyond a link is not read through it.** The walk
31
+ passes over a link unless the API's `followSymlinks` is on, but
32
+ `docs/linked/*.md`, `docs/linked/` and `docs/linked/c.md` read what the
33
+ link `docs/linked` leads to, where `docs/**/*.md` passed over it: reading
34
+ a directory by its path follows every link in the path. Each directory on
35
+ the way to a starting point is now looked up in its parent, and a link
36
+ among them ends that walk before it starts, as silently as the walk
37
+ passes over the link; with `followSymlinks` on it is walked as before
38
+ (ADR-0022). No repository in the family holds a link a pattern starts
39
+ beyond, and over this repository, its fixtures and the rest of the family
40
+ every `check` format and every graph export is byte for byte what 0.9.8
41
+ wrote.
42
+ - **A followed link to a directory is pruned as a directory is.** With the
43
+ API's `followSymlinks` on, the walk entered a link to a directory whatever
44
+ its name or path: a link called `node_modules`, which pnpm writes, one a
45
+ bare `--ignore` name names, and one a path `--ignore` such as `docs/p*`
46
+ matches. The default list, a bare name and the path ignores now take it
47
+ out as they take out a directory of that name and path, and the default
48
+ list gives way to a link a pattern names or starts inside, as it does to
49
+ a directory (ADR-0022). The command line never follows a link, so it sees
50
+ nothing change.
51
+ - **`--ignore-ref` reads a `.` or `..` in a brace alternative as text, as it
52
+ reads one anywhere else.** The dots were escaped before the braces were
53
+ expanded, so an alternative's dot was read as a direction:
54
+ `--ignore-ref "docs/{.,x}"` passed over the target `docs` where
55
+ `docs/.` passes over `docs/.`, `{./docs,x}` over `docs` where `./docs`
56
+ passes over `./docs`, and `docs/{x,..}`, `{../notes/*.md,x}`, `{.,docs}`,
57
+ `{./,docs}` and `.{/,docs}` stopped the run with exit `2`, as climbing out
58
+ of the root or naming no path. Each alternative now reads as it does
59
+ written alone: `docs/{.,x}` passes over `docs/.` and `docs/x`, and
60
+ `{./,docs}` over the targets under `./` and the target `docs` - never
61
+ every target, which it passed over before 0.9.8 refused it. A `\` escapes
62
+ inside braces as outside, as it did, and the path patterns still refuse
63
+ `{./,docs}` (ADR-0022). No configuration in this repository, its fixtures
64
+ or the rest of the family writes such a pattern, and over them every
65
+ `check` format and every graph export is byte for byte what 0.9.8 wrote.
66
+
6
67
  ## 0.9.8
7
68
 
8
69
  A plain name such as `docs/vendor` reads a directory the default list skips,
package/README.md CHANGED
@@ -879,7 +879,10 @@ automaton states. A parenthesis with no `|` in its group
879
879
  is itself, so `C++(notes).md` names that file. A `..` typed below the root,
880
880
  `spec-graph "../*.md"` from `docs/deep`, is resolved against where you typed
881
881
  it. A bare `--ignore` name, `--ignore drafts`, still prunes that directory at
882
- any depth, even where a pattern starts inside it or names it.
882
+ any depth, even where a pattern starts inside it or names it. A path,
883
+ `--ignore "docs/*"`, takes out what it matches wherever a pattern starts:
884
+ `docs/drafts/**/*.md` finds nothing under it, as `docs/**/*.md` finds nothing
885
+ in `docs/drafts`.
883
886
 
884
887
  The walk never enters `.git`, `.hg`, `.svn`, `.cache`, `.next`, `.nuxt`,
885
888
  `.turbo`, `.venv`, `node_modules`, `bower_components`, `vendor`, `dist`,
@@ -888,11 +891,18 @@ patterns name. Unlike an `--ignore`, these yield to a pattern that starts inside
888
891
  one: `vendor/specs/*.md` is read from there, whatever the other patterns reach.
889
892
  They yield to a plain name for one as well: `vendor` and `docs/vendor` read
890
893
  those directories as `vendor/` and `docs/vendor/` do, or the file, where one
891
- of them is a file.
894
+ of them is a file. Nor does the walk follow a link, a pattern that starts
895
+ beyond one included: `docs/linked/*.md` reads nothing through the link
896
+ `docs/linked`, as `docs/**/*.md` reads nothing there. `followSymlinks` in the
897
+ [API](#programmatic-api) follows links, and prunes a link to a directory as it
898
+ prunes a directory of that name and path: a linked `node_modules` is skipped,
899
+ and an `--ignore` takes a link out.
892
900
 
893
901
  `--ignore-ref` is matched against reference targets rather than paths: a bare
894
902
  pattern names one target exactly, case is ignored on every host, a `\` escapes
895
- the next character, and `.` and `..` are the text of the link. See
903
+ the next character, and `.` and `..` are the text of the link, in a brace
904
+ alternative as anywhere else: `docs/{.,x}` names `docs/.` and `docs/x`, and
905
+ `{./,docs}` the targets under `./`, and `docs`. See
896
906
  [ADR-0022](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0022-globs-are-the-family-path-dialect.md).
897
907
 
898
908
  ### Family rules
@@ -1108,9 +1118,9 @@ tools share, whose code is copied into `src/vendor/` and checked by hash rather
1108
1118
  than installed. So the whole package is auditable in an afternoon — and nothing
1109
1119
  in the pipeline can take longer than its input.
1110
1120
 
1111
- **Verified, not just covered.** 1,799 tests; 99.3% statement and 99.7% line
1121
+ **Verified, not just covered.** 1,810 tests; 99.3% statement and 99.7% line
1112
1122
  coverage. Coverage says a line ran, so the suite is also held to a mutation
1113
- score - 96.33% over 8,250 mutants, measured by CI's full sweep, and a build
1123
+ score - 96.34% over 8,274 mutants, measured by CI's full sweep, and a build
1114
1124
  fails under 93 - because a vocabulary entry or a boundary
1115
1125
  condition can be weakened by an ordinary-looking refactor without a single test
1116
1126
  going red. [ADR-0007](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0007-mutation-testing.md) is straight about what
package/dist/glob.d.ts CHANGED
@@ -84,9 +84,10 @@ export declare function createGlobMatcher(patterns: readonly string[]): GlobMatc
84
84
  *
85
85
  * A target is text a document holds rather than a path on this host, and two
86
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.
87
+ * separator to allow for. And a `.` or `..` segment is text, in a brace
88
+ * alternative as anywhere else: in a path the dialect drops the one and
89
+ * refuses the other as climbing out of the root, but `../../notes/gone.md` is
90
+ * a link somebody wrote and may want left alone.
90
91
  */
91
92
  export declare function createReferenceFilter(patterns: readonly string[]): (target: string) => boolean;
92
93
  /**
package/dist/glob.js CHANGED
@@ -177,20 +177,29 @@ export function createGlobMatcher(patterns) {
177
177
  *
178
178
  * A target is text a document holds rather than a path on this host, and two
179
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.
180
+ * separator to allow for. And a `.` or `..` segment is text, in a brace
181
+ * alternative as anywhere else: in a path the dialect drops the one and
182
+ * refuses the other as climbing out of the root, but `../../notes/gone.md` is
183
+ * a link somebody wrote and may want left alone.
183
184
  */
184
185
  export function createReferenceFilter(patterns) {
185
186
  // A blank pattern reaches the dialect, which refuses it, as every pattern
186
187
  // spec-graph reads is refused when it names nothing. It used to be dropped,
187
188
  // so an unset variable in `--ignore-ref "$TAGS"` went unnoticed.
188
189
  const globs = patterns.map((pattern) => {
189
- const literalDots = pattern
190
- .trim()
191
- .split('/')
192
- .map((segment) => (segment === '.' ? '\\.' : segment === '..' ? '\\.\\.' : segment))
193
- .join('/');
190
+ // Each `.` or `..` that may stand as a segment is escaped, and the dialect
191
+ // reads an escaped dot as text. Braces expand before segments are read, so
192
+ // one written beside a brace or a comma may be a segment of an
193
+ // alternative: `docs/{.,x}` is `docs/.` or `docs/x`, `.{/gone.md,x}` is
194
+ // `./gone.md` or `.x`, and `{docs/,x/}.` ends in a segment `.` either way.
195
+ // Escaping one the braces leave inside a name, as in `.x` or `a{.,b}`, or
196
+ // beside a `,` or `}` that is itself text outside braces, reads as the same
197
+ // character, so nothing needs to tell those apart. No other dot is
198
+ // escaped: the advice for a `**` inside a name is written from the
199
+ // pattern, and `.github/**.md` is told `.github/**/*.md`. A mutant that
200
+ // counts dots from none matches the empty run between two boundaries too,
201
+ // and writes nothing for it: equivalent.
202
+ const literalDots = pattern.trim().replace(/(?<=^|[/{,}])\.{1,2}(?=$|[/{,}])/g, (dots) => '\\.'.repeat(dots.length));
194
203
  // Every option spelled out, though the dialect reads `{}` the same way
195
204
  // today: its defaults are spec-core's to change, and this reading is
196
205
  // spec-graph's to keep. A mutant that drops one is equivalent until then.
@@ -226,7 +235,8 @@ export function globBase(pattern) {
226
235
  return shared.slice(0, length).join('/');
227
236
  }
228
237
  /**
229
- * Whether a base names directories that exist with exactly that spelling.
238
+ * Whether a base names directories the walk from the root would reach, each
239
+ * with exactly that spelling and none a link it would not follow.
230
240
  *
231
241
  * A walk starts where a pattern's literal prefix points rather than at the
232
242
  * root, and on a filesystem that ignores case, `readdir('Docs')` lists `docs`.
@@ -234,15 +244,20 @@ export function globBase(pattern) {
234
244
  * baseline key - and `Docs/**` would find it on Windows and macOS and nothing
235
245
  * on Linux. Asking each parent for the name gives git's answer on every host. A
236
246
  * base with an empty segment is rooted at `/`, which is never inside the root.
247
+ *
248
+ * Reading a directory by its path follows every link in the path, so
249
+ * `docs/linked/*.md` was read through the link `docs/linked`, which the walk
250
+ * from the root passes over. Asking each parent for the entry says which it
251
+ * is, and a base beyond a link is left as silently as the walk leaves the link.
237
252
  */
238
- async function spelledAsOnDisk(root, base) {
253
+ async function reachable(root, base, followSymlinks) {
239
254
  if (base.length === 0)
240
255
  return true;
241
256
  let directory = root;
242
257
  for (const segment of base.split('/')) {
243
- let names;
258
+ let entries;
244
259
  try {
245
- names = await readdir(directory);
260
+ entries = await readdir(directory, { withFileTypes: true });
246
261
  }
247
262
  catch {
248
263
  // Answering `true` here would change nothing a test can see: the walk
@@ -250,7 +265,8 @@ async function spelledAsOnDisk(root, base) {
250
265
  // find nothing.
251
266
  return false;
252
267
  }
253
- if (!names.includes(segment))
268
+ const entry = entries.find((candidate) => candidate.name === segment);
269
+ if (entry === undefined || (entry.isSymbolicLink() && !followSymlinks))
254
270
  return false;
255
271
  directory = `${directory}/${segment}`;
256
272
  }
@@ -317,11 +333,31 @@ export async function walkFiles(options) {
317
333
  // prunes `vendor` and the rest of the default list on its way down, and a
318
334
  // pattern that starts inside one of those is read from there. Dropping the
319
335
  // inner base as already covered meant `docs/**/*.md` beside
320
- // `docs/vendor/specs/*.md` found less than the second pattern alone. A bare
321
- // `--ignore` name is not overruled that way: it is the user's, and prunes at
322
- // any depth, a pattern's own starting point included - so `--ignore adr`
323
- // still takes out `adr/`, which the default `adr/**/*.md` starts inside.
324
- const startable = (base) => !base.split('/').some((segment) => named.has(segment));
336
+ // `docs/vendor/specs/*.md` found less than the second pattern alone. An
337
+ // `--ignore` is not overruled that way: it is the user's. A walk that starts
338
+ // at a base never passed through the directories above it, so each of them,
339
+ // and the base, is asked what the walk from the root would have asked on its
340
+ // way down. A bare name prunes at any depth - `--ignore adr` still takes out
341
+ // `adr/`, which the default `adr/**/*.md` starts inside - and a path takes
342
+ // out what it matches, so under `--ignore "docs/*"` `docs/drafts/**/*.md`
343
+ // finds what `docs/**/*.md` finds, which is nothing there. The root is not
344
+ // asked, as the walk from it never asks: `*` matches the empty path, and
345
+ // `**/*.md` under `--ignore "*" --ignore "!{docs,specs}"` would read nothing
346
+ // where it reads those two.
347
+ const startable = (base) => {
348
+ if (base.length === 0)
349
+ return true;
350
+ const segments = base.split('/');
351
+ return !segments.some((segment, i) => named.has(segment) || excluded(segments.slice(0, i + 1).join('/')));
352
+ };
353
+ // Whether the walk leaves a directory it meets, asked of a followed link to
354
+ // one as well: a link called `node_modules`, which pnpm writes, or one an
355
+ // `--ignore` names, is the directory it leads to under that name and path,
356
+ // and was entered whatever they were. Pruning a whole subtree is an
357
+ // optimisation; a pattern that only matches the files inside it is still
358
+ // honoured when they are filtered. A bare `--ignore` name prunes a directory
359
+ // a pattern names as well: only the default list gives way.
360
+ const pruned = (name, path) => named.has(name) || (skippedByDefault.has(name) && !literals.has(path)) || excluded(path);
325
361
  const out = new Map();
326
362
  // Where two walks overlap, whichever reaches a directory first reads it and
327
363
  // the other stops there. Reading it twice would find the same files under
@@ -343,13 +379,8 @@ export async function walkFiles(options) {
343
379
  for (const entry of entries) {
344
380
  const child = relative.length === 0 ? entry.name : `${relative}/${entry.name}`;
345
381
  if (entry.isDirectory()) {
346
- // Pruning a whole subtree is an optimisation; a pattern that only
347
- // matches the files inside it is still honoured when they are filtered.
348
- // A bare `--ignore` name prunes a directory a pattern names as well:
349
- // only the default list gives way.
350
- if (named.has(entry.name) || (skippedByDefault.has(entry.name) && !literals.has(child)) || excluded(child))
351
- continue;
352
- await walk(child);
382
+ if (!pruned(entry.name, child))
383
+ await walk(child);
353
384
  continue;
354
385
  }
355
386
  if (entry.isSymbolicLink()) {
@@ -358,7 +389,8 @@ export async function walkFiles(options) {
358
389
  try {
359
390
  const info = await stat(`${root}/${child}`);
360
391
  if (info.isDirectory()) {
361
- await walk(child);
392
+ if (!pruned(entry.name, child))
393
+ await walk(child);
362
394
  continue;
363
395
  }
364
396
  if (!patterns.match(child) || excluded(child) || info.size > maxSize)
@@ -386,7 +418,7 @@ export async function walkFiles(options) {
386
418
  }
387
419
  };
388
420
  for (const base of bases)
389
- if (startable(base) && (await spelledAsOnDisk(root, base)))
421
+ if (startable(base) && (await reachable(root, base, options.followSymlinks)))
390
422
  await walk(base);
391
423
  // No two paths are equal, since they are the keys of `out`, so the order
392
424
  // needs only `<`, and `<=` would read the same.
package/dist/glob.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EACL,WAAW,IAAI,iBAAiB,EAChC,SAAS,EACT,YAAY,EACZ,SAAS,EACT,aAAa,GAKd,MAAM,qCAAqC,CAAC;AAE7C,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;;;;;;;GAOG;AACH,MAAM,IAAI,GAAsB,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC;AAEpH,kFAAkF;AAClF,MAAM,UAAU,MAAM,CAAC,OAAe;IACpC,OAAO,YAAY,CAAC,OAAO,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,OAAO,IAAI,MAAM,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AAChD,CAAC;AAOD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,OAAO,GAAgB,EAAE;IACpE,0EAA0E;IAC1E,6EAA6E;IAC7E,gEAAgE;IAChE,MAAM,IAAI,GAAG,iBAAiB,CAAC,OAAO,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC,CAAC;IAClH,OAAO,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;AACjF,CAAC;AAED,8EAA8E;AAC9E,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,qFAAqF;AACrF,SAAS,QAAQ,CAAC,QAA2B;IAC3C,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC7C,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IAChE,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAA2B;IAC3D,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAChC,OAAO,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAA2B;IAC/D,0EAA0E;IAC1E,4EAA4E;IAC5E,iEAAiE;IACjE,MAAM,KAAK,GAAW,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7C,MAAM,WAAW,GAAG,OAAO;aACxB,IAAI,EAAE;aACN,KAAK,CAAC,GAAG,CAAC;aACV,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;aACnF,IAAI,CAAC,GAAG,CAAC,CAAC;QACb,uEAAuE;QACvE,qEAAqE;QACrE,0EAA0E;QAC1E,MAAM,MAAM,GAAG,SAAS,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QAClG,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,MAAM,IAAI,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3D,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,MAAc,EAAW,EAAE;QACjC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;QAC5B,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,MAAM,EAAE,IAAI,EAAE,GAAG,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAA4B,CAAC;IAC3E,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAa,CAAC;IACpC,IAAI,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,uEAAuE;QACvE,yEAAyE;QACzE,OAAO,IAAI,GAAG,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,IAAI,CAAC;YAAE,IAAI,IAAI,CAAC,CAAC;QAC/D,MAAM,GAAG,IAAI,CAAC;IAChB,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;;;;GASG;AACH,KAAK,UAAU,eAAe,CAAC,IAAY,EAAE,IAAY;IACvD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,SAAS,GAAG,IAAI,CAAC;IACrB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,IAAI,KAAe,CAAC;QACpB,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,sEAAsE;YACtE,uEAAuE;YACvE,gBAAgB;YAChB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,KAAK,CAAC;QAC3C,SAAS,GAAG,GAAG,SAAS,IAAI,OAAO,EAAE,CAAC;IACxC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,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,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,OAAO,CAAC,WAAW,IAAI,aAAa,CAAC;IAErD,6EAA6E;IAC7E,yEAAyE;IACzE,sEAAsE;IACtE,8EAA8E;IAC9E,0EAA0E;IAC1E,8EAA8E;IAC9E,qEAAqE;IACrE,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;IACrC,6EAA6E;IAC7E,6EAA6E;IAC7E,uEAAuE;IACvE,MAAM,IAAI,GAAG,CAAC,OAAe,EAAW,EAAE,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC5G,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5C,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,2BAA2B,CAAC,CAAC;IAC9D,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAEnE,4EAA4E;IAC5E,6EAA6E;IAC7E,2EAA2E;IAC3E,0EAA0E;IAC1E,6EAA6E;IAC7E,uBAAuB;IACvB,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,8EAA8E;IAC9E,2EAA2E;IAC3E,4EAA4E;IAC5E,8EAA8E;IAC9E,2EAA2E;IAC3E,oEAAoE;IACpE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;QACrC,IAAI,KAAK,CAAC,OAAO;YAAE,SAAS;QAC5B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrD,yEAAyE;QACzE,0EAA0E;QAC1E,0EAA0E;QAC1E,iBAAiB;QACjB,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE;YAC3B,GAAG,IAAI;YACP,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;gBAChB,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACnB,OAAO,QAAQ,CAAC;YAClB,CAAC;SACF,CAAC,CAAC;IACL,CAAC;IACD,wEAAwE;IACxE,0EAA0E;IAC1E,2EAA2E;IAC3E,4DAA4D;IAC5D,4EAA4E;IAC5E,6EAA6E;IAC7E,yEAAyE;IACzE,yEAAyE;IACzE,MAAM,SAAS,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;IACpG,MAAM,GAAG,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC1C,4EAA4E;IAC5E,0EAA0E;IAC1E,mEAAmE;IACnE,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,qEAAqE;gBACrE,mCAAmC;gBACnC,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC;oBAAE,SAAS;gBACrH,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,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,GAAG,OAAO;wBAAE,SAAS;oBAC/E,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,yCAAyC;gBAC3C,CAAC;gBACD,SAAS;YACX,CAAC;YAED,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;gBAAE,SAAS;YAC9B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC;gBAAE,SAAS;YAExD,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,mEAAmE;YACrE,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;IAEvG,yEAAyE;IACzE,gDAAgD;IAChD,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,CAAC,CAAC;AACtE,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 * 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 // A mutant that defaults this to `['Stryker was here']` prunes directories of\n // that name, which no repository has: equivalent, and left untested.\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 named = new Set(ignores.filter(bare));\n const skippedByDefault = new Set(DEFAULT_IGNORED_DIRECTORIES);\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: it\n // takes files back, and starting a walk at its prefix would enter what the\n // walk skips - `!node_modules/**/*.txt` beside `**/*.md` would read every\n // Markdown file in `node_modules`. No positive pattern at all walks nothing,\n // which is the answer.\n const bases = new Set<string>();\n // The paths a positive pattern names outright: `docs/vendor`, or each literal\n // a brace expands to. Such a name may be a file, so its walk starts in the\n // directory above, which pruned `vendor` on its way in: `docs/vendor` found\n // nothing where `docs/vendor/` read the directory. The default list gives way\n // to the directory a pattern names as it gives way to one a pattern starts\n // inside. A negated name adds none, for the reason it adds no base.\n const literals = 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 // The dialect is what knows a literal when it reads one, so each pattern\n // is compiled a second time to be told of them. That glob is thrown away:\n // the reading answered here decides nothing, and a mutant that changes it\n // is equivalent.\n parseGlob(entry.glob.source, {\n ...PATH,\n literal: (path) => {\n literals.add(path);\n return 'either';\n },\n });\n }\n // Every base is walked, even one inside another, because the outer walk\n // prunes `vendor` and the rest of the default list on its way down, and a\n // pattern that starts inside one of those is read from there. Dropping the\n // inner base as already covered meant `docs/**/*.md` beside\n // `docs/vendor/specs/*.md` found less than the second pattern alone. A bare\n // `--ignore` name is not overruled that way: it is the user's, and prunes at\n // any depth, a pattern's own starting point included - so `--ignore adr`\n // still takes out `adr/`, which the default `adr/**/*.md` starts inside.\n const startable = (base: string): boolean => !base.split('/').some((segment) => named.has(segment));\n const out = new Map<string, WalkedFile>();\n // Where two walks overlap, whichever reaches a directory first reads it and\n // the other stops there. Reading it twice would find the same files under\n // the same keys, so the two mutants that drop this are equivalent.\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 // A bare `--ignore` name prunes a directory a pattern names as well:\n // only the default list gives way.\n if (named.has(entry.name) || (skippedByDefault.has(entry.name) && !literals.has(child)) || 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 // A link to nothing has nothing to read.\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 // Gone between the listing and the look: there is nothing to read.\n }\n }\n };\n\n for (const base of bases) if (startable(base) && (await spelledAsOnDisk(root, base))) await walk(base);\n\n // No two paths are equal, since they are the keys of `out`, so the order\n // needs only `<`, and `<=` would read the same.\n return [...out.values()].sort((a, b) => (a.path < b.path ? -1 : 1));\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 type { Dirent } from 'node:fs';\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 brace\n * alternative as anywhere else: in a path the dialect drops the one and\n * refuses the other as climbing out of the root, but `../../notes/gone.md` is\n * 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 // Each `.` or `..` that may stand as a segment is escaped, and the dialect\n // reads an escaped dot as text. Braces expand before segments are read, so\n // one written beside a brace or a comma may be a segment of an\n // alternative: `docs/{.,x}` is `docs/.` or `docs/x`, `.{/gone.md,x}` is\n // `./gone.md` or `.x`, and `{docs/,x/}.` ends in a segment `.` either way.\n // Escaping one the braces leave inside a name, as in `.x` or `a{.,b}`, or\n // beside a `,` or `}` that is itself text outside braces, reads as the same\n // character, so nothing needs to tell those apart. No other dot is\n // escaped: the advice for a `**` inside a name is written from the\n // pattern, and `.github/**.md` is told `.github/**/*.md`. A mutant that\n // counts dots from none matches the empty run between two boundaries too,\n // and writes nothing for it: equivalent.\n const literalDots = pattern.trim().replace(/(?<=^|[/{,}])\\.{1,2}(?=$|[/{,}])/g, (dots) => '\\\\.'.repeat(dots.length));\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 the walk from the root would reach, each\n * with exactly that spelling and none a link it would not follow.\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 *\n * Reading a directory by its path follows every link in the path, so\n * `docs/linked/*.md` was read through the link `docs/linked`, which the walk\n * from the root passes over. Asking each parent for the entry says which it\n * is, and a base beyond a link is left as silently as the walk leaves the link.\n */\nasync function reachable(root: string, base: string, followSymlinks: boolean | undefined): Promise<boolean> {\n if (base.length === 0) return true;\n let directory = root;\n for (const segment of base.split('/')) {\n let entries: Dirent[];\n try {\n entries = await readdir(directory, { withFileTypes: true });\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 const entry = entries.find((candidate) => candidate.name === segment);\n if (entry === undefined || (entry.isSymbolicLink() && !followSymlinks)) 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 // A mutant that defaults this to `['Stryker was here']` prunes directories of\n // that name, which no repository has: equivalent, and left untested.\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 named = new Set(ignores.filter(bare));\n const skippedByDefault = new Set(DEFAULT_IGNORED_DIRECTORIES);\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: it\n // takes files back, and starting a walk at its prefix would enter what the\n // walk skips - `!node_modules/**/*.txt` beside `**/*.md` would read every\n // Markdown file in `node_modules`. No positive pattern at all walks nothing,\n // which is the answer.\n const bases = new Set<string>();\n // The paths a positive pattern names outright: `docs/vendor`, or each literal\n // a brace expands to. Such a name may be a file, so its walk starts in the\n // directory above, which pruned `vendor` on its way in: `docs/vendor` found\n // nothing where `docs/vendor/` read the directory. The default list gives way\n // to the directory a pattern names as it gives way to one a pattern starts\n // inside. A negated name adds none, for the reason it adds no base.\n const literals = 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 // The dialect is what knows a literal when it reads one, so each pattern\n // is compiled a second time to be told of them. That glob is thrown away:\n // the reading answered here decides nothing, and a mutant that changes it\n // is equivalent.\n parseGlob(entry.glob.source, {\n ...PATH,\n literal: (path) => {\n literals.add(path);\n return 'either';\n },\n });\n }\n // Every base is walked, even one inside another, because the outer walk\n // prunes `vendor` and the rest of the default list on its way down, and a\n // pattern that starts inside one of those is read from there. Dropping the\n // inner base as already covered meant `docs/**/*.md` beside\n // `docs/vendor/specs/*.md` found less than the second pattern alone. An\n // `--ignore` is not overruled that way: it is the user's. A walk that starts\n // at a base never passed through the directories above it, so each of them,\n // and the base, is asked what the walk from the root would have asked on its\n // way down. A bare name prunes at any depth - `--ignore adr` still takes out\n // `adr/`, which the default `adr/**/*.md` starts inside - and a path takes\n // out what it matches, so under `--ignore \"docs/*\"` `docs/drafts/**/*.md`\n // finds what `docs/**/*.md` finds, which is nothing there. The root is not\n // asked, as the walk from it never asks: `*` matches the empty path, and\n // `**/*.md` under `--ignore \"*\" --ignore \"!{docs,specs}\"` would read nothing\n // where it reads those two.\n const startable = (base: string): boolean => {\n if (base.length === 0) return true;\n const segments = base.split('/');\n return !segments.some((segment, i) => named.has(segment) || excluded(segments.slice(0, i + 1).join('/')));\n };\n // Whether the walk leaves a directory it meets, asked of a followed link to\n // one as well: a link called `node_modules`, which pnpm writes, or one an\n // `--ignore` names, is the directory it leads to under that name and path,\n // and was entered whatever they were. Pruning a whole subtree is an\n // optimisation; a pattern that only matches the files inside it is still\n // honoured when they are filtered. A bare `--ignore` name prunes a directory\n // a pattern names as well: only the default list gives way.\n const pruned = (name: string, path: string): boolean =>\n named.has(name) || (skippedByDefault.has(name) && !literals.has(path)) || excluded(path);\n const out = new Map<string, WalkedFile>();\n // Where two walks overlap, whichever reaches a directory first reads it and\n // the other stops there. Reading it twice would find the same files under\n // the same keys, so the two mutants that drop this are equivalent.\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 if (!pruned(entry.name, child)) 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 if (!pruned(entry.name, child)) 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 // A link to nothing has nothing to read.\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 // Gone between the listing and the look: there is nothing to read.\n }\n }\n };\n\n for (const base of bases) if (startable(base) && (await reachable(root, base, options.followSymlinks))) await walk(base);\n\n // No two paths are equal, since they are the keys of `out`, so the order\n // needs only `<`, and `<=` would read the same.\n return [...out.values()].sort((a, b) => (a.path < b.path ? -1 : 1));\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descent-vtt/spec-graph",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
4
4
  "description": "Turn Markdown specs into a verifiable relational graph. Catches ghost handovers, stale premises and broken references in CI.",
5
5
  "keywords": [
6
6
  "adr",