@descent-vtt/spec-brief 0.2.7 → 0.4.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/CHANGELOG.md +99 -1
- package/README.md +161 -28
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +61 -43
- package/dist/cli.js.map +1 -1
- package/dist/collisions.js +13 -8
- package/dist/collisions.js.map +1 -1
- package/dist/corpus.d.ts +5 -0
- package/dist/corpus.js +7 -0
- package/dist/corpus.js.map +1 -1
- package/dist/glob.d.ts +6 -5
- package/dist/glob.js +22 -103
- package/dist/glob.js.map +1 -1
- package/dist/lint.js +1 -0
- package/dist/lint.js.map +1 -1
- package/dist/report.d.ts +18 -4
- package/dist/report.js +99 -28
- package/dist/report.js.map +1 -1
- package/dist/rules.d.ts +2 -0
- package/dist/rules.js +31 -9
- package/dist/rules.js.map +1 -1
- package/dist/schedule.js +12 -10
- package/dist/schedule.js.map +1 -1
- package/dist/schema.d.ts +5 -1
- package/dist/schema.js +6 -2
- package/dist/schema.js.map +1 -1
- package/dist/scope.d.ts +26 -4
- package/dist/scope.js +64 -8
- package/dist/scope.js.map +1 -1
- package/dist/trigger.d.ts +2 -1
- package/dist/trigger.js +2 -1
- package/dist/trigger.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.js.map +1 -1
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts +11 -1
- package/dist/vendor/spec-core/markdown/frontmatter.js +20 -3
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
- package/dist/vendor/spec-core/pattern/glob.d.ts +85 -0
- package/dist/vendor/spec-core/pattern/glob.js +207 -17
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -1
- package/dist/vendor/spec-core/pattern/index.d.ts +2 -2
- package/dist/vendor/spec-core/pattern/index.js +1 -1
- package/dist/vendor/spec-core/pattern/index.js.map +1 -1
- package/dist/vendor/spec-core/text/index.d.ts +1 -0
- package/dist/vendor/spec-core/text/index.js +1 -0
- package/dist/vendor/spec-core/text/index.js.map +1 -1
- package/dist/vendor/spec-core/text/width.d.ts +30 -0
- package/dist/vendor/spec-core/text/width.js +113 -0
- package/dist/vendor/spec-core/text/width.js.map +1 -0
- package/package.json +1 -1
package/dist/glob.js
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* spec-brief adds around it:
|
|
9
9
|
*
|
|
10
10
|
* - A pattern is relative to the repository root, so a leading `/` is refused
|
|
11
|
-
* rather than rooted at the filesystem: on the pattern,
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* rather than rooted at the filesystem: on the pattern, and on a text its
|
|
12
|
+
* braces give, since spec-core reads `{/docs,x}` as `/docs` or `x`, and
|
|
13
|
+
* `docs` would fall out of the scope unsaid. A leading `./` goes with the
|
|
14
|
+
* slashes after it, as POSIX reads them: `.//docs` is `docs`, and
|
|
15
|
+
* `./{/docs,x}` is `docs` or `x`. A list entry's `!` has no list to belong
|
|
16
|
+
* to in a scope, so it is refused rather than read.
|
|
16
17
|
* - Paths compare case-sensitively on every host, as git's do.
|
|
17
18
|
* - A path with no glob syntax is read as the tree reads it: a file the tree
|
|
18
19
|
* holds is that file, a directory it holds is everything beneath it, and a
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
* size times the path's length, and a question about two scopes that cannot be
|
|
29
30
|
* answered within the search's budget answers `undecided`, never a guess.
|
|
30
31
|
*/
|
|
31
|
-
import { globCovers as coreCovers, globWitness as coreWitness, isGlobSyntax, parseGlob as coreParse, WITNESS_BUDGET, } from './vendor/spec-core/pattern/index.js';
|
|
32
|
+
import { globAlternatives, globCovers as coreCovers, globWitness as coreWitness, isGlobSyntax, parseGlob as coreParse, WITNESS_BUDGET, } from './vendor/spec-core/pattern/index.js';
|
|
32
33
|
import { segments } from './vendor/spec-core/path/index.js';
|
|
33
34
|
export { isGlobSyntax, WITNESS_BUDGET };
|
|
34
35
|
/** Parses a scope pattern, or says why it cannot. */
|
|
@@ -36,7 +37,15 @@ export function parseGlob(source, options = {}) {
|
|
|
36
37
|
const pattern = source.trim();
|
|
37
38
|
if (pattern.startsWith('!'))
|
|
38
39
|
return { ok: false, error: 'negated patterns are not supported; narrow the positive pattern' };
|
|
39
|
-
|
|
40
|
+
// spec-core's own reading of where each alternative starts, a leading `./`
|
|
41
|
+
// and `/` included (spec-core ADR-0003). A pattern it refuses, the core's
|
|
42
|
+
// parse refuses in the same words, so either may say so. It comes first so
|
|
43
|
+
// that a pattern led by `/`, which the core roots at the filesystem's root,
|
|
44
|
+
// is refused in spec-brief's words.
|
|
45
|
+
const read = globAlternatives(pattern);
|
|
46
|
+
if (!read.ok)
|
|
47
|
+
return { ok: false, error: read.error };
|
|
48
|
+
if (read.rooted)
|
|
40
49
|
return { ok: false, error: 'a pattern is relative to the repository root and cannot start with "/"' };
|
|
41
50
|
const given = options.literal ?? 'file';
|
|
42
51
|
const literals = [];
|
|
@@ -50,106 +59,16 @@ export function parseGlob(source, options = {}) {
|
|
|
50
59
|
const parsed = coreParse(pattern, { dialect: 'path', caseSensitive: true, literal });
|
|
51
60
|
if (!parsed.ok)
|
|
52
61
|
return parsed;
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
62
|
+
// Once the core has parsed the pattern, so that an alternative naming no
|
|
63
|
+
// path, `{/,x}`, is refused in the core's words. The text is named behind
|
|
64
|
+
// the `/` that roots it, as spec-core gives it: it reads the same written
|
|
65
|
+
// alone, so a `}` or `,` no group took is written as a class, `[}]`.
|
|
66
|
+
const rooted = read.alternatives.find((alternative) => alternative.rooted);
|
|
56
67
|
if (rooted !== undefined) {
|
|
57
|
-
return { ok: false, error: `a pattern is relative to the repository root, and the braces expand to "
|
|
68
|
+
return { ok: false, error: `a pattern is relative to the repository root, and the braces expand to "/${rooted.text}", which starts with "/"` };
|
|
58
69
|
}
|
|
59
70
|
return { ok: true, glob: { source, compiled: parsed.glob, literals } };
|
|
60
71
|
}
|
|
61
|
-
/**
|
|
62
|
-
* A pattern less the `./` it starts with. spec-core drops it before it reads
|
|
63
|
-
* a leading `/`, since `.` names nothing: `.//docs` is `/docs`, rooted at the
|
|
64
|
-
* filesystem's root.
|
|
65
|
-
*/
|
|
66
|
-
function undotted(pattern) {
|
|
67
|
-
let rest = pattern;
|
|
68
|
-
while (rest.startsWith('./'))
|
|
69
|
-
rest = rest.slice(2);
|
|
70
|
-
return rest;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* The first text a pattern's braces give that starts with `/` once its `./`
|
|
74
|
-
* is dropped, less that `./`: an alternative spec-core roots at the
|
|
75
|
-
* filesystem's root, since braces expand before a leading `/` is read, so
|
|
76
|
-
* that `{/docs,x}` is `/docs` or `x` (spec-core ADR-0003, amended
|
|
77
|
-
* 2026-09-29). `undefined` when there is none. A `/` after anything else
|
|
78
|
-
* starts no text: `a/{/b,c}` gives `a//b`, which is `a/b`.
|
|
79
|
-
*
|
|
80
|
-
* spec-core keeps nothing of the texts its braces give, and the refusal names
|
|
81
|
-
* the text, as the core's own refusals do. So this expands braces as the core
|
|
82
|
-
* does, for a pattern the core parsed, whose braces close and give at most
|
|
83
|
-
* `MAX_ALTERNATIVES` texts: a `\` escapes the next character, a class holds
|
|
84
|
-
* no brace syntax, a `}` with no `{` open is a literal, and the first braces
|
|
85
|
-
* to close are expanded first, each option in turn, so the text named is the
|
|
86
|
-
* first such text the core gives.
|
|
87
|
-
*
|
|
88
|
-
* Its loop, read one step past the end, reads `''`, which no branch acts on,
|
|
89
|
-
* so a bound one further reads the same.
|
|
90
|
-
*/
|
|
91
|
-
function rootedAlternative(pattern) {
|
|
92
|
-
let depth = 0;
|
|
93
|
-
let open = 0;
|
|
94
|
-
const stops = [];
|
|
95
|
-
for (let i = 0; i < pattern.length; i += 1) {
|
|
96
|
-
const ch = pattern.charAt(i);
|
|
97
|
-
if (ch === '\\') {
|
|
98
|
-
i += 1;
|
|
99
|
-
}
|
|
100
|
-
else if (ch === '[') {
|
|
101
|
-
const close = classEnd(pattern, i);
|
|
102
|
-
if (close !== -1)
|
|
103
|
-
i = close;
|
|
104
|
-
}
|
|
105
|
-
else if (ch === '{') {
|
|
106
|
-
if (depth === 0)
|
|
107
|
-
open = i;
|
|
108
|
-
depth += 1;
|
|
109
|
-
}
|
|
110
|
-
else if (ch === ',' && depth === 1) {
|
|
111
|
-
stops.push(i);
|
|
112
|
-
}
|
|
113
|
-
else if (ch === '}' && depth > 0) {
|
|
114
|
-
depth -= 1;
|
|
115
|
-
if (depth === 0) {
|
|
116
|
-
let start = open + 1;
|
|
117
|
-
for (const stop of [...stops, i]) {
|
|
118
|
-
const rooted = rootedAlternative(`${pattern.slice(0, open)}${pattern.slice(start, stop)}${pattern.slice(i + 1)}`);
|
|
119
|
-
if (rooted !== undefined)
|
|
120
|
-
return rooted;
|
|
121
|
-
start = stop + 1;
|
|
122
|
-
}
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const text = undotted(pattern);
|
|
128
|
-
return text.startsWith('/') ? text : undefined;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* The index of the `]` closing a class opened at `open`, or -1 when none does
|
|
132
|
-
* within its segment, as spec-core finds it while it expands braces: after a
|
|
133
|
-
* `!` or `^`, the first member is a member even when it is `]`.
|
|
134
|
-
*/
|
|
135
|
-
function classEnd(pattern, open) {
|
|
136
|
-
let i = open + 1;
|
|
137
|
-
if (pattern.charAt(i) === '!' || pattern.charAt(i) === '^')
|
|
138
|
-
i += 1;
|
|
139
|
-
if (pattern.charAt(i) === ']')
|
|
140
|
-
i += 1;
|
|
141
|
-
for (; i < pattern.length; i += 1) {
|
|
142
|
-
const ch = pattern.charAt(i);
|
|
143
|
-
if (ch === '\\')
|
|
144
|
-
i += 1;
|
|
145
|
-
else if (ch === '/')
|
|
146
|
-
return -1;
|
|
147
|
-
else if (ch === ']')
|
|
148
|
-
return i;
|
|
149
|
-
}
|
|
150
|
-
/* v8 ignore next -- in a pattern the core parsed, a "[" is closed, or met by a "/", before the pattern ends. */
|
|
151
|
-
return -1;
|
|
152
|
-
}
|
|
153
72
|
/**
|
|
154
73
|
* Whether a path matches. The path is read as a repository path: empty and
|
|
155
74
|
* `.` segments are dropped, so `./src//a.ts` is `src/a.ts`.
|
package/dist/glob.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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: on the pattern, one after a leading\n * `./` too, as in `.//docs`, and on a text its braces give, since spec-core\n * reads `{/docs,x}` as `/docs` or `x`, and `docs` would fall out of the\n * scope unsaid. A list entry's `!` has no list to belong to in a scope, so\n * 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 (undotted(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 if (!parsed.ok) return parsed;\n // Read once the core has parsed the pattern, so that its braces close and\n // give no more texts than the core allows.\n const rooted = rootedAlternative(pattern);\n if (rooted !== undefined) {\n return { ok: false, error: `a pattern is relative to the repository root, and the braces expand to \"${rooted}\", which starts with \"/\"` };\n }\n return { ok: true, glob: { source, compiled: parsed.glob, literals } };\n}\n\n/**\n * A pattern less the `./` it starts with. spec-core drops it before it reads\n * a leading `/`, since `.` names nothing: `.//docs` is `/docs`, rooted at the\n * filesystem's root.\n */\nfunction undotted(pattern: string): string {\n let rest = pattern;\n while (rest.startsWith('./')) rest = rest.slice(2);\n return rest;\n}\n\n/**\n * The first text a pattern's braces give that starts with `/` once its `./`\n * is dropped, less that `./`: an alternative spec-core roots at the\n * filesystem's root, since braces expand before a leading `/` is read, so\n * that `{/docs,x}` is `/docs` or `x` (spec-core ADR-0003, amended\n * 2026-09-29). `undefined` when there is none. A `/` after anything else\n * starts no text: `a/{/b,c}` gives `a//b`, which is `a/b`.\n *\n * spec-core keeps nothing of the texts its braces give, and the refusal names\n * the text, as the core's own refusals do. So this expands braces as the core\n * does, for a pattern the core parsed, whose braces close and give at most\n * `MAX_ALTERNATIVES` texts: a `\\` escapes the next character, a class holds\n * no brace syntax, a `}` with no `{` open is a literal, and the first braces\n * to close are expanded first, each option in turn, so the text named is the\n * first such text the core gives.\n *\n * Its loop, read one step past the end, reads `''`, which no branch acts on,\n * so a bound one further reads the same.\n */\nfunction rootedAlternative(pattern: string): string | undefined {\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 const close = classEnd(pattern, i);\n if (close !== -1) i = close;\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) {\n let start = open + 1;\n for (const stop of [...stops, i]) {\n const rooted = rootedAlternative(`${pattern.slice(0, open)}${pattern.slice(start, stop)}${pattern.slice(i + 1)}`);\n if (rooted !== undefined) return rooted;\n start = stop + 1;\n }\n return undefined;\n }\n }\n }\n const text = undotted(pattern);\n return text.startsWith('/') ? text : undefined;\n}\n\n/**\n * The index of the `]` closing a class opened at `open`, or -1 when none does\n * within its segment, as spec-core finds it while it expands braces: after a\n * `!` or `^`, the first member is a member even when it is `]`.\n */\nfunction classEnd(pattern: string, open: number): number {\n let i = open + 1;\n if (pattern.charAt(i) === '!' || pattern.charAt(i) === '^') i += 1;\n if (pattern.charAt(i) === ']') i += 1;\n for (; i < pattern.length; i += 1) {\n const ch = pattern.charAt(i);\n if (ch === '\\\\') i += 1;\n else if (ch === '/') return -1;\n else if (ch === ']') return i;\n }\n /* v8 ignore next -- in a pattern the core parsed, a \"[\" is closed, or met by a \"/\", before the pattern ends. */\n return -1;\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"]}
|
|
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: on the pattern, and on a text its\n * braces give, since spec-core reads `{/docs,x}` as `/docs` or `x`, and\n * `docs` would fall out of the scope unsaid. A leading `./` goes with the\n * slashes after it, as POSIX reads them: `.//docs` is `docs`, and\n * `./{/docs,x}` is `docs` or `x`. A list entry's `!` has no list to belong\n * 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 globAlternatives,\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 // spec-core's own reading of where each alternative starts, a leading `./`\n // and `/` included (spec-core ADR-0003). A pattern it refuses, the core's\n // parse refuses in the same words, so either may say so. It comes first so\n // that a pattern led by `/`, which the core roots at the filesystem's root,\n // is refused in spec-brief's words.\n const read = globAlternatives(pattern);\n if (!read.ok) return { ok: false, error: read.error };\n if (read.rooted) 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 if (!parsed.ok) return parsed;\n // Once the core has parsed the pattern, so that an alternative naming no\n // path, `{/,x}`, is refused in the core's words. The text is named behind\n // the `/` that roots it, as spec-core gives it: it reads the same written\n // alone, so a `}` or `,` no group took is written as a class, `[}]`.\n const rooted = read.alternatives.find((alternative) => alternative.rooted);\n if (rooted !== undefined) {\n return { ok: false, error: `a pattern is relative to the repository root, and the braces expand to \"/${rooted.text}\", which starts with \"/\"` };\n }\n return { ok: true, glob: { source, compiled: parsed.glob, literals } };\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/lint.js
CHANGED
package/dist/lint.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lint.js","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Running rules: severities from the configuration, plugins beside the\n * built-in rules, findings in a stable order.\n */\n\nimport type { Waiver } from './archive.js';\nimport type { Brief } from './brief.js';\nimport { ConfigError } from './config.js';\nimport type { Corpus } from './corpus.js';\nimport { analyse, ARCHIVE_RULES, COLLISION_RULES, type Rule, type RuleInfo, RULES } from './rules.js';\nimport type { Finding, Severity, SeveritySetting } from './types.js';\n\n/** What a plugin's `waive` hook is asked about: one archival, refused. */\nexport interface WaiveContext {\n /** The absolute root: the directory holding the configuration. */\n readonly root: string;\n readonly brief: { readonly id: string | null; readonly file: string; readonly text: string };\n /**\n * Every finding that refuses the archival, as a frozen copy: writing to it\n * throws, and the waivers are matched against the plan's own. A\n * `protected-file` finding names its `path`; an `out-of-scope` one lists its\n * `paths`. Only those two can be waived.\n */\n readonly findings: readonly Finding[];\n /** The base the round is measured from, as it was given; `null` when none was. */\n readonly base: string | null;\n /** The commit the round landed as; `null` when none was read. */\n readonly commit: string | null;\n}\n\nexport interface Plugin {\n /** Prefixes the plugin's rule ids: `<name>/<rule>`. */\n readonly name: string;\n readonly rules: readonly Rule[];\n /** The plugin's entry in the configuration, handed to each rule. */\n readonly options?: unknown;\n /**\n * Lifts refusals of an archival that the plugin's own check allows - a\n * protected file a verified ruling covers - by rule and path. Asked only\n * when the plan has a refusal it could lift. Returning nothing waives\n * nothing.\n */\n readonly waive?: ((context: WaiveContext) => readonly Waiver[] | void | Promise<readonly Waiver[] | void>) | undefined;\n}\n\nexport interface LintOptions {\n readonly plugins?: readonly Plugin[];\n /** Tracked files, for the rules that read the tree. */\n readonly repoFiles?: readonly string[] | null;\n /** Limit the findings to these briefs. The whole corpus is still read. */\n readonly only?: readonly Brief[];\n}\n\nconst RANK: Readonly<Record<Severity, number>> = { note: 0, warning: 1, error: 2 };\n\nfunction lower(a: Severity, b: Severity): Severity {\n return RANK[a] <= RANK[b] ? a : b;\n}\n\ninterface ActiveRule {\n readonly id: string;\n readonly rule: Rule;\n readonly severity: SeveritySetting;\n readonly options: unknown;\n}\n\n/** Every rule id a run knows: built-in, collision, archive and plugin. */\nexport function ruleIds(plugins: readonly Plugin[] = []): string[] {\n return [\n ...RULES.map((r) => r.id),\n ...COLLISION_RULES.map((r) => r.id),\n ...ARCHIVE_RULES.map((r) => r.id),\n ...plugins.flatMap((p) => p.rules.map((r) => `${p.name}/${r.id}`)),\n ];\n}\n\n/** The severity configuration assigns a rule, or its own. */\nexport function severityOf(corpus: Corpus, id: string, fallback: SeveritySetting): SeveritySetting {\n return corpus.config.rules[id] ?? fallback;\n}\n\n/** The severity of a built-in rule as configured, or `null` when it is off. */\nexport function configuredSeverity(corpus: Corpus, id: string): Severity | null {\n const rule = [...RULES, ...COLLISION_RULES, ...ARCHIVE_RULES].find((r) => r.id === id) as RuleInfo;\n const setting = severityOf(corpus, id, rule.severity);\n return setting === 'off' ? null : setting;\n}\n\n/** Refuses a configuration that names a rule nobody defines: a typo there silences nothing. */\nexport function checkRuleIds(corpus: Corpus, plugins: readonly Plugin[] = []): void {\n const known = new Set(ruleIds(plugins));\n const unknown = Object.keys(corpus.config.rules).filter((id) => !known.has(id));\n if (unknown.length > 0) {\n throw new ConfigError('configuration', unknown.map((id) => `\"rules.${id}\" names no rule`));\n }\n}\n\nexport async function lint(corpus: Corpus, options: LintOptions = {}): Promise<Finding[]> {\n const plugins = options.plugins ?? [];\n checkRuleIds(corpus, plugins);\n const active: ActiveRule[] = [\n ...RULES.map((rule) => ({ id: rule.id, rule, severity: severityOf(corpus, rule.id, rule.severity), options: undefined })),\n ...plugins.flatMap((plugin) =>\n plugin.rules.map((rule) => {\n const id = `${plugin.name}/${rule.id}`;\n return { id, rule, severity: severityOf(corpus, id, rule.severity), options: plugin.options };\n }),\n ),\n ].filter((r) => r.severity !== 'off');\n\n const shared = analyse(corpus);\n const targets = options.only ?? corpus.briefs;\n const findings: Finding[] = [];\n for (const brief of targets) {\n for (const { id, rule, severity, options: ruleOptions } of active) {\n const results = await rule.check({\n brief,\n corpus,\n config: corpus.config,\n repoFiles: options.repoFiles ?? null,\n shared,\n options: ruleOptions,\n });\n for (const result of results) {\n const configured = severity as Severity;\n findings.push({\n rule: id,\n severity: result.severity === undefined ? configured : lower(result.severity, configured),\n message: result.message,\n file: brief.file,\n line: Math.max(1, Math.trunc(result.line)),\n brief: brief.id ?? undefined,\n hint: result.hint,\n });\n }\n }\n }\n return sortFindings(findings);\n}\n\n/** Strings by code unit, which is the same order on every host and in every locale. */\nfunction order(a: string, b: string): number {\n if (a === b) return 0;\n return a < b ? -1 : 1;\n}\n\nexport function sortFindings(findings: readonly Finding[]): Finding[] {\n return [...findings].sort(\n (a, b) => order(a.file, b.file) || a.line - b.line || order(a.rule, b.rule) || order(a.message, b.message),\n );\n}\n\nexport interface Summary {\n readonly errors: number;\n readonly warnings: number;\n readonly notes: number;\n}\n\nexport function summarise(findings: readonly Finding[]): Summary {\n return {\n errors: findings.filter((f) => f.severity === 'error').length,\n warnings: findings.filter((f) => f.severity === 'warning').length,\n notes: findings.filter((f) => f.severity === 'note').length,\n };\n}\n\n/** Whether findings fail a run: any error, or any warning under `--strict`. */\nexport function failing(findings: readonly Finding[], strict: boolean): boolean {\n return findings.some((f) => f.severity === 'error' || (strict && f.severity === 'warning'));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"lint.js","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Running rules: severities from the configuration, plugins beside the\n * built-in rules, findings in a stable order.\n */\n\nimport type { Waiver } from './archive.js';\nimport type { Brief } from './brief.js';\nimport { ConfigError } from './config.js';\nimport type { Corpus } from './corpus.js';\nimport { analyse, ARCHIVE_RULES, COLLISION_RULES, type Rule, type RuleInfo, RULES } from './rules.js';\nimport type { Finding, Severity, SeveritySetting } from './types.js';\n\n/** What a plugin's `waive` hook is asked about: one archival, refused. */\nexport interface WaiveContext {\n /** The absolute root: the directory holding the configuration. */\n readonly root: string;\n readonly brief: { readonly id: string | null; readonly file: string; readonly text: string };\n /**\n * Every finding that refuses the archival, as a frozen copy: writing to it\n * throws, and the waivers are matched against the plan's own. A\n * `protected-file` finding names its `path`; an `out-of-scope` one lists its\n * `paths`. Only those two can be waived.\n */\n readonly findings: readonly Finding[];\n /** The base the round is measured from, as it was given; `null` when none was. */\n readonly base: string | null;\n /** The commit the round landed as; `null` when none was read. */\n readonly commit: string | null;\n}\n\nexport interface Plugin {\n /** Prefixes the plugin's rule ids: `<name>/<rule>`. */\n readonly name: string;\n readonly rules: readonly Rule[];\n /** The plugin's entry in the configuration, handed to each rule. */\n readonly options?: unknown;\n /**\n * Lifts refusals of an archival that the plugin's own check allows - a\n * protected file a verified ruling covers - by rule and path. Asked only\n * when the plan has a refusal it could lift. Returning nothing waives\n * nothing.\n */\n readonly waive?: ((context: WaiveContext) => readonly Waiver[] | void | Promise<readonly Waiver[] | void>) | undefined;\n}\n\nexport interface LintOptions {\n readonly plugins?: readonly Plugin[];\n /** Tracked files, for the rules that read the tree. */\n readonly repoFiles?: readonly string[] | null;\n /** Limit the findings to these briefs. The whole corpus is still read. */\n readonly only?: readonly Brief[];\n}\n\nconst RANK: Readonly<Record<Severity, number>> = { note: 0, warning: 1, error: 2 };\n\nfunction lower(a: Severity, b: Severity): Severity {\n return RANK[a] <= RANK[b] ? a : b;\n}\n\ninterface ActiveRule {\n readonly id: string;\n readonly rule: Rule;\n readonly severity: SeveritySetting;\n readonly options: unknown;\n}\n\n/** Every rule id a run knows: built-in, collision, archive and plugin. */\nexport function ruleIds(plugins: readonly Plugin[] = []): string[] {\n return [\n ...RULES.map((r) => r.id),\n ...COLLISION_RULES.map((r) => r.id),\n ...ARCHIVE_RULES.map((r) => r.id),\n ...plugins.flatMap((p) => p.rules.map((r) => `${p.name}/${r.id}`)),\n ];\n}\n\n/** The severity configuration assigns a rule, or its own. */\nexport function severityOf(corpus: Corpus, id: string, fallback: SeveritySetting): SeveritySetting {\n return corpus.config.rules[id] ?? fallback;\n}\n\n/** The severity of a built-in rule as configured, or `null` when it is off. */\nexport function configuredSeverity(corpus: Corpus, id: string): Severity | null {\n const rule = [...RULES, ...COLLISION_RULES, ...ARCHIVE_RULES].find((r) => r.id === id) as RuleInfo;\n const setting = severityOf(corpus, id, rule.severity);\n return setting === 'off' ? null : setting;\n}\n\n/** Refuses a configuration that names a rule nobody defines: a typo there silences nothing. */\nexport function checkRuleIds(corpus: Corpus, plugins: readonly Plugin[] = []): void {\n const known = new Set(ruleIds(plugins));\n const unknown = Object.keys(corpus.config.rules).filter((id) => !known.has(id));\n if (unknown.length > 0) {\n throw new ConfigError('configuration', unknown.map((id) => `\"rules.${id}\" names no rule`));\n }\n}\n\nexport async function lint(corpus: Corpus, options: LintOptions = {}): Promise<Finding[]> {\n const plugins = options.plugins ?? [];\n checkRuleIds(corpus, plugins);\n const active: ActiveRule[] = [\n ...RULES.map((rule) => ({ id: rule.id, rule, severity: severityOf(corpus, rule.id, rule.severity), options: undefined })),\n ...plugins.flatMap((plugin) =>\n plugin.rules.map((rule) => {\n const id = `${plugin.name}/${rule.id}`;\n return { id, rule, severity: severityOf(corpus, id, rule.severity), options: plugin.options };\n }),\n ),\n ].filter((r) => r.severity !== 'off');\n\n const shared = analyse(corpus);\n const targets = options.only ?? corpus.briefs;\n const findings: Finding[] = [];\n for (const brief of targets) {\n for (const { id, rule, severity, options: ruleOptions } of active) {\n const results = await rule.check({\n brief,\n corpus,\n config: corpus.config,\n repoFiles: options.repoFiles ?? null,\n shared,\n options: ruleOptions,\n });\n for (const result of results) {\n const configured = severity as Severity;\n findings.push({\n rule: id,\n severity: result.severity === undefined ? configured : lower(result.severity, configured),\n message: result.message,\n file: brief.file,\n line: Math.max(1, Math.trunc(result.line)),\n brief: brief.id ?? undefined,\n hint: result.hint,\n subject: result.subject,\n });\n }\n }\n }\n return sortFindings(findings);\n}\n\n/** Strings by code unit, which is the same order on every host and in every locale. */\nfunction order(a: string, b: string): number {\n if (a === b) return 0;\n return a < b ? -1 : 1;\n}\n\nexport function sortFindings(findings: readonly Finding[]): Finding[] {\n return [...findings].sort(\n (a, b) => order(a.file, b.file) || a.line - b.line || order(a.rule, b.rule) || order(a.message, b.message),\n );\n}\n\nexport interface Summary {\n readonly errors: number;\n readonly warnings: number;\n readonly notes: number;\n}\n\nexport function summarise(findings: readonly Finding[]): Summary {\n return {\n errors: findings.filter((f) => f.severity === 'error').length,\n warnings: findings.filter((f) => f.severity === 'warning').length,\n notes: findings.filter((f) => f.severity === 'note').length,\n };\n}\n\n/** Whether findings fail a run: any error, or any warning under `--strict`. */\nexport function failing(findings: readonly Finding[], strict: boolean): boolean {\n return findings.some((f) => f.severity === 'error' || (strict && f.severity === 'warning'));\n}\n"]}
|
package/dist/report.d.ts
CHANGED
|
@@ -38,10 +38,14 @@ export declare function sarif(findings: readonly Finding[], version: string): st
|
|
|
38
38
|
/**
|
|
39
39
|
* GitLab Code Quality: a JSON array of issues, which a merge request shows
|
|
40
40
|
* beside the lines they are on. The fingerprint is how GitLab tells a new
|
|
41
|
-
* issue from one it has seen, so it hashes the
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* issue from one it has seen, so it hashes the finding's identity and nothing
|
|
42
|
+
* that changes while the problem stays: not the line, so a finding that moves
|
|
43
|
+
* down a page is the same finding, and not the message, so rewording one, or
|
|
44
|
+
* naming another file two colliding briefs both write, does not show as one
|
|
45
|
+
* problem fixed and another found. Findings that share an identity - a front
|
|
46
|
+
* matter with two problems - are told apart by their order, the second hashed
|
|
47
|
+
* with a `2`, as spec-graph hashes them. The description carries the next
|
|
48
|
+
* action as well as the message.
|
|
45
49
|
*/
|
|
46
50
|
export declare function gitlabCodeQuality(findings: readonly Finding[]): string;
|
|
47
51
|
export declare function githubCommands(findings: readonly Finding[]): string;
|
|
@@ -53,6 +57,16 @@ export declare function briefJson(brief: Brief, extra?: Record<string, unknown>)
|
|
|
53
57
|
* knows in advance.
|
|
54
58
|
*/
|
|
55
59
|
export declare function sectionsJson(config: Config): Record<string, unknown>[];
|
|
60
|
+
/**
|
|
61
|
+
* A title as a table shows it, without the id it repeats: `new` writes
|
|
62
|
+
* `# 012 — Rotate tokens`, and the table has an id column already. Only the
|
|
63
|
+
* brief's own id comes off, compared as ids are, so that `12 - x` in brief
|
|
64
|
+
* `012` loses it and `0010 — x` in brief `001` keeps it, and only before a
|
|
65
|
+
* separator: `Fix - the login bug` and `2026:roadmap` are titles. For
|
|
66
|
+
* display alone: `Brief.title`, and every JSON document, keep the title as
|
|
67
|
+
* written.
|
|
68
|
+
*/
|
|
69
|
+
export declare function titleWithoutId(title: string, id: string | null): string;
|
|
56
70
|
export interface ListRow {
|
|
57
71
|
readonly brief: Brief;
|
|
58
72
|
readonly ready: boolean;
|
package/dist/report.js
CHANGED
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
* so the family is not GitHub's alone.
|
|
10
10
|
*/
|
|
11
11
|
import { createHash } from 'node:crypto';
|
|
12
|
+
import { idKey } from './corpus.js';
|
|
12
13
|
import { summarise } from './lint.js';
|
|
13
14
|
import { ARCHIVE_RULES, COLLISION_RULES, RULES } from './rules.js';
|
|
14
15
|
import { breachReasons, holds, moves, reasons } from './schedule.js';
|
|
16
|
+
import { witnessText } from './scope.js';
|
|
15
17
|
import { inWords } from './text.js';
|
|
18
|
+
import { displayWidth } from './vendor/spec-core/text/index.js';
|
|
16
19
|
export const FORMATS = ['pretty', 'json', 'sarif', 'github', 'gitlab'];
|
|
17
20
|
/**
|
|
18
21
|
* The version of the JSON documents this tool prints, every command's alike.
|
|
@@ -121,29 +124,44 @@ export function sarif(findings, version) {
|
|
|
121
124
|
return `${JSON.stringify(document, null, 2)}\n`;
|
|
122
125
|
}
|
|
123
126
|
/**
|
|
124
|
-
* GitLab's Code Quality severities
|
|
125
|
-
*
|
|
127
|
+
* GitLab's Code Quality severities, as the family maps them: an error is
|
|
128
|
+
* `major`, a warning `minor`, a note `info`. An error fails the run but is not
|
|
129
|
+
* a security hole or a crash, which is what `critical` and `blocker` say in
|
|
126
130
|
* GitLab's own reports, so the scale tops out at `major`.
|
|
127
131
|
*/
|
|
128
132
|
const GITLAB_SEVERITY = { error: 'major', warning: 'minor', note: 'info' };
|
|
133
|
+
/**
|
|
134
|
+
* What a finding is, whatever it says: its rule, its file, its brief, and what
|
|
135
|
+
* it is about there - a section, a pattern, the other brief of a pair, the one
|
|
136
|
+
* path a refusal names. Never the message, the hint or the line.
|
|
137
|
+
*/
|
|
138
|
+
function identity(f) {
|
|
139
|
+
return [f.rule, f.file, f.brief ?? '', f.subject ?? f.path ?? ''];
|
|
140
|
+
}
|
|
129
141
|
/**
|
|
130
142
|
* GitLab Code Quality: a JSON array of issues, which a merge request shows
|
|
131
143
|
* beside the lines they are on. The fingerprint is how GitLab tells a new
|
|
132
|
-
* issue from one it has seen, so it hashes the
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
144
|
+
* issue from one it has seen, so it hashes the finding's identity and nothing
|
|
145
|
+
* that changes while the problem stays: not the line, so a finding that moves
|
|
146
|
+
* down a page is the same finding, and not the message, so rewording one, or
|
|
147
|
+
* naming another file two colliding briefs both write, does not show as one
|
|
148
|
+
* problem fixed and another found. Findings that share an identity - a front
|
|
149
|
+
* matter with two problems - are told apart by their order, the second hashed
|
|
150
|
+
* with a `2`, as spec-graph hashes them. The description carries the next
|
|
151
|
+
* action as well as the message.
|
|
136
152
|
*/
|
|
137
153
|
export function gitlabCodeQuality(findings) {
|
|
138
154
|
const seen = new Map();
|
|
139
155
|
const issues = findings.map((f) => {
|
|
140
|
-
const key =
|
|
141
|
-
const
|
|
142
|
-
seen.set(key,
|
|
156
|
+
const key = JSON.stringify(identity(f));
|
|
157
|
+
const occurrence = (seen.get(key) ?? 0) + 1;
|
|
158
|
+
seen.set(key, occurrence);
|
|
143
159
|
return {
|
|
144
160
|
description: f.hint === undefined ? f.message : `${f.message}. ${f.hint}`,
|
|
145
161
|
check_name: f.rule,
|
|
146
|
-
fingerprint: createHash('sha256')
|
|
162
|
+
fingerprint: createHash('sha256')
|
|
163
|
+
.update(occurrence === 1 ? key : JSON.stringify([...identity(f), occurrence]))
|
|
164
|
+
.digest('hex'),
|
|
147
165
|
severity: GITLAB_SEVERITY[f.severity],
|
|
148
166
|
location: { path: f.file, lines: { begin: f.line } },
|
|
149
167
|
};
|
|
@@ -204,12 +222,57 @@ export function sectionsJson(config) {
|
|
|
204
222
|
...Object.entries(config.types).flatMap(([type, rules]) => rules.map((rule) => describe(rule, type))),
|
|
205
223
|
];
|
|
206
224
|
}
|
|
207
|
-
/**
|
|
225
|
+
/**
|
|
226
|
+
* What may stand between an id and the rest of a title: an em or an en dash,
|
|
227
|
+
* a colon, ASCII or full-width, with or without spaces, or a hyphen with a
|
|
228
|
+
* space on each side, so that `001-2 migration` keeps its id.
|
|
229
|
+
*/
|
|
230
|
+
const ID_SEPARATOR = /^(?:\s*[\u2014\u2013:\uff1a]|\s+-(?=\s))\s*(?=\S)/u;
|
|
231
|
+
/**
|
|
232
|
+
* A title as a table shows it, without the id it repeats: `new` writes
|
|
233
|
+
* `# 012 — Rotate tokens`, and the table has an id column already. Only the
|
|
234
|
+
* brief's own id comes off, compared as ids are, so that `12 - x` in brief
|
|
235
|
+
* `012` loses it and `0010 — x` in brief `001` keeps it, and only before a
|
|
236
|
+
* separator: `Fix - the login bug` and `2026:roadmap` are titles. For
|
|
237
|
+
* display alone: `Brief.title`, and every JSON document, keep the title as
|
|
238
|
+
* written.
|
|
239
|
+
*/
|
|
240
|
+
export function titleWithoutId(title, id) {
|
|
241
|
+
if (id === null)
|
|
242
|
+
return title;
|
|
243
|
+
const text = title.trimStart();
|
|
244
|
+
// An id of digits is compared by value, so the title's own leading digits
|
|
245
|
+
// are the candidate. A title with none has `''`, which no id's key equals;
|
|
246
|
+
// any other text in its place fails `startsWith` and answers the same.
|
|
247
|
+
const lead = /^\d+$/.test(id) ? (/^\d+/.exec(text)?.[0] ?? '') : id;
|
|
248
|
+
if (!text.startsWith(lead) || idKey(lead) !== idKey(id))
|
|
249
|
+
return title;
|
|
250
|
+
const separator = ID_SEPARATOR.exec(text.slice(lead.length));
|
|
251
|
+
return separator === null ? title : text.slice(lead.length + separator[0].length);
|
|
252
|
+
}
|
|
253
|
+
function shownTitle(brief) {
|
|
254
|
+
return brief.title === null ? brief.name : titleWithoutId(brief.title, brief.id);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* A text padded to `width` columns of a terminal. `padEnd` counts UTF-16
|
|
258
|
+
* units, and a Han character is one unit and two columns, so `已接受` padded
|
|
259
|
+
* so fell three columns short. Every width here is the widest of the texts it
|
|
260
|
+
* pads, so none is longer than its width.
|
|
261
|
+
*/
|
|
262
|
+
function padEnd(text, width) {
|
|
263
|
+
return text + ' '.repeat(width - displayWidth(text));
|
|
264
|
+
}
|
|
265
|
+
/** A text padded on the left to `width` columns of a terminal, as {@link padEnd} pads on the right. */
|
|
266
|
+
function padStart(text, width) {
|
|
267
|
+
return ' '.repeat(width - displayWidth(text)) + text;
|
|
268
|
+
}
|
|
269
|
+
/** Columns padded to their widest cell, in a terminal's columns; every row has the header's length. */
|
|
208
270
|
function table(header, rows) {
|
|
209
271
|
const all = [header, ...rows];
|
|
210
|
-
const widths = header.map((_, c) => Math.max(...all.map((r) => r[c]
|
|
272
|
+
const widths = header.map((_, c) => Math.max(...all.map((r) => displayWidth(r[c]))));
|
|
273
|
+
// Padding the last cell would change nothing: trimEnd takes it off again.
|
|
211
274
|
return all.map((r) => r
|
|
212
|
-
.map((cell, c) => (c === r.length - 1 ? cell :
|
|
275
|
+
.map((cell, c) => (c === r.length - 1 ? cell : padEnd(cell, widths[c])))
|
|
213
276
|
.join(' ')
|
|
214
277
|
.trimEnd());
|
|
215
278
|
}
|
|
@@ -218,11 +281,11 @@ export function prettyList(rows, style) {
|
|
|
218
281
|
return 'no briefs';
|
|
219
282
|
const lines = table(['ID', 'STATUS', 'WAVE', 'TASKS', 'READY', 'TITLE'], rows.map(({ brief, ready, waitingOn }) => [
|
|
220
283
|
brief.id ?? '?',
|
|
221
|
-
brief.
|
|
284
|
+
brief.statusWord ?? brief.status ?? '?',
|
|
222
285
|
brief.wave === null ? '-' : String(brief.wave),
|
|
223
286
|
`${brief.tasks.filter((t) => t.checked).length}/${brief.tasks.length}`,
|
|
224
287
|
brief.phase === 'archived' ? '-' : ready ? 'yes' : waitingOn.length > 0 ? `after ${waitingOn.join(', ')}` : 'no',
|
|
225
|
-
brief
|
|
288
|
+
shownTitle(brief),
|
|
226
289
|
]));
|
|
227
290
|
return [paint(style, 'dim', lines[0]), ...lines.slice(1)].join('\n');
|
|
228
291
|
}
|
|
@@ -233,12 +296,12 @@ export function prettyMatrix(report, style) {
|
|
|
233
296
|
const title = wave.wave === null ? 'all live briefs' : `wave ${wave.wave}`;
|
|
234
297
|
out.push(paint(style, 'bold', `${title} \u00b7 ${plural(wave.briefs.length, 'brief')}`));
|
|
235
298
|
const names = wave.briefs.map(label);
|
|
236
|
-
const width = Math.max(3, ...names.map(
|
|
299
|
+
const width = Math.max(3, ...names.map(displayWidth));
|
|
237
300
|
const pairs = (list) => new Set(list.flatMap((p) => [`${label(p.a)}\u0000${label(p.b)}`, `${label(p.b)}\u0000${label(p.a)}`]));
|
|
238
301
|
const hits = pairs(wave.collisions);
|
|
239
302
|
const unknown = pairs(wave.undecided);
|
|
240
303
|
const near = pairs(wave.shared);
|
|
241
|
-
out.push(` ${''.
|
|
304
|
+
out.push(` ${' '.repeat(width)} ${names.map((n) => padStart(n, width)).join(' ')}`);
|
|
242
305
|
for (const row of names) {
|
|
243
306
|
const cells = names.map((col) => {
|
|
244
307
|
const key = `${row}\u0000${col}`;
|
|
@@ -253,12 +316,12 @@ export function prettyMatrix(report, style) {
|
|
|
253
316
|
: '\u00b7';
|
|
254
317
|
return `${' '.repeat(width - 1)}${mark}`;
|
|
255
318
|
});
|
|
256
|
-
out.push(` ${
|
|
319
|
+
out.push(` ${padEnd(row, width)} ${cells.join(' ')}`);
|
|
257
320
|
}
|
|
258
321
|
// One mark per pair of briefs; each further pair of patterns on a line beneath it.
|
|
259
322
|
for (const c of wave.collisions) {
|
|
260
323
|
c.overlaps.forEach((o, i) => {
|
|
261
|
-
out.push(` ${i === 0 ? paint(style, 'red', 'X') : ' '} ${label(c.a)} "${o.patterns[0]}" and ${label(c.b)} "${o.patterns[1]}" both cover ${o
|
|
324
|
+
out.push(` ${i === 0 ? paint(style, 'red', 'X') : ' '} ${label(c.a)} "${o.patterns[0]}" and ${label(c.b)} "${o.patterns[1]}" both cover ${witnessText(o)}`);
|
|
262
325
|
});
|
|
263
326
|
}
|
|
264
327
|
for (const u of wave.undecided) {
|
|
@@ -288,6 +351,15 @@ export function prettyMatrix(report, style) {
|
|
|
288
351
|
out.push('no live briefs');
|
|
289
352
|
return out.join('\n').trimEnd();
|
|
290
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* A pair of patterns that meets, and the file named: `inTree` is `true` for a
|
|
356
|
+
* file the tree holds, `false` for an example the search built, `null` when
|
|
357
|
+
* the tree could not be read. Added to schema 2: a reader of it that does not
|
|
358
|
+
* know the field reads the rest as before.
|
|
359
|
+
*/
|
|
360
|
+
function overlapJson(o) {
|
|
361
|
+
return { patterns: o.patterns, witness: o.witness, inTree: o.inTree };
|
|
362
|
+
}
|
|
291
363
|
/** The matrix as JSON: briefs by id, and a collision per pair of briefs. */
|
|
292
364
|
export function matrixJson(report) {
|
|
293
365
|
return {
|
|
@@ -297,7 +369,7 @@ export function matrixJson(report) {
|
|
|
297
369
|
collisions: w.collisions.map((c) => ({
|
|
298
370
|
a: c.a.id,
|
|
299
371
|
b: c.b.id,
|
|
300
|
-
overlaps: c.overlaps.map(
|
|
372
|
+
overlaps: c.overlaps.map(overlapJson),
|
|
301
373
|
})),
|
|
302
374
|
undecided: w.undecided.map((u) => ({ a: u.a.id, b: u.b.id, patterns: u.patterns })),
|
|
303
375
|
sharedDirectories: w.shared.map((s) => ({ a: s.a.id, b: s.b.id, directories: s.directories })),
|
|
@@ -323,21 +395,20 @@ function wavesOf(s) {
|
|
|
323
395
|
export function prettySchedule(s, written, style) {
|
|
324
396
|
const label = (b) => b.id ?? b.name;
|
|
325
397
|
const out = [];
|
|
326
|
-
const width = Math.max(3, ...s.placements.map((p) => label(p.brief)
|
|
327
|
-
const
|
|
328
|
-
const titles = Math.max(0, ...s.placements.map((p) => titleOf(p.brief).length));
|
|
398
|
+
const width = Math.max(3, ...s.placements.map((p) => displayWidth(label(p.brief))));
|
|
399
|
+
const titles = Math.max(0, ...s.placements.map((p) => displayWidth(shownTitle(p.brief))));
|
|
329
400
|
const waves = wavesOf(s);
|
|
330
401
|
for (const wave of waves) {
|
|
331
402
|
const here = s.placements.filter((p) => p.proposed === wave);
|
|
332
403
|
out.push(paint(style, 'bold', `wave ${wave} · ${plural(here.length, 'brief')}`));
|
|
333
404
|
for (const p of here) {
|
|
334
|
-
const title =
|
|
405
|
+
const title = shownTitle(p.brief);
|
|
335
406
|
if (p.proposed === p.declared) {
|
|
336
|
-
out.push(` ${label(p.brief)
|
|
407
|
+
out.push(` ${padEnd(label(p.brief), width)} ${title}`);
|
|
337
408
|
continue;
|
|
338
409
|
}
|
|
339
410
|
const from = p.declared === null ? 'no wave' : `wave ${p.declared}`;
|
|
340
|
-
out.push(` ${label(p.brief)
|
|
411
|
+
out.push(` ${padEnd(label(p.brief), width)} ${padEnd(title, titles)} ${paint(style, 'yellow', `moves from ${from}`)}`);
|
|
341
412
|
// Why the declared wave holds or does not comes first: it decides whether the move is asked for.
|
|
342
413
|
const verdict = p.declared === null ? [] : holds(p) ? [`wave ${p.declared} also holds`] : [`wave ${p.declared} does not hold: ${breachReasons(p).join('; ')}`];
|
|
343
414
|
for (const reason of [...verdict, ...reasons(p)])
|
|
@@ -379,12 +450,12 @@ export function prettySchedule(s, written, style) {
|
|
|
379
450
|
/** The schedule as JSON: a row per placed brief with its declared and proposed wave and why. */
|
|
380
451
|
export function scheduleJson(s) {
|
|
381
452
|
const breach = (b) => b.reason === 'collision'
|
|
382
|
-
? { reason: b.reason, brief: b.brief.id, overlaps: b.overlaps.map(
|
|
453
|
+
? { reason: b.reason, brief: b.brief.id, overlaps: b.overlaps.map(overlapJson) }
|
|
383
454
|
: { reason: b.reason, brief: b.brief.id, wave: b.wave };
|
|
384
455
|
const passed = (p) => {
|
|
385
456
|
const base = { wave: p.wave, reason: p.reason, brief: p.brief.id };
|
|
386
457
|
if (p.reason === 'collision')
|
|
387
|
-
return { ...base, overlaps: p.overlaps.map(
|
|
458
|
+
return { ...base, overlaps: p.overlaps.map(overlapJson) };
|
|
388
459
|
if (p.reason === 'undecided')
|
|
389
460
|
return { ...base, patterns: p.patterns };
|
|
390
461
|
return base;
|