@descent-vtt/spec-brief 0.2.4 → 0.2.6
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 +66 -0
- package/README.md +2 -2
- package/dist/glob.d.ts +29 -2
- package/dist/glob.js +67 -12
- package/dist/glob.js.map +1 -1
- package/dist/rules.js +37 -10
- package/dist/rules.js.map +1 -1
- package/dist/vendor/spec-core/markdown/links.js +15 -2
- package/dist/vendor/spec-core/markdown/links.js.map +1 -1
- package/dist/vendor/spec-core/markdown/tables.js +3 -0
- package/dist/vendor/spec-core/markdown/tables.js.map +1 -1
- package/dist/vendor/spec-core/pattern/automaton.d.ts +15 -2
- package/dist/vendor/spec-core/pattern/automaton.js +21 -2
- package/dist/vendor/spec-core/pattern/automaton.js.map +1 -1
- package/dist/vendor/spec-core/pattern/charset.d.ts +5 -1
- package/dist/vendor/spec-core/pattern/charset.js +8 -1
- package/dist/vendor/spec-core/pattern/charset.js.map +1 -1
- package/dist/vendor/spec-core/pattern/glob.d.ts +16 -3
- package/dist/vendor/spec-core/pattern/glob.js +78 -18
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -1
- package/dist/vendor/spec-core/text/lines.js +10 -0
- package/dist/vendor/spec-core/text/lines.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,72 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes, newest first. Versions follow [semver](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 0.2.6
|
|
6
|
+
|
|
7
|
+
spec-core at 56c7e54: a brace alternative that names no path, such as
|
|
8
|
+
`{./,src}`, is refused as the same text alone is, where it read as every
|
|
9
|
+
path. `literal-read-as-file` advises the `/` on the brace alternative it
|
|
10
|
+
names, built from the pattern written.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- spec-core at 56c7e54. A brace alternative that names no path is refused,
|
|
15
|
+
as the same text written alone is. `{./,src}` in `affectedFiles` or
|
|
16
|
+
`protectedFiles` read its `./` as every path, where `./` alone is
|
|
17
|
+
refused: protecting it protected the whole tree, so `scope-contradiction`
|
|
18
|
+
reported every affected pattern, and in `affectedFiles` it overlapped
|
|
19
|
+
every other brief's scope. It is now a `glob` error, `the braces expand
|
|
20
|
+
to "./", which names no path`, and so are `{src,./}`, `{.,src}/` and
|
|
21
|
+
`.{/,src}`. A `files` or `exclude` pattern spelt so matched every name in
|
|
22
|
+
the briefs directory; it is now a configuration problem, and the run
|
|
23
|
+
exits 2. `src/{./,a}`, `{./a,b}` and `a{,.ts}` read as they did.
|
|
24
|
+
- `glob` names the alternative in refusals it already made: `{.,src}` is
|
|
25
|
+
`the braces expand to ".", which names no path`, and `{/,src}` the same
|
|
26
|
+
with `"/"`, where both were `the pattern names no path`; `{,src}`,
|
|
27
|
+
`{src,}` and `{}` are `the braces expand to an empty pattern`.
|
|
28
|
+
- `literal-read-as-file` advises the pattern as written with a `/` ending
|
|
29
|
+
each alternative it names, where it said to write a directory with a
|
|
30
|
+
trailing `/`, in an entry of its own. Since 0.2.5 a trailing `/` on a
|
|
31
|
+
brace alternative says directory, so for `lib/{util,new}` in a tree
|
|
32
|
+
without `lib/new` the hint is `write "lib/{util,new/}" for a directory,
|
|
33
|
+
or "lib/new/" in an entry of its own`. Where more of the pattern follows
|
|
34
|
+
the braces, `{a,b}/new`, a `/` inside them cannot end one alternative
|
|
35
|
+
alone, and the hint names the entries: `write "a/new/" and "b/new/" for
|
|
36
|
+
directories, in entries of their own`. What the note fires on is
|
|
37
|
+
unchanged.
|
|
38
|
+
|
|
39
|
+
## 0.2.5
|
|
40
|
+
|
|
41
|
+
spec-core at f9ce375. A trailing `/` on a brace alternative means that
|
|
42
|
+
directory's contents, as one written alone does: `{src/newmod/,lib}` scopes
|
|
43
|
+
what `src/newmod/` and `lib` scope, so a collision inside a directory the
|
|
44
|
+
round creates is no longer missed.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- spec-core at f9ce375. Its glob reads a trailing `/` on a brace
|
|
49
|
+
alternative as it reads one on the whole pattern. In a scope,
|
|
50
|
+
`{src/newmod/,lib}` covers `src/newmod/index.ts` and not `src/newmod`,
|
|
51
|
+
whatever the tree holds. `src/newmod` was a literal read from the tree,
|
|
52
|
+
and while the tree did not hold the directory, or with no tree, it
|
|
53
|
+
covered only a file of that name: `matrix` missed a collision with a
|
|
54
|
+
brief writing `src/newmod/index.ts`, which it now reports, and `schedule`
|
|
55
|
+
now keeps the two briefs apart. A directory the tree holds reads as
|
|
56
|
+
before.
|
|
57
|
+
- `literal-read-as-file` no longer names what a trailing `/` says is a
|
|
58
|
+
directory: `{src/newmod/,lib}` in a tree holding neither names `lib`
|
|
59
|
+
alone, where it named `src/newmod` and `lib`. A pattern left with no
|
|
60
|
+
bare name to place, `{src/newmod/,docs/new.md}` in a tree holding
|
|
61
|
+
neither, is `glob-matches-nothing`'s, as `src/newmod/` alone is, where it
|
|
62
|
+
was noted for `src/newmod`.
|
|
63
|
+
- `archive`, which reads a literal as either, no longer takes a changed file
|
|
64
|
+
at `build` itself as inside `{build/,Makefile}`, protected or in scope, as
|
|
65
|
+
it never took one inside `build/`.
|
|
66
|
+
- In the library, `parseGlob(...).glob.literals` leaves out an alternative
|
|
67
|
+
written with a trailing `/`, and the base `globBases` gives for
|
|
68
|
+
`{src/newmod/,lib}` is `src/newmod` whatever the tree holds, where it was
|
|
69
|
+
`src` while the tree did not hold the directory.
|
|
70
|
+
|
|
5
71
|
## 0.2.4
|
|
6
72
|
|
|
7
73
|
spec-core at 65ef842. Archival rewrites a link reference definition only
|
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Work put off for later is a brief too, with `status: deferred` and a `trigger` n
|
|
|
52
52
|
|
|
53
53
|
The id comes from the file name (`012_rotate-session-tokens.md`) or from an `id` field. `affectedFiles` is the scope the round may write; `protectedFiles` is what it is not empowered to change, and archival refuses a round that changed it. Protection wins: what a round may write is `affectedFiles` less `protectedFiles`, so "all of `src/` but the schema" is `affectedFiles: [src/**]` with `protectedFiles: [src/db/schema.ts]`.
|
|
54
54
|
|
|
55
|
-
Scopes are globs in spec-core's `path` dialect, the one every spec-\* tool reads: `*`, `?`, `[a-z]` and `{a,b}` within a name, `**` for any number of directories, compared case-sensitively on every host. `**` is a whole segment: `docs/**.md` is refused, since tools read it three ways - write `docs/**/*.md` for any depth, or `docs/*.md` for one level. Parentheses are literal, `C++(notes).md`, unless a group holds a `|`: an extended glob such as `+(a|b)` is refused, and written `{a,b}`. A trailing `/**` or `/` is a directory's contents - at least one name below it, never the directory itself
|
|
55
|
+
Scopes are globs in spec-core's `path` dialect, the one every spec-\* tool reads: `*`, `?`, `[a-z]` and `{a,b}` within a name, `**` for any number of directories, compared case-sensitively on every host. `**` is a whole segment: `docs/**.md` is refused, since tools read it three ways - write `docs/**/*.md` for any depth, or `docs/*.md` for one level. Parentheses are literal, `C++(notes).md`, unless a group holds a `|`: an extended glob such as `+(a|b)` is refused, and written `{a,b}`. A trailing `/**` or `/` is a directory's contents - at least one name below it, never the directory itself - on a brace alternative as on the whole pattern: `{src/,lib}` is `src/` or `lib`. Each alternative names a path under the root, as a whole pattern does: `./` is refused, and so is `{./,src}`. A path with no glob syntax is read from the tree: a file the tree holds is that file, a directory it holds is everything beneath it, and a path it does not hold yet is a file unless it ends in `/`. Write `src/newmod/` for a directory the round creates; `lint` notes a bare `src/newmod` it cannot place.
|
|
56
56
|
|
|
57
57
|
## Commands
|
|
58
58
|
|
|
@@ -229,7 +229,7 @@ A brief is read as CommonMark reads it wherever that decides what is code or a c
|
|
|
229
229
|
| `glob` | error | A scope pattern spec-brief cannot read. |
|
|
230
230
|
| `scope-contradiction` | error | Patterns in `affectedFiles` that `protectedFiles` cover entirely, so nothing of them is writable - one finding per brief, naming each. A pattern the search cannot decide is a warning. |
|
|
231
231
|
| `glob-matches-nothing` | note | A scope pattern that matches no file git sees, tracked or untracked - expected when the round creates it. |
|
|
232
|
-
| `literal-read-as-file` | note | A path with no glob syntax that the tree does not hold and whose name has no extension, such as `src/newmod`: read as a file, and `src/newmod/` if a directory was meant. |
|
|
232
|
+
| `literal-read-as-file` | note | A path with no glob syntax that the tree does not hold and whose name has no extension, such as `src/newmod`: read as a file, and `src/newmod/` if a directory was meant. Inside braces the advice is the pattern written with the `/` added, `lib/{util,new/}`, or the alternative in an entry of its own, `lib/new/`. |
|
|
233
233
|
| `archive-freeze` | error | An archived brief that changed after it was archived. |
|
|
234
234
|
| `collision` | error | Two briefs in one wave whose scopes can name the same file (`matrix`). |
|
|
235
235
|
| `collision-undecided` | warning | Two briefs in one wave whose collision the search could not decide within its budget (`matrix`), or a wave `schedule` passed over for it. |
|
package/dist/glob.d.ts
CHANGED
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* The tree, not the spelling, knows that `Dockerfile` is a file and
|
|
18
18
|
* `docs/v1.2` a directory (ADR-0005, amended 2026-09-26).
|
|
19
19
|
* - A trailing `/**` is at least one segment, so `docs/**` is the directory's
|
|
20
|
-
* contents and a collision witness is always a file.
|
|
20
|
+
* contents and a collision witness is always a file. A trailing `/` means
|
|
21
|
+
* the same on a brace alternative as on the whole pattern: `{src/,lib}` is
|
|
22
|
+
* `src/` or `lib`, and the tree is asked about `lib` alone.
|
|
21
23
|
*
|
|
22
24
|
* Nothing here compiles to a `RegExp`. Matching costs at most the pattern's
|
|
23
25
|
* size times the path's length, and a question about two scopes that cannot be
|
|
@@ -34,7 +36,10 @@ export interface Glob {
|
|
|
34
36
|
readonly source: string;
|
|
35
37
|
/** The compiled pattern: spec-core's `path` dialect, case-sensitive. */
|
|
36
38
|
readonly compiled: CoreGlob;
|
|
37
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Each path the pattern names with no glob syntax and no trailing `/`, one
|
|
41
|
+
* per brace alternative; empty when it has none.
|
|
42
|
+
*/
|
|
38
43
|
readonly literals: readonly Literal[];
|
|
39
44
|
}
|
|
40
45
|
export type GlobParse = {
|
|
@@ -82,6 +87,28 @@ export declare function intersectGlobs(a: Glob, b: Glob, budget?: number): strin
|
|
|
82
87
|
export declare function globBases(glob: Glob): readonly string[];
|
|
83
88
|
/** The first of {@link globBases}: the 0.1 interface, kept for the library's callers. */
|
|
84
89
|
export declare function globBase(glob: Glob): string;
|
|
90
|
+
/** A brace alternative as its pattern writes it, and the index in the pattern where it ends. */
|
|
91
|
+
export interface WrittenAlternative {
|
|
92
|
+
readonly written: string;
|
|
93
|
+
readonly end: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Each alternative of a pattern as written, in the order the core expands
|
|
97
|
+
* them, and where each ends, when a `/` written there ends that alternative
|
|
98
|
+
* and no other: the pattern itself when it has no braces, or the alternatives
|
|
99
|
+
* of braces that end the pattern and whose own braces end theirs -
|
|
100
|
+
* `lib/{util,new}`, `{a,b/{c,d}}`. `null` when more of the pattern follows
|
|
101
|
+
* braces, `{a,b}/new`, since every alternative they give ends where the
|
|
102
|
+
* pattern does; and for a pattern with a class, which may hold a brace or a
|
|
103
|
+
* comma of its own and which this does not read.
|
|
104
|
+
*
|
|
105
|
+
* spec-core expands braces and keeps nothing of where an alternative was
|
|
106
|
+
* written, and a note's advice is built from what was written, as the core
|
|
107
|
+
* builds its own. So this reads braces as the expansion does, for a pattern
|
|
108
|
+
* that parses: a `\` escapes the next character, and a `}` with no `{` open
|
|
109
|
+
* is a literal.
|
|
110
|
+
*/
|
|
111
|
+
export declare function writtenAlternatives(pattern: string): WrittenAlternative[] | null;
|
|
85
112
|
/** A name with an extension, `login.ts` or `.eslintrc.json`, and not a dot-name such as `.github`. */
|
|
86
113
|
export declare function hasExtension(name: string): boolean;
|
|
87
114
|
/** The files a tree holds, and every directory above one. */
|
package/dist/glob.js
CHANGED
|
@@ -17,13 +17,15 @@
|
|
|
17
17
|
* The tree, not the spelling, knows that `Dockerfile` is a file and
|
|
18
18
|
* `docs/v1.2` a directory (ADR-0005, amended 2026-09-26).
|
|
19
19
|
* - A trailing `/**` is at least one segment, so `docs/**` is the directory's
|
|
20
|
-
* contents and a collision witness is always a file.
|
|
20
|
+
* contents and a collision witness is always a file. A trailing `/` means
|
|
21
|
+
* the same on a brace alternative as on the whole pattern: `{src/,lib}` is
|
|
22
|
+
* `src/` or `lib`, and the tree is asked about `lib` alone.
|
|
21
23
|
*
|
|
22
24
|
* Nothing here compiles to a `RegExp`. Matching costs at most the pattern's
|
|
23
25
|
* size times the path's length, and a question about two scopes that cannot be
|
|
24
26
|
* answered within the search's budget answers `undecided`, never a guess.
|
|
25
27
|
*/
|
|
26
|
-
import {
|
|
28
|
+
import { globCovers as coreCovers, globWitness as coreWitness, isGlobSyntax, parseGlob as coreParse, WITNESS_BUDGET, } from './vendor/spec-core/pattern/index.js';
|
|
27
29
|
import { segments } from './vendor/spec-core/path/index.js';
|
|
28
30
|
export { isGlobSyntax, WITNESS_BUDGET };
|
|
29
31
|
/** Parses a scope pattern, or says why it cannot. */
|
|
@@ -40,16 +42,10 @@ export function parseGlob(source, options = {}) {
|
|
|
40
42
|
literals.push({ path, reading });
|
|
41
43
|
return reading;
|
|
42
44
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
// Too many states is a pattern nobody meant; anything else is a defect to surface.
|
|
49
|
-
if (error instanceof AutomatonTooLarge)
|
|
50
|
-
return { ok: false, error: error.message };
|
|
51
|
-
throw error;
|
|
52
|
-
}
|
|
45
|
+
// A pattern too large to compile is refused by the core, with its reason,
|
|
46
|
+
// as a malformed one is; what the reading throws is a defect, and passes on.
|
|
47
|
+
const parsed = coreParse(pattern, { dialect: 'path', caseSensitive: true, literal });
|
|
48
|
+
return parsed.ok ? { ok: true, glob: { source, compiled: parsed.glob, literals } } : parsed;
|
|
53
49
|
}
|
|
54
50
|
/**
|
|
55
51
|
* Whether a path matches. The path is read as a repository path: empty and
|
|
@@ -96,6 +92,65 @@ export function globBase(glob) {
|
|
|
96
92
|
// One base per alternative, and a pattern has at least one.
|
|
97
93
|
return glob.compiled.bases[0];
|
|
98
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Each alternative of a pattern as written, in the order the core expands
|
|
97
|
+
* them, and where each ends, when a `/` written there ends that alternative
|
|
98
|
+
* and no other: the pattern itself when it has no braces, or the alternatives
|
|
99
|
+
* of braces that end the pattern and whose own braces end theirs -
|
|
100
|
+
* `lib/{util,new}`, `{a,b/{c,d}}`. `null` when more of the pattern follows
|
|
101
|
+
* braces, `{a,b}/new`, since every alternative they give ends where the
|
|
102
|
+
* pattern does; and for a pattern with a class, which may hold a brace or a
|
|
103
|
+
* comma of its own and which this does not read.
|
|
104
|
+
*
|
|
105
|
+
* spec-core expands braces and keeps nothing of where an alternative was
|
|
106
|
+
* written, and a note's advice is built from what was written, as the core
|
|
107
|
+
* builds its own. So this reads braces as the expansion does, for a pattern
|
|
108
|
+
* that parses: a `\` escapes the next character, and a `}` with no `{` open
|
|
109
|
+
* is a literal.
|
|
110
|
+
*/
|
|
111
|
+
export function writtenAlternatives(pattern) {
|
|
112
|
+
let depth = 0;
|
|
113
|
+
let open = 0;
|
|
114
|
+
const stops = [];
|
|
115
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
116
|
+
const ch = pattern.charAt(i);
|
|
117
|
+
if (ch === '\\') {
|
|
118
|
+
i += 1;
|
|
119
|
+
}
|
|
120
|
+
else if (ch === '[') {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
else if (ch === '{') {
|
|
124
|
+
if (depth === 0)
|
|
125
|
+
open = i;
|
|
126
|
+
depth += 1;
|
|
127
|
+
}
|
|
128
|
+
else if (ch === ',' && depth === 1) {
|
|
129
|
+
stops.push(i);
|
|
130
|
+
}
|
|
131
|
+
else if (ch === '}' && depth > 0) {
|
|
132
|
+
depth -= 1;
|
|
133
|
+
if (depth === 0)
|
|
134
|
+
return i === pattern.length - 1 ? optionsOf(pattern, open, [...stops, i]) : null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [{ written: pattern, end: pattern.length }];
|
|
138
|
+
}
|
|
139
|
+
/** The alternatives of the braces opened at `open`, each option ending at one of `stops`. */
|
|
140
|
+
function optionsOf(pattern, open, stops) {
|
|
141
|
+
const prefix = pattern.slice(0, open);
|
|
142
|
+
const alternatives = [];
|
|
143
|
+
let start = open + 1;
|
|
144
|
+
for (const stop of stops) {
|
|
145
|
+
const inner = writtenAlternatives(pattern.slice(start, stop));
|
|
146
|
+
if (inner === null)
|
|
147
|
+
return null;
|
|
148
|
+
for (const { written, end } of inner)
|
|
149
|
+
alternatives.push({ written: prefix + written, end: start + end });
|
|
150
|
+
start = stop + 1;
|
|
151
|
+
}
|
|
152
|
+
return alternatives;
|
|
153
|
+
}
|
|
99
154
|
/** A name with an extension, `login.ts` or `.eslintrc.json`, and not a dot-name such as `.github`. */
|
|
100
155
|
export function hasExtension(name) {
|
|
101
156
|
return /.\.[^.]+$/.test(name);
|
package/dist/glob.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EACL,iBAAiB,EACjB,UAAU,IAAI,UAAU,EACxB,WAAW,IAAI,WAAW,EAC1B,YAAY,EACZ,SAAS,IAAI,SAAS,EAGtB,cAAc,GAEf,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAE5D,OAAO,EAAE,YAAY,EAAuB,cAAc,EAAgB,CAAC;AA4B3E,qDAAqD;AACrD,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,OAAO,GAAiB,EAAE;IAClE,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,iEAAiE,EAAE,CAAC;IAC5H,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,wEAAwE,EAAE,CAAC;IACnI,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,IAAI,MAAM,CAAC;IACxC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,OAAO,GAAG,CAAC,IAAY,EAAkB,EAAE;QAC/C,MAAM,OAAO,GAAG,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACjC,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IACF,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACrF,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;IAC9F,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,mFAAmF;QACnF,IAAI,KAAK,YAAY,iBAAiB;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;QACnF,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAU,EAAE,IAAY;IAChD,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3C,OAAO,SAAS,KAAK,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,OAAwB,EAAE,OAAO,GAAoB,EAAE,EAAE,MAAM,GAAW,cAAc;IAClH,OAAO,WAAW,CAChB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAC9B,MAAM,CACP,CAAC;AACJ,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,UAAU,CAAC,KAAsB,EAAE,KAAW,EAAE,MAAM,GAAW,cAAc;IAC7F,OAAO,UAAU,CACf,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAC5B,KAAK,CAAC,QAAQ,EACd,MAAM,CACP,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,CAAO,EAAE,CAAO,EAAE,MAAM,GAAW,cAAc;IAC9E,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;IAChD,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,MAAM,UAAU,CAAC,CAAC,MAAM,gDAAgD,CAAC,CAAC;IAC1G,CAAC;IACD,OAAO,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AACxD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,IAAU;IAClC,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;AAC7B,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,QAAQ,CAAC,IAAU;IACjC,4DAA4D;IAC5D,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAW,CAAC;AAC1C,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC;AAQD,MAAM,KAAK,GAAG,IAAI,OAAO,EAA2B,CAAC;AAErD,kGAAkG;AAClG,MAAM,UAAU,MAAM,CAAC,KAAwB;IAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC/B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,IAAI,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC;YAAE,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IAChI,CAAC;IACD,MAAM,IAAI,GAAG,EAAE,KAAK,EAAE,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;IACpD,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACvB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,IAAI,CAAC,IAAU,EAAE,IAAY;IAC3C,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,MAAM,CAAC;IACxC,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;AACzD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,KAA+B;IACvD,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3B,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAC7E,CAAC","sourcesContent":["/**\n * Scopes as globs: spec-core's `path` dialect, read the way spec-brief reads a\n * brief.\n *\n * The syntax, the automaton and the witness search are spec-core's, copied\n * into `src/vendor/` and verified by hash, so the family's tools read one\n * pattern the same way (spec-core ADR-0003). What is decided here is what\n * spec-brief adds around it:\n *\n * - A pattern is relative to the repository root, so a leading `/` is refused\n * rather than rooted at the filesystem; and a list entry's `!` has no list to\n * belong to in a scope, so it is refused rather than read.\n * - Paths compare case-sensitively on every host, as git's do.\n * - A path with no glob syntax is read as the tree reads it: a file the tree\n * holds is that file, a directory it holds is everything beneath it, and a\n * path it does not hold is a file, unless it is written with a trailing `/`.\n * The tree, not the spelling, knows that `Dockerfile` is a file and\n * `docs/v1.2` a directory (ADR-0005, amended 2026-09-26).\n * - A trailing `/**` is at least one segment, so `docs/**` is the directory's\n * contents and a collision witness is always a file.\n *\n * Nothing here compiles to a `RegExp`. Matching costs at most the pattern's\n * size times the path's length, and a question about two scopes that cannot be\n * answered within the search's budget answers `undecided`, never a guess.\n */\n\nimport {\n AutomatonTooLarge,\n globCovers as coreCovers,\n globWitness as coreWitness,\n isGlobSyntax,\n parseGlob as coreParse,\n type Glob as CoreGlob,\n type LiteralReading,\n WITNESS_BUDGET,\n type Witness,\n} from './vendor/spec-core/pattern/index.js';\nimport { segments } from './vendor/spec-core/path/index.js';\n\nexport { isGlobSyntax, type LiteralReading, WITNESS_BUDGET, type Witness };\n\n/** A literal path a pattern names, and how it was read. */\nexport interface Literal {\n readonly path: string;\n readonly reading: LiteralReading;\n}\n\nexport interface Glob {\n readonly source: string;\n /** The compiled pattern: spec-core's `path` dialect, case-sensitive. */\n readonly compiled: CoreGlob;\n /** Each path the pattern names with no glob syntax, one per brace alternative; empty when it has none. */\n readonly literals: readonly Literal[];\n}\n\nexport type GlobParse = { readonly ok: true; readonly glob: Glob } | { readonly ok: false; readonly error: string };\n\nexport interface ParseOptions {\n /**\n * How a path with no glob syntax is read: `file`, that path alone;\n * `directory`, everything beneath it; `either`, both. A function is asked\n * per literal path, as {@link readingIn} answers for a tree. `file` when\n * unset: a path nothing says is a directory is not read as one.\n */\n readonly literal?: LiteralReading | ((path: string) => LiteralReading) | undefined;\n}\n\n/** Parses a scope pattern, or says why it cannot. */\nexport function parseGlob(source: string, options: ParseOptions = {}): GlobParse {\n const pattern = source.trim();\n if (pattern.startsWith('!')) return { ok: false, error: 'negated patterns are not supported; narrow the positive pattern' };\n if (pattern.startsWith('/')) return { ok: false, error: 'a pattern is relative to the repository root and cannot start with \"/\"' };\n const given = options.literal ?? 'file';\n const literals: Literal[] = [];\n const literal = (path: string): LiteralReading => {\n const reading = typeof given === 'function' ? given(path) : given;\n literals.push({ path, reading });\n return reading;\n };\n try {\n const parsed = coreParse(pattern, { dialect: 'path', caseSensitive: true, literal });\n return parsed.ok ? { ok: true, glob: { source, compiled: parsed.glob, literals } } : parsed;\n } catch (error) {\n // Too many states is a pattern nobody meant; anything else is a defect to surface.\n if (error instanceof AutomatonTooLarge) return { ok: false, error: error.message };\n throw error;\n }\n}\n\n/**\n * Whether a path matches. The path is read as a repository path: empty and\n * `.` segments are dropped, so `./src//a.ts` is `src/a.ts`.\n */\nexport function matchGlob(glob: Glob, path: string): boolean {\n const canonical = segments(path).join('/');\n return canonical !== '' && glob.compiled.match(canonical);\n}\n\n/**\n * A shortest path every glob in `include` matches and none in `exclude` does,\n * `none` as a proof that there is no such path, or `undecided` when the search\n * met its budget first.\n */\nexport function globWitness(include: readonly Glob[], exclude: readonly Glob[] = [], budget: number = WITNESS_BUDGET): Witness {\n return coreWitness(\n include.map((g) => g.compiled),\n exclude.map((g) => g.compiled),\n budget,\n );\n}\n\n/** Whether every path `inner` matches is matched by one of `outer`, or `undecided`. */\nexport function globCovers(outer: readonly Glob[], inner: Glob, budget: number = WITNESS_BUDGET): boolean | 'undecided' {\n return coreCovers(\n outer.map((g) => g.compiled),\n inner.compiled,\n budget,\n );\n}\n\n/**\n * A path both globs match, or `null` when there is none: the 0.1 interface,\n * kept for the library's callers. It cannot say `undecided`, so a search that\n * meets its budget throws; {@link globWitness} answers in three ways.\n */\nexport function intersectGlobs(a: Glob, b: Glob, budget: number = WITNESS_BUDGET): string | null {\n const witness = globWitness([a, b], [], budget);\n if (witness.kind === 'undecided') {\n throw new Error(`whether \"${a.source}\" and \"${b.source}\" meet is undecided within the search's budget`);\n }\n return witness.kind === 'found' ? witness.path : null;\n}\n\n/**\n * The directories a walk must enter to find every match, one per brace\n * alternative: the leading literal segments, or a literal file's directory.\n * `''` for an alternative that can match at the root.\n */\nexport function globBases(glob: Glob): readonly string[] {\n return glob.compiled.bases;\n}\n\n/** The first of {@link globBases}: the 0.1 interface, kept for the library's callers. */\nexport function globBase(glob: Glob): string {\n // One base per alternative, and a pattern has at least one.\n return glob.compiled.bases[0] as string;\n}\n\n/** A name with an extension, `login.ts` or `.eslintrc.json`, and not a dot-name such as `.github`. */\nexport function hasExtension(name: string): boolean {\n return /.\\.[^.]+$/.test(name);\n}\n\n/** The files a tree holds, and every directory above one. */\nexport interface Tree {\n readonly files: ReadonlySet<string>;\n readonly directories: ReadonlySet<string>;\n}\n\nconst trees = new WeakMap<readonly string[], Tree>();\n\n/** The tree a list of files describes. Built once per list, which every brief of a run shares. */\nexport function treeOf(files: readonly string[]): Tree {\n const known = trees.get(files);\n if (known !== undefined) return known;\n const directories = new Set<string>();\n for (const file of files) {\n for (let slash = file.indexOf('/'); slash !== -1; slash = file.indexOf('/', slash + 1)) directories.add(file.slice(0, slash));\n }\n const tree = { files: new Set(files), directories };\n trees.set(files, tree);\n return tree;\n}\n\n/** What the tree holds at a path: a file, a directory, or nothing. */\nexport function held(tree: Tree, path: string): 'file' | 'directory' | null {\n if (tree.files.has(path)) return 'file';\n return tree.directories.has(path) ? 'directory' : null;\n}\n\n/**\n * How a tree reads a literal path: a directory it holds is a directory, and\n * anything else is a file. Without a tree nothing is known to be a directory,\n * so every literal is a file; a trailing `/` says otherwise, and never reaches\n * this question.\n */\nexport function readingIn(files: readonly string[] | null): (path: string) => LiteralReading {\n if (files === null) return () => 'file';\n const tree = treeOf(files);\n return (path) => (held(tree, path) === 'directory' ? 'directory' : 'file');\n}\n"]}
|
|
1
|
+
{"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Scopes as globs: spec-core's `path` dialect, read the way spec-brief reads a\n * brief.\n *\n * The syntax, the automaton and the witness search are spec-core's, copied\n * into `src/vendor/` and verified by hash, so the family's tools read one\n * pattern the same way (spec-core ADR-0003). What is decided here is what\n * spec-brief adds around it:\n *\n * - A pattern is relative to the repository root, so a leading `/` is refused\n * rather than rooted at the filesystem; and a list entry's `!` has no list to\n * belong to in a scope, so it is refused rather than read.\n * - Paths compare case-sensitively on every host, as git's do.\n * - A path with no glob syntax is read as the tree reads it: a file the tree\n * holds is that file, a directory it holds is everything beneath it, and a\n * path it does not hold is a file, unless it is written with a trailing `/`.\n * The tree, not the spelling, knows that `Dockerfile` is a file and\n * `docs/v1.2` a directory (ADR-0005, amended 2026-09-26).\n * - A trailing `/**` is at least one segment, so `docs/**` is the directory's\n * contents and a collision witness is always a file. A trailing `/` means\n * the same on a brace alternative as on the whole pattern: `{src/,lib}` is\n * `src/` or `lib`, and the tree is asked about `lib` alone.\n *\n * Nothing here compiles to a `RegExp`. Matching costs at most the pattern's\n * size times the path's length, and a question about two scopes that cannot be\n * answered within the search's budget answers `undecided`, never a guess.\n */\n\nimport {\n globCovers as coreCovers,\n globWitness as coreWitness,\n isGlobSyntax,\n parseGlob as coreParse,\n type Glob as CoreGlob,\n type LiteralReading,\n WITNESS_BUDGET,\n type Witness,\n} from './vendor/spec-core/pattern/index.js';\nimport { segments } from './vendor/spec-core/path/index.js';\n\nexport { isGlobSyntax, type LiteralReading, WITNESS_BUDGET, type Witness };\n\n/** A literal path a pattern names, and how it was read. */\nexport interface Literal {\n readonly path: string;\n readonly reading: LiteralReading;\n}\n\nexport interface Glob {\n readonly source: string;\n /** The compiled pattern: spec-core's `path` dialect, case-sensitive. */\n readonly compiled: CoreGlob;\n /**\n * Each path the pattern names with no glob syntax and no trailing `/`, one\n * per brace alternative; empty when it has none.\n */\n readonly literals: readonly Literal[];\n}\n\nexport type GlobParse = { readonly ok: true; readonly glob: Glob } | { readonly ok: false; readonly error: string };\n\nexport interface ParseOptions {\n /**\n * How a path with no glob syntax is read: `file`, that path alone;\n * `directory`, everything beneath it; `either`, both. A function is asked\n * per literal path, as {@link readingIn} answers for a tree. `file` when\n * unset: a path nothing says is a directory is not read as one.\n */\n readonly literal?: LiteralReading | ((path: string) => LiteralReading) | undefined;\n}\n\n/** Parses a scope pattern, or says why it cannot. */\nexport function parseGlob(source: string, options: ParseOptions = {}): GlobParse {\n const pattern = source.trim();\n if (pattern.startsWith('!')) return { ok: false, error: 'negated patterns are not supported; narrow the positive pattern' };\n if (pattern.startsWith('/')) return { ok: false, error: 'a pattern is relative to the repository root and cannot start with \"/\"' };\n const given = options.literal ?? 'file';\n const literals: Literal[] = [];\n const literal = (path: string): LiteralReading => {\n const reading = typeof given === 'function' ? given(path) : given;\n literals.push({ path, reading });\n return reading;\n };\n // A pattern too large to compile is refused by the core, with its reason,\n // as a malformed one is; what the reading throws is a defect, and passes on.\n const parsed = coreParse(pattern, { dialect: 'path', caseSensitive: true, literal });\n return parsed.ok ? { ok: true, glob: { source, compiled: parsed.glob, literals } } : parsed;\n}\n\n/**\n * Whether a path matches. The path is read as a repository path: empty and\n * `.` segments are dropped, so `./src//a.ts` is `src/a.ts`.\n */\nexport function matchGlob(glob: Glob, path: string): boolean {\n const canonical = segments(path).join('/');\n return canonical !== '' && glob.compiled.match(canonical);\n}\n\n/**\n * A shortest path every glob in `include` matches and none in `exclude` does,\n * `none` as a proof that there is no such path, or `undecided` when the search\n * met its budget first.\n */\nexport function globWitness(include: readonly Glob[], exclude: readonly Glob[] = [], budget: number = WITNESS_BUDGET): Witness {\n return coreWitness(\n include.map((g) => g.compiled),\n exclude.map((g) => g.compiled),\n budget,\n );\n}\n\n/** Whether every path `inner` matches is matched by one of `outer`, or `undecided`. */\nexport function globCovers(outer: readonly Glob[], inner: Glob, budget: number = WITNESS_BUDGET): boolean | 'undecided' {\n return coreCovers(\n outer.map((g) => g.compiled),\n inner.compiled,\n budget,\n );\n}\n\n/**\n * A path both globs match, or `null` when there is none: the 0.1 interface,\n * kept for the library's callers. It cannot say `undecided`, so a search that\n * meets its budget throws; {@link globWitness} answers in three ways.\n */\nexport function intersectGlobs(a: Glob, b: Glob, budget: number = WITNESS_BUDGET): string | null {\n const witness = globWitness([a, b], [], budget);\n if (witness.kind === 'undecided') {\n throw new Error(`whether \"${a.source}\" and \"${b.source}\" meet is undecided within the search's budget`);\n }\n return witness.kind === 'found' ? witness.path : null;\n}\n\n/**\n * The directories a walk must enter to find every match, one per brace\n * alternative: the leading literal segments, or a literal file's directory.\n * `''` for an alternative that can match at the root.\n */\nexport function globBases(glob: Glob): readonly string[] {\n return glob.compiled.bases;\n}\n\n/** The first of {@link globBases}: the 0.1 interface, kept for the library's callers. */\nexport function globBase(glob: Glob): string {\n // One base per alternative, and a pattern has at least one.\n return glob.compiled.bases[0] as string;\n}\n\n/** A brace alternative as its pattern writes it, and the index in the pattern where it ends. */\nexport interface WrittenAlternative {\n readonly written: string;\n readonly end: number;\n}\n\n/**\n * Each alternative of a pattern as written, in the order the core expands\n * them, and where each ends, when a `/` written there ends that alternative\n * and no other: the pattern itself when it has no braces, or the alternatives\n * of braces that end the pattern and whose own braces end theirs -\n * `lib/{util,new}`, `{a,b/{c,d}}`. `null` when more of the pattern follows\n * braces, `{a,b}/new`, since every alternative they give ends where the\n * pattern does; and for a pattern with a class, which may hold a brace or a\n * comma of its own and which this does not read.\n *\n * spec-core expands braces and keeps nothing of where an alternative was\n * written, and a note's advice is built from what was written, as the core\n * builds its own. So this reads braces as the expansion does, for a pattern\n * that parses: a `\\` escapes the next character, and a `}` with no `{` open\n * is a literal.\n */\nexport function writtenAlternatives(pattern: string): WrittenAlternative[] | null {\n let depth = 0;\n let open = 0;\n const stops: number[] = [];\n for (let i = 0; i < pattern.length; i += 1) {\n const ch = pattern.charAt(i);\n if (ch === '\\\\') {\n i += 1;\n } else if (ch === '[') {\n return null;\n } else if (ch === '{') {\n if (depth === 0) open = i;\n depth += 1;\n } else if (ch === ',' && depth === 1) {\n stops.push(i);\n } else if (ch === '}' && depth > 0) {\n depth -= 1;\n if (depth === 0) return i === pattern.length - 1 ? optionsOf(pattern, open, [...stops, i]) : null;\n }\n }\n return [{ written: pattern, end: pattern.length }];\n}\n\n/** The alternatives of the braces opened at `open`, each option ending at one of `stops`. */\nfunction optionsOf(pattern: string, open: number, stops: readonly number[]): WrittenAlternative[] | null {\n const prefix = pattern.slice(0, open);\n const alternatives: WrittenAlternative[] = [];\n let start = open + 1;\n for (const stop of stops) {\n const inner = writtenAlternatives(pattern.slice(start, stop));\n if (inner === null) return null;\n for (const { written, end } of inner) alternatives.push({ written: prefix + written, end: start + end });\n start = stop + 1;\n }\n return alternatives;\n}\n\n/** A name with an extension, `login.ts` or `.eslintrc.json`, and not a dot-name such as `.github`. */\nexport function hasExtension(name: string): boolean {\n return /.\\.[^.]+$/.test(name);\n}\n\n/** The files a tree holds, and every directory above one. */\nexport interface Tree {\n readonly files: ReadonlySet<string>;\n readonly directories: ReadonlySet<string>;\n}\n\nconst trees = new WeakMap<readonly string[], Tree>();\n\n/** The tree a list of files describes. Built once per list, which every brief of a run shares. */\nexport function treeOf(files: readonly string[]): Tree {\n const known = trees.get(files);\n if (known !== undefined) return known;\n const directories = new Set<string>();\n for (const file of files) {\n for (let slash = file.indexOf('/'); slash !== -1; slash = file.indexOf('/', slash + 1)) directories.add(file.slice(0, slash));\n }\n const tree = { files: new Set(files), directories };\n trees.set(files, tree);\n return tree;\n}\n\n/** What the tree holds at a path: a file, a directory, or nothing. */\nexport function held(tree: Tree, path: string): 'file' | 'directory' | null {\n if (tree.files.has(path)) return 'file';\n return tree.directories.has(path) ? 'directory' : null;\n}\n\n/**\n * How a tree reads a literal path: a directory it holds is a directory, and\n * anything else is a file. Without a tree nothing is known to be a directory,\n * so every literal is a file; a trailing `/` says otherwise, and never reaches\n * this question.\n */\nexport function readingIn(files: readonly string[] | null): (path: string) => LiteralReading {\n if (files === null) return () => 'file';\n const tree = treeOf(files);\n return (path) => (held(tree, path) === 'directory' ? 'directory' : 'file');\n}\n"]}
|
package/dist/rules.js
CHANGED
|
@@ -12,7 +12,7 @@ import { statusWords } from './config.js';
|
|
|
12
12
|
import { dependencyCycles, duplicateIds, idKey, resolveDependency } from './corpus.js';
|
|
13
13
|
import { findEntry } from './frontmatter.js';
|
|
14
14
|
import { integrityOf } from './integrity.js';
|
|
15
|
-
import { hasExtension, held, isGlobSyntax, matchGlob, parseGlob, readingIn, treeOf, WITNESS_BUDGET } from './glob.js';
|
|
15
|
+
import { hasExtension, held, isGlobSyntax, matchGlob, parseGlob, readingIn, treeOf, WITNESS_BUDGET, writtenAlternatives } from './glob.js';
|
|
16
16
|
import { hasContent } from './markdown.js';
|
|
17
17
|
import { closest } from './schema.js';
|
|
18
18
|
import { contradictions, patternsOf, scopeOf } from './scope.js';
|
|
@@ -76,6 +76,39 @@ export function unheldLiterals(glob, files) {
|
|
|
76
76
|
.map((literal) => literal.path)
|
|
77
77
|
.filter((path) => held(tree, path) === null && !hasExtension(path.slice(path.lastIndexOf('/') + 1)));
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* What to write for a directory where a pattern names paths the tree reads as
|
|
81
|
+
* files, built from the pattern as written, as spec-core builds its own
|
|
82
|
+
* advice: the pattern with a `/` ending each alternative that names one, since
|
|
83
|
+
* a trailing `/` inside braces says directory as it does alone, or each such
|
|
84
|
+
* alternative in an entry of its own (ADR-0005, amended 2026-09-29). Where
|
|
85
|
+
* more of the pattern follows braces, `{a,b}/new`, no `/` written inside them
|
|
86
|
+
* ends one alternative alone, so an entry of its own is the advice; and an
|
|
87
|
+
* alternative the reader refuses as an entry, one opening with `!`, is left as
|
|
88
|
+
* written. The reader trims every list item, so the pattern is as meant.
|
|
89
|
+
*/
|
|
90
|
+
function directoryAdvice(pattern, paths) {
|
|
91
|
+
const alternatives = writtenAlternatives(pattern) ?? [];
|
|
92
|
+
const named = alternatives.filter(({ written }) => {
|
|
93
|
+
const parsed = parseGlob(written);
|
|
94
|
+
return parsed.ok && parsed.glob.literals.some((literal) => paths.includes(literal.path));
|
|
95
|
+
});
|
|
96
|
+
const entries = (named.length === 0 ? paths : named.map(({ written }) => written)).map((entry) => `${entry}/`);
|
|
97
|
+
const one = entries.length === 1;
|
|
98
|
+
const kind = one ? 'a directory' : 'directories';
|
|
99
|
+
const own = one ? 'an entry of its own' : 'entries of their own';
|
|
100
|
+
if (named.length === 0)
|
|
101
|
+
return `write ${quoted(entries)} for ${kind}, in ${own}`;
|
|
102
|
+
if (alternatives.length === 1)
|
|
103
|
+
return `write ${quoted(entries)} for ${kind}`;
|
|
104
|
+
let slashed = '';
|
|
105
|
+
let from = 0;
|
|
106
|
+
for (const { end } of named) {
|
|
107
|
+
slashed += `${pattern.slice(from, end)}/`;
|
|
108
|
+
from = end;
|
|
109
|
+
}
|
|
110
|
+
return `write "${slashed}${pattern.slice(from)}" for ${kind}, or ${quoted(entries)} in ${own}`;
|
|
111
|
+
}
|
|
79
112
|
/**
|
|
80
113
|
* Affected patterns a brief's own protections cover entirely: one error for
|
|
81
114
|
* all of them, and a warning for those the search could not decide.
|
|
@@ -499,21 +532,15 @@ export const RULES = [
|
|
|
499
532
|
if (paths.length === 0)
|
|
500
533
|
return [];
|
|
501
534
|
const line = at(lineOfField(brief, field));
|
|
535
|
+
const hint = directoryAdvice(pattern, paths);
|
|
502
536
|
if (!isGlobSyntax(pattern)) {
|
|
503
|
-
return [
|
|
504
|
-
{
|
|
505
|
-
line,
|
|
506
|
-
message: `"${pattern}" in ${field} is not in the tree and is read as a file`,
|
|
507
|
-
// The reader trims every list item, so the pattern is as meant.
|
|
508
|
-
hint: `write "${pattern}/" for a directory`,
|
|
509
|
-
},
|
|
510
|
-
];
|
|
537
|
+
return [{ line, message: `"${pattern}" in ${field} is not in the tree and is read as a file`, hint }];
|
|
511
538
|
}
|
|
512
539
|
return [
|
|
513
540
|
{
|
|
514
541
|
line,
|
|
515
542
|
message: `"${pattern}" in ${field} names ${inWords(paths)}, which ${paths.length === 1 ? 'is' : 'are'} not in the tree and ${paths.length === 1 ? 'is' : 'are'} read as ${paths.length === 1 ? 'a file' : 'files'}`,
|
|
516
|
-
hint
|
|
543
|
+
hint,
|
|
517
544
|
},
|
|
518
545
|
];
|
|
519
546
|
}));
|