@leonardo-marziali/ad-lfl-kit 1.5.1 → 1.7.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/README.md +78 -0
- package/index.js +1 -0
- package/package.json +2 -1
- package/scope.js +334 -0
package/README.md
CHANGED
|
@@ -118,6 +118,55 @@ failing → block with a message Claude can act on, up to 3 attempts by
|
|
|
118
118
|
default; still failing at the cap → `onCap` runs once, then the stop is
|
|
119
119
|
allowed anyway.
|
|
120
120
|
|
|
121
|
+
## Scoping to changes against HEAD
|
|
122
|
+
|
|
123
|
+
Tracking a file is not the same as being answerable for all of it. By
|
|
124
|
+
default `runFixLoop` checks every touched file whole, which means a one-line
|
|
125
|
+
edit to a 400-line README makes every pre-existing violation in it the
|
|
126
|
+
session's problem. `scope.js` narrows that to the lines the working tree
|
|
127
|
+
actually changed:
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
const { scopesFor, partitionByScope, fixableFiles } = require('@leonardo-marziali/ad-lfl-kit');
|
|
131
|
+
|
|
132
|
+
const scopes = scopesFor(files); // → Map<absPath, scope>, vs HEAD, ±2 lines
|
|
133
|
+
const { inScope, outOfScope, outOfScopeFiles } = partitionByScope(violations, {
|
|
134
|
+
scopes,
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The gate this produces is **"uncommitted work must not introduce lint
|
|
139
|
+
errors"**, not "this session must not". `HEAD` is the last known-clean
|
|
140
|
+
state, so anything dirty is fair game regardless of what dirtied it, and
|
|
141
|
+
anything committed is somebody else's problem — worth reporting once and
|
|
142
|
+
leaving to whatever escape hatch the plugin offers.
|
|
143
|
+
|
|
144
|
+
Scoping against a ref rather than a session-start snapshot is deliberate:
|
|
145
|
+
the verdict is reproducible from a shell (`git diff HEAD`), it survives
|
|
146
|
+
context compaction and session restarts, and it sees every write —
|
|
147
|
+
including the ones no `PostToolUse` hook ever hears about, like a `sed -i`,
|
|
148
|
+
a formatter run from another hook, or the user's own editor.
|
|
149
|
+
|
|
150
|
+
**Ordering.** Because `fixableFiles` needs to know what's out of scope, an
|
|
151
|
+
auto-fixing consumer runs check → fix → re-check rather than fix → check:
|
|
152
|
+
lint once to get the findings, partition them, fix only the files with
|
|
153
|
+
nothing out of scope, then lint again for the result `runFixLoop` blocks on.
|
|
154
|
+
|
|
155
|
+
**Everything fails open.** No repo, no git, an untracked or ignored path, a
|
|
156
|
+
ref that doesn't resolve, a diff that won't parse — each resolves to
|
|
157
|
+
`all: true`, the whole-file behaviour callers had before this module
|
|
158
|
+
existed. A scope that couldn't be computed must never be the reason a
|
|
159
|
+
linter goes quiet. The corollary is that unversioned trees get no narrowing
|
|
160
|
+
at all.
|
|
161
|
+
|
|
162
|
+
**What it won't catch.** Whole-document rules report the _other_ end of a
|
|
163
|
+
relationship an edit changed — delete the last use of a link reference and
|
|
164
|
+
markdownlint's MD053 fires on the definition hundreds of lines away, which
|
|
165
|
+
is out of scope. This is inherent to line-scoped review (GitHub PR
|
|
166
|
+
annotations, reviewdog and `lint-staged` line filters all share it); the
|
|
167
|
+
context margin covers the near misses, and a consumer should say what it
|
|
168
|
+
excluded rather than drop it silently.
|
|
169
|
+
|
|
121
170
|
## API
|
|
122
171
|
|
|
123
172
|
### Hook I/O (`hook-input.js`)
|
|
@@ -168,6 +217,35 @@ allowed anyway.
|
|
|
168
217
|
shell ever parses a file path on any other platform. `options` also
|
|
169
218
|
accepts `extraDirs`, forwarded to `resolveBin`.
|
|
170
219
|
|
|
220
|
+
### Scoping (`scope.js`)
|
|
221
|
+
|
|
222
|
+
Narrows a check from _this file_ to _the lines in this file that differ from
|
|
223
|
+
a git ref_, so a one-line edit to a long, already-messy document doesn't hand
|
|
224
|
+
Claude every pre-existing violation in it. See
|
|
225
|
+
[Scoping to changes against HEAD](#scoping-to-changes-against-head).
|
|
226
|
+
|
|
227
|
+
- **`scopesFor(files, { ref, context })`** — the entry point: a `Map` of
|
|
228
|
+
absolute path → scope, with the context margin already applied. `ref`
|
|
229
|
+
defaults to `HEAD`, `context` to `DEFAULT_CONTEXT_LINES` (2). A scope is
|
|
230
|
+
`{ file, all, ranges, includes(line) }`, where `all` means the whole file
|
|
231
|
+
is in scope and `ranges` are inclusive, 1-based `[start, end]` pairs in
|
|
232
|
+
current-file coordinates. `includes()` called without a line number
|
|
233
|
+
returns `all`.
|
|
234
|
+
- **`changedRangesByFile(files, { ref })`** — the same answer before the
|
|
235
|
+
margin is applied, as `{ all, ranges }`. Exposed for callers that want to
|
|
236
|
+
do their own widening.
|
|
237
|
+
- **`partitionByScope(items, { scopes, getFile, getLine })`** — splits a
|
|
238
|
+
linter's findings into `{ inScope, outOfScope, outOfScopeFiles }`.
|
|
239
|
+
`getFile`/`getLine` default to reading `file`/`fileName` and
|
|
240
|
+
`line`/`lineNumber`, which covers most JSON reporters.
|
|
241
|
+
- **`fixableFiles({ files, outOfScopeFiles })`** — the files an auto-fixer
|
|
242
|
+
may safely rewrite, being those with nothing out of scope. A `--fix` pass
|
|
243
|
+
rewrites whole files, so running it on a file that also carries
|
|
244
|
+
pre-existing violations would quietly "fix" lines that match the ref.
|
|
245
|
+
- **`mergeRanges(ranges)`** / **`widenRanges(ranges, context)`** — the range
|
|
246
|
+
arithmetic, exposed for testing and for callers with their own ranges.
|
|
247
|
+
- **`DEFAULT_CONTEXT_LINES`** — `2`.
|
|
248
|
+
|
|
171
249
|
### The loop (`loop.js`)
|
|
172
250
|
|
|
173
251
|
- **`runFixLoop({ input, dir, stateFiles, attemptsFile, maxAttempts, files,
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leonardo-marziali/ad-lfl-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Agentic development lint-fix-loop kit: session-scoped file tracking and a capped Stop-hook fix loop for Claude Code plugins.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"hook-input.js",
|
|
29
29
|
"state.js",
|
|
30
30
|
"track.js",
|
|
31
|
+
"scope.js",
|
|
31
32
|
"loop.js"
|
|
32
33
|
],
|
|
33
34
|
"engines": {
|
package/scope.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Narrows a check from "this file" to "the lines in this file that differ
|
|
5
|
+
* from a git ref" — by default HEAD, the last known-clean state.
|
|
6
|
+
*
|
|
7
|
+
* The loop already knows which files a session touched; what it couldn't tell
|
|
8
|
+
* was which parts of them the working tree is actually responsible for. So a
|
|
9
|
+
* one-line edit to a long, already-messy document used to hand Claude every
|
|
10
|
+
* pre-existing violation in it.
|
|
11
|
+
*
|
|
12
|
+
* Scoping against HEAD rather than a session-start snapshot is deliberate:
|
|
13
|
+
* the verdict is then reproducible from a shell (`git diff HEAD`), it
|
|
14
|
+
* survives compaction and session restarts, and it sees every write —
|
|
15
|
+
* including the ones no PostToolUse hook ever hears about, like a `sed -i`,
|
|
16
|
+
* a formatter run from another hook, or the user's own editor.
|
|
17
|
+
*
|
|
18
|
+
* Everything here fails OPEN. No repo, no git, an untracked path, a diff we
|
|
19
|
+
* can't parse — each resolves to "the whole file is in scope", which is
|
|
20
|
+
* exactly the behaviour callers had before this module existed. A scope we
|
|
21
|
+
* failed to compute must never be the reason a linter goes quiet.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
const { runBin } = require('./exec');
|
|
26
|
+
|
|
27
|
+
const DEFAULT_CONTEXT_LINES = 2;
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
* A diff over a large change set can be many megabytes; spawnSync's 1 MiB
|
|
31
|
+
* default would truncate it, and a truncated diff reads as "fewer changed
|
|
32
|
+
* lines" — the one failure this module must not have.
|
|
33
|
+
*/
|
|
34
|
+
const MAX_BUFFER = 64 * 1024 * 1024;
|
|
35
|
+
|
|
36
|
+
const HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
|
|
37
|
+
|
|
38
|
+
function git(args, cwd) {
|
|
39
|
+
return runBin('git', args, { cwd, encoding: 'utf8', maxBuffer: MAX_BUFFER });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ok(result) {
|
|
43
|
+
return Boolean(result) && !result.error && result.status === 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function repoRoot(dir) {
|
|
47
|
+
const result = git(['rev-parse', '--show-toplevel'], dir);
|
|
48
|
+
if (!ok(result)) return null;
|
|
49
|
+
return result.stdout.trim() || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
* Files are grouped by repository so each repo costs a fixed three git
|
|
54
|
+
* invocations however many files it holds, rather than three per file. The
|
|
55
|
+
* root lookup is cached per directory, which is where the repeats are.
|
|
56
|
+
*/
|
|
57
|
+
function groupByRepo(files) {
|
|
58
|
+
const rootFor = new Map();
|
|
59
|
+
const groups = new Map();
|
|
60
|
+
const outsideRepo = [];
|
|
61
|
+
|
|
62
|
+
for (const file of files) {
|
|
63
|
+
const absPath = path.resolve(file);
|
|
64
|
+
const dir = path.dirname(absPath);
|
|
65
|
+
if (!rootFor.has(dir)) rootFor.set(dir, repoRoot(dir));
|
|
66
|
+
|
|
67
|
+
const root = rootFor.get(dir);
|
|
68
|
+
if (!root) {
|
|
69
|
+
outsideRepo.push(absPath);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
73
|
+
groups.get(root).push(absPath);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { groups, outsideRepo };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/*
|
|
80
|
+
* NUL-separated so paths with spaces or newlines survive. Returns null when
|
|
81
|
+
* git couldn't answer, which callers treat as "assume nothing is tracked" and
|
|
82
|
+
* therefore scope whole files.
|
|
83
|
+
*/
|
|
84
|
+
function readPathSet(result, root) {
|
|
85
|
+
if (!ok(result)) return null;
|
|
86
|
+
const paths = new Set();
|
|
87
|
+
for (const rel of (result.stdout || '').split('\0')) {
|
|
88
|
+
if (rel) paths.add(path.resolve(root, rel));
|
|
89
|
+
}
|
|
90
|
+
return paths;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
* Maps one `@@` header to a range of line numbers in the NEW file.
|
|
95
|
+
*
|
|
96
|
+
* `+c,0` is a pure deletion: git points at the line before the splice, so
|
|
97
|
+
* both sides of it go in scope — a violation caused by removing a line (the
|
|
98
|
+
* blank line under a heading, say) is reported against whatever now sits
|
|
99
|
+
* there.
|
|
100
|
+
*/
|
|
101
|
+
function rangeFromHunk(header) {
|
|
102
|
+
const match = HUNK_HEADER.exec(header);
|
|
103
|
+
if (!match) return null;
|
|
104
|
+
|
|
105
|
+
const start = Number(match[1]);
|
|
106
|
+
const count = match[2] === undefined ? 1 : Number(match[2]);
|
|
107
|
+
if (count > 0) return [start, start + count - 1];
|
|
108
|
+
|
|
109
|
+
const at = Math.max(start, 1);
|
|
110
|
+
return [at, at + 1];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/*
|
|
114
|
+
* Parses `git diff --unified=0 --no-prefix` into ranges per absolute path.
|
|
115
|
+
*
|
|
116
|
+
* The trailing-whitespace strip on the `+++` line is load-bearing: git
|
|
117
|
+
* appends a TAB to that line when the path contains a space, and without it
|
|
118
|
+
* every such file would resolve to a path that exists nowhere and silently
|
|
119
|
+
* drop out of scope.
|
|
120
|
+
*/
|
|
121
|
+
function parseDiff(text, root) {
|
|
122
|
+
const byFile = new Map();
|
|
123
|
+
let current = null;
|
|
124
|
+
|
|
125
|
+
for (const line of (text || '').split('\n')) {
|
|
126
|
+
if (line.startsWith('+++ ')) {
|
|
127
|
+
const target = line.slice(4).trimEnd();
|
|
128
|
+
current = target === '/dev/null' ? null : path.resolve(root, target);
|
|
129
|
+
if (current && !byFile.has(current)) byFile.set(current, []);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!current || !line.startsWith('@@')) continue;
|
|
134
|
+
const range = rangeFromHunk(line);
|
|
135
|
+
if (range) byFile.get(current).push(range);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return byFile;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function scopeWholeFile() {
|
|
142
|
+
return { all: true, ranges: [] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* Resolves one repository's worth of files. `--name-only` is read separately
|
|
147
|
+
* from the unified diff on purpose: it is the authority on WHICH files
|
|
148
|
+
* changed, so a file that changed but whose hunks we failed to parse (an
|
|
149
|
+
* exotically quoted path, a binary file) is caught and scoped whole rather
|
|
150
|
+
* than mistaken for unchanged.
|
|
151
|
+
*/
|
|
152
|
+
function resolveRepoGroup(root, files, ref, into) {
|
|
153
|
+
const tracked = readPathSet(git(['ls-files', '-z', '--', ...files], root), root);
|
|
154
|
+
const trackedFiles = [];
|
|
155
|
+
|
|
156
|
+
for (const file of files) {
|
|
157
|
+
if (tracked?.has(file)) trackedFiles.push(file);
|
|
158
|
+
else into.set(file, scopeWholeFile());
|
|
159
|
+
}
|
|
160
|
+
if (trackedFiles.length === 0) return;
|
|
161
|
+
|
|
162
|
+
const changed = readPathSet(
|
|
163
|
+
git(['diff', ref, '--name-only', '-z', '--', ...trackedFiles], root),
|
|
164
|
+
root,
|
|
165
|
+
);
|
|
166
|
+
const diff = git(
|
|
167
|
+
[
|
|
168
|
+
'-c',
|
|
169
|
+
'core.quotePath=false',
|
|
170
|
+
'diff',
|
|
171
|
+
ref,
|
|
172
|
+
'--unified=0',
|
|
173
|
+
'--no-color',
|
|
174
|
+
'--no-ext-diff',
|
|
175
|
+
'--no-renames',
|
|
176
|
+
'--no-prefix',
|
|
177
|
+
'--',
|
|
178
|
+
...trackedFiles,
|
|
179
|
+
],
|
|
180
|
+
root,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (!changed || !ok(diff)) {
|
|
184
|
+
for (const file of trackedFiles) into.set(file, scopeWholeFile());
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const ranges = parseDiff(diff.stdout, root);
|
|
189
|
+
for (const file of trackedFiles) {
|
|
190
|
+
if (!changed.has(file)) {
|
|
191
|
+
into.set(file, { all: false, ranges: [] });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const parsed = ranges.get(file);
|
|
195
|
+
into.set(file, parsed && parsed.length > 0 ? { all: false, ranges: parsed } : scopeWholeFile());
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/*
|
|
200
|
+
* The raw per-file answer, before any context margin is applied:
|
|
201
|
+
* { all, ranges } keyed by absolute path, where `all` means the whole file
|
|
202
|
+
* is in scope.
|
|
203
|
+
*/
|
|
204
|
+
function changedRangesByFile(files, options = {}) {
|
|
205
|
+
const { ref = 'HEAD' } = options;
|
|
206
|
+
const result = new Map();
|
|
207
|
+
if (!files || files.length === 0) return result;
|
|
208
|
+
|
|
209
|
+
const { groups, outsideRepo } = groupByRepo(files);
|
|
210
|
+
for (const file of outsideRepo) result.set(file, scopeWholeFile());
|
|
211
|
+
for (const [root, group] of groups) resolveRepoGroup(root, group, ref, result);
|
|
212
|
+
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function mergeRanges(ranges) {
|
|
217
|
+
const sorted = [...ranges].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
218
|
+
const merged = [];
|
|
219
|
+
|
|
220
|
+
for (const [start, end] of sorted) {
|
|
221
|
+
const last = merged.at(-1);
|
|
222
|
+
// `start - 1` so ranges that merely touch collapse into one.
|
|
223
|
+
if (last && start - 1 <= last[1]) {
|
|
224
|
+
if (end > last[1]) last[1] = end;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
merged.push([start, end]);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return merged;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/*
|
|
234
|
+
* Widens each range by `context` lines on either side.
|
|
235
|
+
*
|
|
236
|
+
* Linters routinely report a violation an edit caused against a line the
|
|
237
|
+
* edit didn't touch — append a paragraph under a heading and MD022 fires on
|
|
238
|
+
* the heading above. Only the lower bound is clamped; a range running past
|
|
239
|
+
* the end of the file is harmless, since nothing is ever reported there, and
|
|
240
|
+
* clamping the top would mean reading every file just to count its lines.
|
|
241
|
+
*/
|
|
242
|
+
function widenRanges(ranges, context) {
|
|
243
|
+
if (context <= 0) return mergeRanges(ranges);
|
|
244
|
+
return mergeRanges(ranges.map(([start, end]) => [Math.max(start - context, 1), end + context]));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function makeScope(file, changed, context) {
|
|
248
|
+
const all = changed.all;
|
|
249
|
+
const ranges = all ? [] : widenRanges(changed.ranges, context);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
file,
|
|
253
|
+
all,
|
|
254
|
+
ranges,
|
|
255
|
+
/*
|
|
256
|
+
* A violation with no line number is in scope only when the whole file
|
|
257
|
+
* is — the linter couldn't place it, so we can't claim the working tree
|
|
258
|
+
* introduced it.
|
|
259
|
+
*/
|
|
260
|
+
includes(line) {
|
|
261
|
+
if (all) return true;
|
|
262
|
+
if (typeof line !== 'number' || !Number.isFinite(line)) return false;
|
|
263
|
+
return ranges.some(([start, end]) => line >= start && line <= end);
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/*
|
|
269
|
+
* The entry point callers want: absolute path → scope, context margin
|
|
270
|
+
* already applied.
|
|
271
|
+
*/
|
|
272
|
+
function scopesFor(files, options = {}) {
|
|
273
|
+
const { ref = 'HEAD', context = DEFAULT_CONTEXT_LINES } = options;
|
|
274
|
+
const scopes = new Map();
|
|
275
|
+
|
|
276
|
+
for (const [file, changed] of changedRangesByFile(files, { ref })) {
|
|
277
|
+
scopes.set(file, makeScope(file, changed, context));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return scopes;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const defaultGetFile = (item) => item.file ?? item.fileName;
|
|
284
|
+
const defaultGetLine = (item) => item.line ?? item.lineNumber;
|
|
285
|
+
|
|
286
|
+
/*
|
|
287
|
+
* Splits a linter's findings into the ones the working tree is answerable
|
|
288
|
+
* for and the ones that were already there. `outOfScopeFiles` is handed back
|
|
289
|
+
* so callers don't have to re-derive it for fixableFiles.
|
|
290
|
+
*
|
|
291
|
+
* A finding whose file has no scope recorded was never scoped at all, so it
|
|
292
|
+
* counts as in scope — fail open, as everywhere else here.
|
|
293
|
+
*/
|
|
294
|
+
function partitionByScope(items, options = {}) {
|
|
295
|
+
const { scopes, getFile = defaultGetFile, getLine = defaultGetLine } = options;
|
|
296
|
+
const inScope = [];
|
|
297
|
+
const outOfScope = [];
|
|
298
|
+
const outOfScopeFiles = new Set();
|
|
299
|
+
|
|
300
|
+
for (const item of items) {
|
|
301
|
+
const file = path.resolve(getFile(item));
|
|
302
|
+
const scope = scopes.get(file);
|
|
303
|
+
|
|
304
|
+
if (!scope || scope.includes(getLine(item))) {
|
|
305
|
+
inScope.push(item);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
outOfScope.push(item);
|
|
309
|
+
outOfScopeFiles.add(file);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { inScope, outOfScope, outOfScopeFiles };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/*
|
|
316
|
+
* The files an auto-fixer may safely rewrite: those with nothing out of
|
|
317
|
+
* scope. A `--fix` pass rewrites whole files, so running it on a file that
|
|
318
|
+
* also carries pre-existing violations would quietly "fix" lines that match
|
|
319
|
+
* HEAD — the diff noise this whole module exists to avoid.
|
|
320
|
+
*/
|
|
321
|
+
function fixableFiles(options = {}) {
|
|
322
|
+
const { files = [], outOfScopeFiles = new Set() } = options;
|
|
323
|
+
return files.filter((file) => !outOfScopeFiles.has(path.resolve(file)));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
DEFAULT_CONTEXT_LINES,
|
|
328
|
+
changedRangesByFile,
|
|
329
|
+
scopesFor,
|
|
330
|
+
partitionByScope,
|
|
331
|
+
fixableFiles,
|
|
332
|
+
mergeRanges,
|
|
333
|
+
widenRanges,
|
|
334
|
+
};
|