@descent-vtt/spec-harness 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +17 -0
- package/.mcp.json +12 -0
- package/CHANGELOG.md +46 -0
- package/README.md +127 -9
- package/bin/spec-harness.js +19 -0
- package/dist/audit.d.ts +69 -0
- package/dist/audit.js +93 -0
- package/dist/audit.js.map +1 -0
- package/dist/branch.d.ts +26 -0
- package/dist/branch.js +100 -0
- package/dist/branch.js.map +1 -0
- package/dist/briefs.d.ts +49 -0
- package/dist/briefs.js +111 -0
- package/dist/briefs.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +245 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +224 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.js +167 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +56 -0
- package/dist/context.js +125 -0
- package/dist/context.js.map +1 -0
- package/dist/document.d.ts +33 -0
- package/dist/document.js +9 -0
- package/dist/document.js.map +1 -0
- package/dist/fs.d.ts +29 -0
- package/dist/fs.js +85 -0
- package/dist/fs.js.map +1 -0
- package/dist/git.d.ts +57 -0
- package/dist/git.js +129 -0
- package/dist/git.js.map +1 -0
- package/dist/guard.d.ts +43 -0
- package/dist/guard.js +120 -0
- package/dist/guard.js.map +1 -0
- package/dist/hooks.d.ts +50 -0
- package/dist/hooks.js +109 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/junit.d.ts +30 -0
- package/dist/junit.js +166 -0
- package/dist/junit.js.map +1 -0
- package/dist/manifests.d.ts +52 -0
- package/dist/manifests.js +346 -0
- package/dist/manifests.js.map +1 -0
- package/dist/plugin.d.ts +45 -0
- package/dist/plugin.js +79 -0
- package/dist/plugin.js.map +1 -0
- package/dist/premises.d.ts +16 -0
- package/dist/premises.js +71 -0
- package/dist/premises.js.map +1 -0
- package/dist/probe.d.ts +108 -0
- package/dist/probe.js +203 -0
- package/dist/probe.js.map +1 -0
- package/dist/reader.d.ts +8 -0
- package/dist/reader.js +105 -0
- package/dist/reader.js.map +1 -0
- package/dist/round.d.ts +68 -0
- package/dist/round.js +339 -0
- package/dist/round.js.map +1 -0
- package/dist/rulings.d.ts +92 -0
- package/dist/rulings.js +160 -0
- package/dist/rulings.js.map +1 -0
- package/dist/sandbox.d.ts +28 -0
- package/dist/sandbox.js +110 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +212 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.d.ts +32 -0
- package/dist/setup.js +218 -0
- package/dist/setup.js.map +1 -0
- package/dist/siblings.d.ts +66 -0
- package/dist/siblings.js +134 -0
- package/dist/siblings.js.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/index.d.ts +2 -0
- package/dist/vendor/spec-core/jsonrpc/index.js +2 -0
- package/dist/vendor/spec-core/jsonrpc/index.js.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.d.ts +157 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.js +353 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.js.map +1 -0
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts +103 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
- package/dist/vendor/spec-core/markdown/headings.d.ts +45 -0
- package/dist/vendor/spec-core/markdown/headings.js +199 -0
- package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
- package/dist/vendor/spec-core/markdown/index.d.ts +5 -0
- package/dist/vendor/spec-core/markdown/index.js +4 -0
- package/dist/vendor/spec-core/markdown/index.js.map +1 -0
- package/dist/vendor/spec-core/markdown/layout.d.ts +39 -0
- package/dist/vendor/spec-core/markdown/layout.js +28 -0
- package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
- package/dist/vendor/spec-core/markdown/links.d.ts +22 -0
- package/dist/vendor/spec-core/markdown/links.js +364 -0
- package/dist/vendor/spec-core/markdown/links.js.map +1 -0
- package/dist/vendor/spec-core/markdown/lists.d.ts +22 -0
- package/dist/vendor/spec-core/markdown/lists.js +127 -0
- package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
- package/dist/vendor/spec-core/markdown/scan.d.ts +36 -0
- package/dist/vendor/spec-core/markdown/scan.js +439 -0
- package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
- package/dist/vendor/spec-core/markdown/syntax.d.ts +75 -0
- package/dist/vendor/spec-core/markdown/syntax.js +181 -0
- package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
- package/dist/vendor/spec-core/markdown/tables.d.ts +18 -0
- package/dist/vendor/spec-core/markdown/tables.js +86 -0
- package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
- package/dist/vendor/spec-core/markdown/types.d.ts +287 -0
- package/dist/vendor/spec-core/markdown/types.js +9 -0
- package/dist/vendor/spec-core/markdown/types.js.map +1 -0
- package/dist/vendor/spec-core/path/index.d.ts +1 -0
- package/dist/vendor/spec-core/path/index.js +2 -0
- package/dist/vendor/spec-core/path/index.js.map +1 -0
- package/dist/vendor/spec-core/path/posix.d.ts +83 -0
- package/dist/vendor/spec-core/path/posix.js +150 -0
- package/dist/vendor/spec-core/path/posix.js.map +1 -0
- package/dist/vendor/spec-core/pattern/automaton.d.ts +129 -0
- package/dist/vendor/spec-core/pattern/automaton.js +340 -0
- package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
- package/dist/vendor/spec-core/pattern/charset.d.ts +58 -0
- package/dist/vendor/spec-core/pattern/charset.js +116 -0
- package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
- package/dist/vendor/spec-core/pattern/glob.d.ts +121 -0
- package/dist/vendor/spec-core/pattern/glob.js +481 -0
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
- package/dist/vendor/spec-core/pattern/index.d.ts +6 -0
- package/dist/vendor/spec-core/pattern/index.js +4 -0
- package/dist/vendor/spec-core/pattern/index.js.map +1 -0
- package/dist/vendor/spec-core/pattern/regex.d.ts +67 -0
- package/dist/vendor/spec-core/pattern/regex.js +813 -0
- package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
- package/dist/vendor/spec-core/text/index.d.ts +2 -0
- package/dist/vendor/spec-core/text/index.js +2 -0
- package/dist/vendor/spec-core/text/index.js.map +1 -0
- package/dist/vendor/spec-core/text/lines.d.ts +72 -0
- package/dist/vendor/spec-core/text/lines.js +156 -0
- package/dist/vendor/spec-core/text/lines.js.map +1 -0
- package/dist/versions.d.ts +42 -0
- package/dist/versions.js +57 -0
- package/dist/versions.js.map +1 -0
- package/dist/workspace.d.ts +77 -0
- package/dist/workspace.js +138 -0
- package/dist/workspace.js.map +1 -0
- package/hooks/hooks.json +28 -0
- package/package.json +86 -6
- package/skills/close-round/SKILL.md +38 -0
- package/skills/draft-brief/SKILL.md +64 -0
- package/skills/run-round/SKILL.md +46 -0
- package/skills/split-goal/SKILL.md +42 -0
- package/src/vendor/spec-core/LICENSE +21 -0
- package/src/vendor/spec-core/README.md +8 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Globs, in the three dialects the spec-* tools speak, compiled to one kind of
|
|
3
|
+
* automaton.
|
|
4
|
+
*
|
|
5
|
+
* The syntax is the part people type: `*`, `**`, `?`, `[abc]`, `[!a-z]`,
|
|
6
|
+
* `{a,b}`, and `\` to escape. What differs between the tools is not the
|
|
7
|
+
* syntax but what a pattern is matched against, and those differences are
|
|
8
|
+
* deliberate, so each has a name (docs/adr/0003-glob-dialects.md):
|
|
9
|
+
*
|
|
10
|
+
* - `path` - the whole repository-relative path, anchored at the root. A
|
|
11
|
+
* pattern with no glob syntax names a file, a directory, or either, as the
|
|
12
|
+
* caller says; `src/auth` may cover `src/auth/login.ts`.
|
|
13
|
+
* - `ripgrep` - as ripgrep's `-g`: a pattern with no `/` is matched against
|
|
14
|
+
* the last segment at any depth, so `*.ts` matches `src/deep/a.ts`; one with
|
|
15
|
+
* a `/` is matched against the whole path.
|
|
16
|
+
* - `gitignore` - as an exclusion list reads: a pattern matches a path or any
|
|
17
|
+
* directory above it, so `tests` excludes everything under any `tests`; one
|
|
18
|
+
* with a `/` other than a trailing one is anchored at the root, and a
|
|
19
|
+
* leading `/` only anchors.
|
|
20
|
+
*
|
|
21
|
+
* Shared by all three:
|
|
22
|
+
*
|
|
23
|
+
* - `**` is a globstar only as a whole segment. `a/**\/b` matches `a/b` and
|
|
24
|
+
* `a/x/y/b`; a trailing `/**` matches everything inside a directory and not
|
|
25
|
+
* the directory itself. Two stars inside a name, `**.md`, are refused: the
|
|
26
|
+
* tools read it three ways, and the quiet reading narrowed their scope.
|
|
27
|
+
* - `*`, `?` and classes never match `/`, and `*` matches a leading dot.
|
|
28
|
+
* - Case is the caller's decision, stated every time. A result must not
|
|
29
|
+
* depend on the host it ran on.
|
|
30
|
+
* - A malformed pattern is an error, never a literal. An unclosed `[` or `{`,
|
|
31
|
+
* an extended glob, a `..` - each is refused with a reason, because a typo
|
|
32
|
+
* read as a literal is a scope that silently matches nothing. An extended
|
|
33
|
+
* glob is a group holding a `|`, `+(a|b)`: without one, `C++(notes).md` is
|
|
34
|
+
* a name with parentheses in it, as ripgrep and `.gitignore` read it.
|
|
35
|
+
*/
|
|
36
|
+
import { type Automaton, type Witness } from './automaton.js';
|
|
37
|
+
export type GlobDialect = 'path' | 'ripgrep' | 'gitignore';
|
|
38
|
+
/** How a `path` pattern with no glob syntax is read. */
|
|
39
|
+
export type LiteralReading = 'file' | 'directory' | 'either';
|
|
40
|
+
export interface GlobOptions {
|
|
41
|
+
readonly dialect: GlobDialect;
|
|
42
|
+
/** Whether `A` and `a` differ. No default: the caller decides, every time. */
|
|
43
|
+
readonly caseSensitive: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* `path` dialect only: how a pattern with no glob syntax is read - as a
|
|
46
|
+
* file, as a directory and everything beneath it, or as either. A function
|
|
47
|
+
* is asked per literal path, for a caller that knows the tree. `either` when
|
|
48
|
+
* unset. A trailing `/` always means a directory.
|
|
49
|
+
*/
|
|
50
|
+
readonly literal?: LiteralReading | ((path: string) => LiteralReading) | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* `escape` (the default): `\` escapes the next character, and a `\` before a
|
|
53
|
+
* letter or digit is refused as a Windows separator typed by mistake.
|
|
54
|
+
* `separator`: every `\` is a `/`, for patterns typed on a Windows shell.
|
|
55
|
+
*/
|
|
56
|
+
readonly backslash?: 'escape' | 'separator' | undefined;
|
|
57
|
+
}
|
|
58
|
+
export interface Glob {
|
|
59
|
+
readonly source: string;
|
|
60
|
+
readonly dialect: GlobDialect;
|
|
61
|
+
readonly caseSensitive: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The directories a walk must enter to find every match: one per
|
|
64
|
+
* alternative, the leading literal segments. `''` for an alternative that
|
|
65
|
+
* can match at any depth.
|
|
66
|
+
*/
|
|
67
|
+
readonly bases: readonly string[];
|
|
68
|
+
readonly automaton: Automaton;
|
|
69
|
+
/** Whether a canonical path - POSIX, no `.`, no empty or trailing segment - matches. */
|
|
70
|
+
match(path: string): boolean;
|
|
71
|
+
}
|
|
72
|
+
export type GlobParse = {
|
|
73
|
+
readonly ok: true;
|
|
74
|
+
readonly glob: Glob;
|
|
75
|
+
} | {
|
|
76
|
+
readonly ok: false;
|
|
77
|
+
readonly error: string;
|
|
78
|
+
};
|
|
79
|
+
export declare class GlobError extends Error {
|
|
80
|
+
readonly source: string;
|
|
81
|
+
constructor(source: string, reason: string);
|
|
82
|
+
}
|
|
83
|
+
/** More alternatives than this is a pattern nobody meant. */
|
|
84
|
+
export declare const MAX_ALTERNATIVES = 256;
|
|
85
|
+
/** Whether a string uses glob syntax at all. */
|
|
86
|
+
export declare function isGlobSyntax(source: string): boolean;
|
|
87
|
+
/** Parses and compiles a glob, or says why it cannot. */
|
|
88
|
+
export declare function parseGlob(source: string, options: GlobOptions): GlobParse;
|
|
89
|
+
/** Parses and compiles a glob, throwing {@link GlobError} when it cannot. */
|
|
90
|
+
export declare function compileGlob(source: string, options: GlobOptions): Glob;
|
|
91
|
+
/**
|
|
92
|
+
* A shortest path every glob in `include` matches and no glob in `exclude`
|
|
93
|
+
* does: the file two scopes would both write, less what either protects.
|
|
94
|
+
* `none` is a proof that there is no such path. Case-sensitive globs only.
|
|
95
|
+
*/
|
|
96
|
+
export declare function globWitness(include: readonly Glob[], exclude?: readonly Glob[], budget?: number): Witness;
|
|
97
|
+
/**
|
|
98
|
+
* Whether every path `inner` matches is matched by one of `outer`: `true`,
|
|
99
|
+
* `false`, or `undecided` when the search met its budget.
|
|
100
|
+
*/
|
|
101
|
+
export declare function globCovers(outer: readonly Glob[], inner: Glob, budget?: number): boolean | 'undecided';
|
|
102
|
+
export interface GlobList {
|
|
103
|
+
/** Whether a path is in: the last entry that matches it decides, and a `!` entry takes it out. */
|
|
104
|
+
match(path: string): boolean;
|
|
105
|
+
readonly entries: readonly {
|
|
106
|
+
readonly negated: boolean;
|
|
107
|
+
readonly glob: Glob;
|
|
108
|
+
}[];
|
|
109
|
+
}
|
|
110
|
+
export type GlobListParse = {
|
|
111
|
+
readonly ok: true;
|
|
112
|
+
readonly list: GlobList;
|
|
113
|
+
} | {
|
|
114
|
+
readonly ok: false;
|
|
115
|
+
readonly error: string;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* A list of patterns read the way a `.gitignore` reads: in order, each entry
|
|
119
|
+
* deciding the paths it matches, a leading `!` taking them back out.
|
|
120
|
+
*/
|
|
121
|
+
export declare function parseGlobList(patterns: readonly string[], options: GlobOptions): GlobListParse;
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Globs, in the three dialects the spec-* tools speak, compiled to one kind of
|
|
3
|
+
* automaton.
|
|
4
|
+
*
|
|
5
|
+
* The syntax is the part people type: `*`, `**`, `?`, `[abc]`, `[!a-z]`,
|
|
6
|
+
* `{a,b}`, and `\` to escape. What differs between the tools is not the
|
|
7
|
+
* syntax but what a pattern is matched against, and those differences are
|
|
8
|
+
* deliberate, so each has a name (docs/adr/0003-glob-dialects.md):
|
|
9
|
+
*
|
|
10
|
+
* - `path` - the whole repository-relative path, anchored at the root. A
|
|
11
|
+
* pattern with no glob syntax names a file, a directory, or either, as the
|
|
12
|
+
* caller says; `src/auth` may cover `src/auth/login.ts`.
|
|
13
|
+
* - `ripgrep` - as ripgrep's `-g`: a pattern with no `/` is matched against
|
|
14
|
+
* the last segment at any depth, so `*.ts` matches `src/deep/a.ts`; one with
|
|
15
|
+
* a `/` is matched against the whole path.
|
|
16
|
+
* - `gitignore` - as an exclusion list reads: a pattern matches a path or any
|
|
17
|
+
* directory above it, so `tests` excludes everything under any `tests`; one
|
|
18
|
+
* with a `/` other than a trailing one is anchored at the root, and a
|
|
19
|
+
* leading `/` only anchors.
|
|
20
|
+
*
|
|
21
|
+
* Shared by all three:
|
|
22
|
+
*
|
|
23
|
+
* - `**` is a globstar only as a whole segment. `a/**\/b` matches `a/b` and
|
|
24
|
+
* `a/x/y/b`; a trailing `/**` matches everything inside a directory and not
|
|
25
|
+
* the directory itself. Two stars inside a name, `**.md`, are refused: the
|
|
26
|
+
* tools read it three ways, and the quiet reading narrowed their scope.
|
|
27
|
+
* - `*`, `?` and classes never match `/`, and `*` matches a leading dot.
|
|
28
|
+
* - Case is the caller's decision, stated every time. A result must not
|
|
29
|
+
* depend on the host it ran on.
|
|
30
|
+
* - A malformed pattern is an error, never a literal. An unclosed `[` or `{`,
|
|
31
|
+
* an extended glob, a `..` - each is refused with a reason, because a typo
|
|
32
|
+
* read as a literal is a scope that silently matches nothing. An extended
|
|
33
|
+
* glob is a group holding a `|`, `+(a|b)`: without one, `C++(notes).md` is
|
|
34
|
+
* a name with parentheses in it, as ripgrep and `.gitignore` read it.
|
|
35
|
+
*/
|
|
36
|
+
import { Builder, findWitness, Matcher, WITNESS_BUDGET, } from './automaton.js';
|
|
37
|
+
import { ANY, classSet, literalSet, SEPARATOR } from './charset.js';
|
|
38
|
+
export class GlobError extends Error {
|
|
39
|
+
source;
|
|
40
|
+
constructor(source, reason) {
|
|
41
|
+
super(`invalid glob "${source}": ${reason}`);
|
|
42
|
+
this.name = 'GlobError';
|
|
43
|
+
this.source = source;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** More alternatives than this is a pattern nobody meant. */
|
|
47
|
+
export const MAX_ALTERNATIVES = 256;
|
|
48
|
+
/** Whether a string uses glob syntax at all. */
|
|
49
|
+
export function isGlobSyntax(source) {
|
|
50
|
+
return /[*?[\]{}]/.test(source);
|
|
51
|
+
}
|
|
52
|
+
/** Parses and compiles a glob, or says why it cannot. */
|
|
53
|
+
export function parseGlob(source, options) {
|
|
54
|
+
const result = build(source, options);
|
|
55
|
+
return typeof result === 'string' ? { ok: false, error: result } : { ok: true, glob: result };
|
|
56
|
+
}
|
|
57
|
+
/** Parses and compiles a glob, throwing {@link GlobError} when it cannot. */
|
|
58
|
+
export function compileGlob(source, options) {
|
|
59
|
+
const result = build(source, options);
|
|
60
|
+
if (typeof result === 'string')
|
|
61
|
+
throw new GlobError(source, result);
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
function build(source, options) {
|
|
65
|
+
const escapes = options.backslash !== 'separator';
|
|
66
|
+
let pattern = source.trim();
|
|
67
|
+
if (!escapes)
|
|
68
|
+
pattern = pattern.replace(/\\/g, '/');
|
|
69
|
+
if (pattern.length === 0)
|
|
70
|
+
return 'the pattern is empty';
|
|
71
|
+
if (pattern.startsWith('!'))
|
|
72
|
+
return 'a negated pattern is a list entry, not a glob; narrow the positive pattern';
|
|
73
|
+
if (/(?:^|[^\\])[?*+@!]\([^)]*\|/.test(pattern))
|
|
74
|
+
return EXTGLOB;
|
|
75
|
+
while (pattern.startsWith('./'))
|
|
76
|
+
pattern = pattern.slice(2);
|
|
77
|
+
// A leading slash roots a pattern at the filesystem's root in the path and
|
|
78
|
+
// ripgrep dialects, and only anchors it at the repository root in gitignore.
|
|
79
|
+
let rooted = false;
|
|
80
|
+
let anchored = false;
|
|
81
|
+
if (pattern.startsWith('/')) {
|
|
82
|
+
if (options.dialect === 'gitignore')
|
|
83
|
+
anchored = true;
|
|
84
|
+
else
|
|
85
|
+
rooted = true;
|
|
86
|
+
pattern = pattern.replace(/^\/+/, '');
|
|
87
|
+
}
|
|
88
|
+
// A trailing slash means a directory's contents, except in an exclusion,
|
|
89
|
+
// where `build/` and `build` exclude the same things.
|
|
90
|
+
if (pattern.endsWith('/')) {
|
|
91
|
+
pattern = pattern.replace(/\/+$/, '');
|
|
92
|
+
if (options.dialect !== 'gitignore')
|
|
93
|
+
pattern = `${pattern}/**`;
|
|
94
|
+
}
|
|
95
|
+
if (pattern.length === 0)
|
|
96
|
+
return 'the pattern names the root itself, not a path under it';
|
|
97
|
+
const expanded = expandBraces(pattern, escapes);
|
|
98
|
+
if (typeof expanded === 'string')
|
|
99
|
+
return expanded;
|
|
100
|
+
const alternatives = [];
|
|
101
|
+
for (const text of expanded) {
|
|
102
|
+
const parsed = parseAlternative(text, escapes);
|
|
103
|
+
if (typeof parsed === 'string')
|
|
104
|
+
return parsed;
|
|
105
|
+
alternatives.push(parsed);
|
|
106
|
+
}
|
|
107
|
+
const builder = new Builder();
|
|
108
|
+
const fragments = [];
|
|
109
|
+
const bases = [];
|
|
110
|
+
for (const alternative of alternatives) {
|
|
111
|
+
const compiled = compileAlternative(builder, alternative, options, rooted, anchored);
|
|
112
|
+
fragments.push(compiled.fragment);
|
|
113
|
+
bases.push(compiled.base);
|
|
114
|
+
}
|
|
115
|
+
const automaton = builder.finish(builder.either(fragments), options.caseSensitive);
|
|
116
|
+
let matcher = null;
|
|
117
|
+
return {
|
|
118
|
+
source,
|
|
119
|
+
dialect: options.dialect,
|
|
120
|
+
caseSensitive: options.caseSensitive,
|
|
121
|
+
bases: [...new Set(bases)],
|
|
122
|
+
automaton,
|
|
123
|
+
match(path) {
|
|
124
|
+
matcher ??= new Matcher(automaton);
|
|
125
|
+
return matcher.test(path);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/* ------------------------------------------------------------------ braces */
|
|
130
|
+
/**
|
|
131
|
+
* Expands `{a,b}` groups into the patterns they stand for, or says why the
|
|
132
|
+
* pattern is malformed.
|
|
133
|
+
*
|
|
134
|
+
* Classes and escapes are skipped while looking for braces and commas, so
|
|
135
|
+
* `{[,]x,y}` is two alternatives, `[,]x` and `y`. A lone `}` is a literal: it
|
|
136
|
+
* cannot be read as anything else. An unclosed `{` is an error.
|
|
137
|
+
*/
|
|
138
|
+
function expandBraces(pattern, escapes) {
|
|
139
|
+
let depth = 0;
|
|
140
|
+
let open = -1;
|
|
141
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
142
|
+
const ch = pattern.charAt(i);
|
|
143
|
+
if (ch === '\\' && escapes) {
|
|
144
|
+
i += 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (ch === '[') {
|
|
148
|
+
const close = classEnd(pattern, i, escapes);
|
|
149
|
+
if (close > 0)
|
|
150
|
+
i = close;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (ch === '{') {
|
|
154
|
+
if (depth === 0)
|
|
155
|
+
open = i;
|
|
156
|
+
depth += 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (ch !== '}' || depth === 0)
|
|
160
|
+
continue;
|
|
161
|
+
depth -= 1;
|
|
162
|
+
if (depth > 0)
|
|
163
|
+
continue;
|
|
164
|
+
const prefix = pattern.slice(0, open);
|
|
165
|
+
const suffix = pattern.slice(i + 1);
|
|
166
|
+
const results = [];
|
|
167
|
+
for (const option of splitOptions(pattern.slice(open + 1, i), escapes)) {
|
|
168
|
+
const expanded = expandBraces(`${prefix}${option}${suffix}`, escapes);
|
|
169
|
+
if (typeof expanded === 'string')
|
|
170
|
+
return expanded;
|
|
171
|
+
results.push(...expanded);
|
|
172
|
+
if (results.length > MAX_ALTERNATIVES)
|
|
173
|
+
return `the braces expand to more than ${MAX_ALTERNATIVES} patterns`;
|
|
174
|
+
}
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
if (depth > 0)
|
|
178
|
+
return 'a "{" is never closed';
|
|
179
|
+
return [pattern];
|
|
180
|
+
}
|
|
181
|
+
/** Splits a brace body on its top-level commas. */
|
|
182
|
+
function splitOptions(body, escapes) {
|
|
183
|
+
const parts = [];
|
|
184
|
+
let depth = 0;
|
|
185
|
+
let start = 0;
|
|
186
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
187
|
+
const ch = body.charAt(i);
|
|
188
|
+
if (ch === '\\' && escapes) {
|
|
189
|
+
i += 1;
|
|
190
|
+
}
|
|
191
|
+
else if (ch === '[') {
|
|
192
|
+
const close = classEnd(body, i, escapes);
|
|
193
|
+
if (close > 0)
|
|
194
|
+
i = close;
|
|
195
|
+
}
|
|
196
|
+
else if (ch === '{') {
|
|
197
|
+
depth += 1;
|
|
198
|
+
}
|
|
199
|
+
else if (ch === '}') {
|
|
200
|
+
depth -= 1;
|
|
201
|
+
}
|
|
202
|
+
else if (ch === ',' && depth === 0) {
|
|
203
|
+
parts.push(body.slice(start, i));
|
|
204
|
+
start = i + 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
parts.push(body.slice(start));
|
|
208
|
+
return parts;
|
|
209
|
+
}
|
|
210
|
+
/** The index of the `]` closing a class opened at `open`, or -1 when none does within the segment. */
|
|
211
|
+
function classEnd(pattern, open, escapes) {
|
|
212
|
+
let i = open + 1;
|
|
213
|
+
if (pattern.charAt(i) === '!' || pattern.charAt(i) === '^')
|
|
214
|
+
i += 1;
|
|
215
|
+
// The first member is always a member, even `]`.
|
|
216
|
+
if (pattern.charAt(i) === ']')
|
|
217
|
+
i += 1;
|
|
218
|
+
for (; i < pattern.length; i += 1) {
|
|
219
|
+
const ch = pattern.charAt(i);
|
|
220
|
+
if (ch === '\\' && escapes) {
|
|
221
|
+
i += 1;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (ch === '/')
|
|
225
|
+
return -1;
|
|
226
|
+
if (ch === ']')
|
|
227
|
+
return i;
|
|
228
|
+
}
|
|
229
|
+
return -1;
|
|
230
|
+
}
|
|
231
|
+
/* ---------------------------------------------------------------- segments */
|
|
232
|
+
function parseAlternative(text, escapes) {
|
|
233
|
+
const segments = [];
|
|
234
|
+
const raw = text.split('/');
|
|
235
|
+
for (const part of raw) {
|
|
236
|
+
if (part === '' || part === '.')
|
|
237
|
+
continue;
|
|
238
|
+
if (part === '..')
|
|
239
|
+
return 'a pattern cannot climb out of its root with ".."';
|
|
240
|
+
if (part === '**') {
|
|
241
|
+
if (segments[segments.length - 1]?.kind !== 'globstar')
|
|
242
|
+
segments.push({ kind: 'globstar' });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const tokens = tokenize(part, escapes);
|
|
246
|
+
if (typeof tokens === 'string')
|
|
247
|
+
return tokens;
|
|
248
|
+
segments.push({ kind: 'name', tokens });
|
|
249
|
+
}
|
|
250
|
+
if (segments.length === 0)
|
|
251
|
+
return 'the pattern names no path';
|
|
252
|
+
return { segments, slashed: raw.filter((part) => part !== '' && part !== '.').length > 1 };
|
|
253
|
+
}
|
|
254
|
+
const EXTGLOB = 'extended globs such as "+(a|b)" are not supported: write alternatives as "{a,b}", and a literal parenthesis as "[(]"';
|
|
255
|
+
const GLOBSTAR_IN_NAME = '"**" means any number of directories only as a whole segment: write "docs/**/*.md" for any depth, or "*.md" for one level';
|
|
256
|
+
function tokenize(segment, escapes) {
|
|
257
|
+
const tokens = [];
|
|
258
|
+
const chars = Array.from(segment);
|
|
259
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
260
|
+
const ch = chars[i];
|
|
261
|
+
if (ch === '\\' && escapes) {
|
|
262
|
+
const next = chars[i + 1];
|
|
263
|
+
if (next === undefined || /[A-Za-z0-9]/.test(next)) {
|
|
264
|
+
return '"\\" escapes glob syntax; separate directories with "/"';
|
|
265
|
+
}
|
|
266
|
+
tokens.push({ kind: 'literal', point: next.codePointAt(0) });
|
|
267
|
+
i += 1;
|
|
268
|
+
}
|
|
269
|
+
else if (ch === '*') {
|
|
270
|
+
// Two stars inside a name are refused rather than read as one. The
|
|
271
|
+
// tools this replaces read `docs/**.md` three ways - recursive, one
|
|
272
|
+
// level, and as ripgrep does - and reading it as one level silently
|
|
273
|
+
// dropped every nested document from a run that used to include them.
|
|
274
|
+
if (chars[i + 1] === '*')
|
|
275
|
+
return GLOBSTAR_IN_NAME;
|
|
276
|
+
tokens.push({ kind: 'star' });
|
|
277
|
+
}
|
|
278
|
+
else if (ch === '?') {
|
|
279
|
+
tokens.push({ kind: 'any' });
|
|
280
|
+
}
|
|
281
|
+
else if (ch === '[') {
|
|
282
|
+
const parsed = readClass(chars, i, escapes);
|
|
283
|
+
if (typeof parsed === 'string')
|
|
284
|
+
return parsed;
|
|
285
|
+
tokens.push(parsed.token);
|
|
286
|
+
i = parsed.end;
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
tokens.push({ kind: 'literal', point: ch.codePointAt(0) });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return tokens;
|
|
293
|
+
}
|
|
294
|
+
function readClass(chars, open, escapes) {
|
|
295
|
+
let i = open + 1;
|
|
296
|
+
let negated = false;
|
|
297
|
+
if (chars[i] === '!' || chars[i] === '^') {
|
|
298
|
+
negated = true;
|
|
299
|
+
i += 1;
|
|
300
|
+
}
|
|
301
|
+
const ranges = [];
|
|
302
|
+
let first = true;
|
|
303
|
+
for (; i < chars.length; i += 1) {
|
|
304
|
+
const ch = chars[i];
|
|
305
|
+
if (ch === ']' && !first)
|
|
306
|
+
return { token: { kind: 'class', negated, ranges }, end: i };
|
|
307
|
+
first = false;
|
|
308
|
+
let low = ch;
|
|
309
|
+
if (ch === '\\' && escapes && chars[i + 1] !== undefined) {
|
|
310
|
+
i += 1;
|
|
311
|
+
low = chars[i];
|
|
312
|
+
}
|
|
313
|
+
let high = low;
|
|
314
|
+
if (chars[i + 1] === '-' && chars[i + 2] !== undefined && chars[i + 2] !== ']') {
|
|
315
|
+
i += 2;
|
|
316
|
+
high = chars[i];
|
|
317
|
+
if (high === '\\' && escapes && chars[i + 1] !== undefined) {
|
|
318
|
+
i += 1;
|
|
319
|
+
high = chars[i];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const from = low.codePointAt(0);
|
|
323
|
+
const to = high.codePointAt(0);
|
|
324
|
+
if (to < from)
|
|
325
|
+
return `the range "${low}-${high}" runs backwards`;
|
|
326
|
+
ranges.push([from, to]);
|
|
327
|
+
}
|
|
328
|
+
return 'a "[" is never closed';
|
|
329
|
+
}
|
|
330
|
+
/* --------------------------------------------------------------- compiling */
|
|
331
|
+
function literalText(segments) {
|
|
332
|
+
const names = [];
|
|
333
|
+
for (const segment of segments) {
|
|
334
|
+
if (segment.kind !== 'name')
|
|
335
|
+
return null;
|
|
336
|
+
let name = '';
|
|
337
|
+
for (const token of segment.tokens) {
|
|
338
|
+
if (token.kind !== 'literal')
|
|
339
|
+
return null;
|
|
340
|
+
name += String.fromCodePoint(token.point);
|
|
341
|
+
}
|
|
342
|
+
names.push(name);
|
|
343
|
+
}
|
|
344
|
+
return names.join('/');
|
|
345
|
+
}
|
|
346
|
+
/** One segment name: at least one character, none of them `/`. */
|
|
347
|
+
function name(b) {
|
|
348
|
+
return b.plus(b.char(ANY));
|
|
349
|
+
}
|
|
350
|
+
/** Zero or more whole directories: `(name/)*`. */
|
|
351
|
+
function anyDirectories(b) {
|
|
352
|
+
return b.star(b.sequence([name(b), b.char(SEPARATOR)]));
|
|
353
|
+
}
|
|
354
|
+
/** One or more whole segments below a path: `/name(/name)*`. */
|
|
355
|
+
function beneath(b) {
|
|
356
|
+
return b.sequence([b.char(SEPARATOR), name(b), b.star(b.sequence([b.char(SEPARATOR), name(b)]))]);
|
|
357
|
+
}
|
|
358
|
+
function tokens(b, list) {
|
|
359
|
+
return b.sequence(list.map((token) => {
|
|
360
|
+
switch (token.kind) {
|
|
361
|
+
case 'literal':
|
|
362
|
+
return b.char(literalSet(token.point));
|
|
363
|
+
case 'any':
|
|
364
|
+
return b.char(ANY);
|
|
365
|
+
case 'star':
|
|
366
|
+
return b.star(b.char(ANY));
|
|
367
|
+
case 'class':
|
|
368
|
+
return b.char(classSet(token.ranges, token.negated));
|
|
369
|
+
}
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
function segmentsFragment(b, segments) {
|
|
373
|
+
const parts = [];
|
|
374
|
+
segments.forEach((segment, i) => {
|
|
375
|
+
const last = i === segments.length - 1;
|
|
376
|
+
if (segment.kind === 'globstar') {
|
|
377
|
+
// Inside a path a globstar is any number of whole directories, the
|
|
378
|
+
// separator after them included; at its end, at least one segment.
|
|
379
|
+
parts.push(last ? b.sequence([name(b), b.star(b.sequence([b.char(SEPARATOR), name(b)]))]) : anyDirectories(b));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
parts.push(tokens(b, segment.tokens));
|
|
383
|
+
if (!last)
|
|
384
|
+
parts.push(b.char(SEPARATOR));
|
|
385
|
+
});
|
|
386
|
+
return b.sequence(parts);
|
|
387
|
+
}
|
|
388
|
+
function compileAlternative(b, alternative, options, rooted, anchored) {
|
|
389
|
+
const { segments } = alternative;
|
|
390
|
+
const body = segmentsFragment(b, segments);
|
|
391
|
+
const root = rooted ? '/' : '';
|
|
392
|
+
const literal = literalText(segments);
|
|
393
|
+
const leading = leadingLiteral(segments);
|
|
394
|
+
if (options.dialect === 'gitignore') {
|
|
395
|
+
const floating = !anchored && !alternative.slashed;
|
|
396
|
+
const parts = [body, b.optional(beneath(b))];
|
|
397
|
+
if (floating)
|
|
398
|
+
parts.unshift(anyDirectories(b));
|
|
399
|
+
const base = literal !== null ? leading.slice(0, -1) : leading;
|
|
400
|
+
return { fragment: b.sequence(parts), base: floating ? '' : base.join('/') };
|
|
401
|
+
}
|
|
402
|
+
if (options.dialect === 'ripgrep' && !rooted && !alternative.slashed) {
|
|
403
|
+
return { fragment: b.sequence([anyDirectories(b), body]), base: '' };
|
|
404
|
+
}
|
|
405
|
+
const parts = rooted ? [b.char(SEPARATOR), body] : [body];
|
|
406
|
+
if (options.dialect === 'path' && literal !== null) {
|
|
407
|
+
const reading = typeof options.literal === 'function' ? options.literal(root + literal) : options.literal ?? 'either';
|
|
408
|
+
if (reading === 'directory') {
|
|
409
|
+
parts.push(beneath(b));
|
|
410
|
+
return { fragment: b.sequence(parts), base: root + literal };
|
|
411
|
+
}
|
|
412
|
+
if (reading === 'either')
|
|
413
|
+
parts.push(b.optional(beneath(b)));
|
|
414
|
+
}
|
|
415
|
+
// A literal path may be a file, whose base is the directory holding it.
|
|
416
|
+
const base = literal !== null ? leading.slice(0, -1) : leading;
|
|
417
|
+
return { fragment: b.sequence(parts), base: root + base.join('/') };
|
|
418
|
+
}
|
|
419
|
+
function leadingLiteral(segments) {
|
|
420
|
+
const out = [];
|
|
421
|
+
for (const segment of segments) {
|
|
422
|
+
const text = literalText([segment]);
|
|
423
|
+
if (text === null)
|
|
424
|
+
break;
|
|
425
|
+
out.push(text);
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
/* ---------------------------------------------------------------- questions */
|
|
430
|
+
function assertComparable(globs) {
|
|
431
|
+
for (const glob of globs) {
|
|
432
|
+
if (!glob.caseSensitive)
|
|
433
|
+
throw new Error(`"${glob.source}" ignores case; scopes are compared case-sensitively`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* A shortest path every glob in `include` matches and no glob in `exclude`
|
|
438
|
+
* does: the file two scopes would both write, less what either protects.
|
|
439
|
+
* `none` is a proof that there is no such path. Case-sensitive globs only.
|
|
440
|
+
*/
|
|
441
|
+
export function globWitness(include, exclude = [], budget = WITNESS_BUDGET) {
|
|
442
|
+
assertComparable([...include, ...exclude]);
|
|
443
|
+
return findWitness(include.map((glob) => glob.automaton), exclude.map((glob) => glob.automaton), budget);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Whether every path `inner` matches is matched by one of `outer`: `true`,
|
|
447
|
+
* `false`, or `undecided` when the search met its budget.
|
|
448
|
+
*/
|
|
449
|
+
export function globCovers(outer, inner, budget = WITNESS_BUDGET) {
|
|
450
|
+
const witness = globWitness([inner], outer, budget);
|
|
451
|
+
return witness.kind === 'undecided' ? 'undecided' : witness.kind === 'none';
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* A list of patterns read the way a `.gitignore` reads: in order, each entry
|
|
455
|
+
* deciding the paths it matches, a leading `!` taking them back out.
|
|
456
|
+
*/
|
|
457
|
+
export function parseGlobList(patterns, options) {
|
|
458
|
+
const entries = [];
|
|
459
|
+
for (const pattern of patterns) {
|
|
460
|
+
const trimmed = pattern.trim();
|
|
461
|
+
const negated = trimmed.startsWith('!');
|
|
462
|
+
const parsed = parseGlob(negated ? trimmed.slice(1) : trimmed, options);
|
|
463
|
+
if (!parsed.ok)
|
|
464
|
+
return { ok: false, error: `"${pattern}": ${parsed.error}` };
|
|
465
|
+
entries.push({ negated, glob: parsed.glob });
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
ok: true,
|
|
469
|
+
list: {
|
|
470
|
+
entries,
|
|
471
|
+
match(path) {
|
|
472
|
+
let included = false;
|
|
473
|
+
for (const entry of entries)
|
|
474
|
+
if (entry.glob.match(path))
|
|
475
|
+
included = !entry.negated;
|
|
476
|
+
return included;
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
//# sourceMappingURL=glob.js.map
|