@drkmttr/adhere 0.21.2 → 0.23.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 +28 -1552
- package/package.json +10 -7
- package/presets/README.md +9 -1
- package/presets/alchemy/runtime/bind-tag-not-implementation/RULE.md +1 -0
- package/presets/effect/data/brand-meaningful-primitives/RULE.md +1 -0
- package/presets/effect/data/variants-are-tagged-unions/RULE.md +1 -0
- package/presets/typescript/types/derive-dont-restate/RULE.md +1 -0
- package/skills/adhere/SKILL.md +194 -30
- package/skills/adhere-fix/SKILL.md +1 -1
- package/skills/adhere-setup/SKILL.md +49 -16
- package/src/cli.ts +216 -25
- package/src/config.ts +27 -4
- package/src/create/ask.ts +225 -9
- package/src/create/examples.ts +94 -0
- package/src/create/look.ts +70 -19
- package/src/create/oxlint-trial.ts +464 -0
- package/src/create/oxlint.ts +1012 -0
- package/src/create/trial-review.ts +562 -0
- package/src/create/trial.ts +611 -0
- package/src/create/wizard.ts +128 -94
- package/src/init.ts +169 -40
- package/src/install.ts +295 -93
- package/src/instructions.ts +34 -12
- package/src/markdown.ts +12 -2
- package/src/oxlint/catalog.json +7844 -0
- package/src/oxlint/catalog.ts +72 -0
- package/src/oxlint/shared.ts +140 -0
- package/src/readme-guide.ts +431 -0
- package/src/readme.ts +291 -0
- package/src/services/AdhereConfig.ts +36 -3
- package/src/services/AuditCache.ts +176 -169
- package/src/services/SourceWalker.ts +12 -5
- package/src/wording.ts +3 -3
- package/src/workflows/audit.ts +5 -2
- package/src/workflows/format.ts +1 -1
|
@@ -155,180 +155,187 @@ const OLD_NAME = /^[0-9a-f]{64}$/;
|
|
|
155
155
|
/**
|
|
156
156
|
* `.adhere/cache/`, which is meant to be committed. `files/` holds answers by
|
|
157
157
|
* the content they are about, and `tallies/` the linter check's tallies, with
|
|
158
|
-
* paths from the
|
|
159
|
-
* rewritten: it is named for its group, a
|
|
160
|
-
* the hash of its own text. Two branches
|
|
161
|
-
* files, or the same one, so git merges them
|
|
162
|
-
* group takes the union of its files, and pruning
|
|
158
|
+
* paths from the checkout root (`base`, the working directory by default). A
|
|
159
|
+
* file is written once and never rewritten: it is named for its group, a
|
|
160
|
+
* content hash or a tally key, and for the hash of its own text. Two branches
|
|
161
|
+
* that add to a group add different files, or the same one, so git merges them
|
|
162
|
+
* without a conflict. Reading a group takes the union of its files, and pruning
|
|
163
|
+
* folds them back into one.
|
|
163
164
|
*
|
|
164
165
|
* A cache must never fail a run: whatever cannot be read counts as missing,
|
|
165
166
|
* and whatever cannot be written is left unwritten.
|
|
166
167
|
*/
|
|
167
|
-
export const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
type Folder = keyof typeof indexes;
|
|
194
|
-
|
|
195
|
-
const read = <A>(decode: (text: string) => Option.Option<A>, file: string) =>
|
|
196
|
-
fs.readFileString(file).pipe(
|
|
197
|
-
Effect.map((text) => Option.getOrUndefined(decode(text))),
|
|
198
|
-
Effect.orElseSucceed(() => undefined),
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
/** A group's files that decode, in name order. */
|
|
202
|
-
const readGroup = Effect.fn("AuditCache.readGroup")(function* <A>(
|
|
203
|
-
folder: Folder,
|
|
204
|
-
group: string,
|
|
205
|
-
decode: (text: string) => Option.Option<A>,
|
|
206
|
-
) {
|
|
207
|
-
const names = [...((yield* indexes[folder]).get(group) ?? [])].sort();
|
|
208
|
-
const decoded = yield* Effect.forEach(names, (name) =>
|
|
209
|
-
read(decode, path.join(directory, folder, name)),
|
|
210
|
-
);
|
|
211
|
-
return decoded.filter((value): value is A => value !== undefined);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const nameOf = (group: string, text: string) =>
|
|
215
|
-
Effect.map(hash(text), (digest) => `${group}.${digest.slice(0, 16)}.json`);
|
|
216
|
-
|
|
217
|
-
/** Adds `value` to a group as a new file, unless the group has that file already. */
|
|
218
|
-
const add = Effect.fn("AuditCache.add")(function* (
|
|
219
|
-
folder: Folder,
|
|
220
|
-
group: string,
|
|
221
|
-
value: unknown,
|
|
222
|
-
) {
|
|
223
|
-
const text = textOf(value);
|
|
224
|
-
const name = yield* nameOf(group, text);
|
|
225
|
-
const index = yield* indexes[folder];
|
|
226
|
-
const names = index.get(group) ?? new Set<string>();
|
|
227
|
-
if (names.has(name)) return;
|
|
228
|
-
const written = yield* fs
|
|
229
|
-
.makeDirectory(path.join(directory, folder), { recursive: true })
|
|
230
|
-
.pipe(
|
|
231
|
-
Effect.andThen(fs.writeFileString(path.join(directory, folder, name), text)),
|
|
232
|
-
Effect.as(true),
|
|
233
|
-
Effect.orElseSucceed(() => false),
|
|
168
|
+
export const AuditCacheAt = (base?: string) =>
|
|
169
|
+
Layer.effect(AuditCache)(
|
|
170
|
+
Effect.gen(function* () {
|
|
171
|
+
const fs = yield* FileSystem.FileSystem;
|
|
172
|
+
const path = yield* Path.Path;
|
|
173
|
+
const crypto = yield* Crypto.Crypto;
|
|
174
|
+
const root = path.resolve(base ?? ".");
|
|
175
|
+
const directory = path.join(root, CACHE_DIRECTORY);
|
|
176
|
+
const hash = (text: string) =>
|
|
177
|
+
sha256(text).pipe(Effect.provideService(Crypto.Crypto, crypto));
|
|
178
|
+
|
|
179
|
+
/** Each group's file names in one folder, listed once and kept current as files come and go. */
|
|
180
|
+
const indexOf = (folder: string) =>
|
|
181
|
+
Effect.cached(
|
|
182
|
+
fs.readDirectory(path.join(directory, folder)).pipe(
|
|
183
|
+
Effect.orElseSucceed((): ReadonlyArray<string> => []),
|
|
184
|
+
Effect.map((names) => {
|
|
185
|
+
const groups = new Map<string, Set<string>>();
|
|
186
|
+
for (const name of names) {
|
|
187
|
+
const group = NAME.exec(name)?.[1];
|
|
188
|
+
if (group !== undefined)
|
|
189
|
+
groups.set(group, (groups.get(group) ?? new Set()).add(name));
|
|
190
|
+
}
|
|
191
|
+
return groups;
|
|
192
|
+
}),
|
|
193
|
+
),
|
|
234
194
|
);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
/** Replaces a group's files with one holding `value`, or with none. Succeeds with the files deleted. */
|
|
245
|
-
const fold = Effect.fn("AuditCache.fold")(function* (
|
|
246
|
-
folder: Folder,
|
|
247
|
-
group: string,
|
|
248
|
-
value: unknown,
|
|
249
|
-
) {
|
|
250
|
-
const index = yield* indexes[folder];
|
|
251
|
-
const names = [...(index.get(group) ?? [])];
|
|
252
|
-
const keep = value === undefined ? undefined : yield* nameOf(group, textOf(value));
|
|
253
|
-
if (value !== undefined) yield* add(folder, group, value);
|
|
254
|
-
let deleted = 0;
|
|
255
|
-
for (const name of names) {
|
|
256
|
-
if (name !== keep) deleted += yield* remove(path.join(directory, folder, name));
|
|
257
|
-
}
|
|
258
|
-
if (keep === undefined) index.delete(group);
|
|
259
|
-
else index.set(group, new Set([keep]));
|
|
260
|
-
return deleted;
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
const relativeOf = (tally: Tally): Tally => ({
|
|
264
|
-
files: sorted(
|
|
265
|
-
Record.mapKeys(tally.files, (file) =>
|
|
266
|
-
path.isAbsolute(file) ? path.relative(root, file) : file,
|
|
267
|
-
),
|
|
268
|
-
),
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
const get = Effect.fn("AuditCache.get")(function* (contentHash: string, filePath: string) {
|
|
272
|
-
const entries = yield* readGroup("files", contentHash, decodeEntry);
|
|
273
|
-
if (entries.length > 0) return unionOf(entries.map((entry) => entry.answers));
|
|
274
|
-
// Before content keys, a file's entry sat under the hash of its absolute path.
|
|
275
|
-
const old = yield* read(decodeOldEntry, path.join(directory, yield* hash(filePath)));
|
|
276
|
-
if (old === undefined || old.hash !== contentHash) return {};
|
|
277
|
-
const answers = answersOf(old.judgments);
|
|
278
|
-
yield* add("files", contentHash, { answers: sorted(answers) });
|
|
279
|
-
return answers;
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
const tally = Effect.fn("AuditCache.tally")(function* (key: string) {
|
|
283
|
-
const tallies = yield* readGroup("tallies", key, decodeTally);
|
|
284
|
-
if (tallies.length > 0) {
|
|
285
|
-
const { files } = tallyUnionOf(tallies);
|
|
286
|
-
return { files: Record.mapKeys(files, (file) => path.resolve(root, file)) };
|
|
287
|
-
}
|
|
288
|
-
// Before, a tally sat under the hash of its key, with absolute paths.
|
|
289
|
-
const old = yield* read(
|
|
290
|
-
decodeTally,
|
|
291
|
-
path.join(directory, yield* hash(`\u0000tally\u0000${key}`)),
|
|
292
|
-
);
|
|
293
|
-
if (old !== undefined) yield* add("tallies", key, relativeOf(old));
|
|
294
|
-
return old;
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
const prune = Effect.fn("AuditCache.prune")(function* (live: Live) {
|
|
298
|
-
// Tallies still in the layout before content keys move first, so deleting what is left of it loses nothing.
|
|
299
|
-
yield* Effect.forEach(live.tallies, tally, { discard: true });
|
|
300
|
-
let deleted = 0;
|
|
301
|
-
// Folding replaces or removes the group being visited, which a Map's iteration allows.
|
|
302
|
-
for (const group of (yield* indexes.files).keys()) {
|
|
303
|
-
const entries = live.hashes.has(group) ? yield* readGroup("files", group, decodeEntry) : [];
|
|
304
|
-
const answers = unionOf(entries.map((entry) => entry.answers));
|
|
305
|
-
deleted += yield* fold(
|
|
306
|
-
"files",
|
|
307
|
-
group,
|
|
308
|
-
Object.keys(answers).length === 0 ? undefined : { answers: sorted(answers) },
|
|
195
|
+
const indexes = { files: yield* indexOf("files"), tallies: yield* indexOf("tallies") };
|
|
196
|
+
type Folder = keyof typeof indexes;
|
|
197
|
+
|
|
198
|
+
const read = <A>(decode: (text: string) => Option.Option<A>, file: string) =>
|
|
199
|
+
fs.readFileString(file).pipe(
|
|
200
|
+
Effect.map((text) => Option.getOrUndefined(decode(text))),
|
|
201
|
+
Effect.orElseSucceed(() => undefined),
|
|
309
202
|
);
|
|
310
|
-
|
|
311
|
-
|
|
203
|
+
|
|
204
|
+
/** A group's files that decode, in name order. */
|
|
205
|
+
const readGroup = Effect.fn("AuditCache.readGroup")(function* <A>(
|
|
206
|
+
folder: Folder,
|
|
207
|
+
group: string,
|
|
208
|
+
decode: (text: string) => Option.Option<A>,
|
|
209
|
+
) {
|
|
210
|
+
const names = [...((yield* indexes[folder]).get(group) ?? [])].sort();
|
|
211
|
+
const decoded = yield* Effect.forEach(names, (name) =>
|
|
212
|
+
read(decode, path.join(directory, folder, name)),
|
|
213
|
+
);
|
|
214
|
+
return decoded.filter((value): value is A => value !== undefined);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const nameOf = (group: string, text: string) =>
|
|
218
|
+
Effect.map(hash(text), (digest) => `${group}.${digest.slice(0, 16)}.json`);
|
|
219
|
+
|
|
220
|
+
/** Adds `value` to a group as a new file, unless the group has that file already. */
|
|
221
|
+
const add = Effect.fn("AuditCache.add")(function* (
|
|
222
|
+
folder: Folder,
|
|
223
|
+
group: string,
|
|
224
|
+
value: unknown,
|
|
225
|
+
) {
|
|
226
|
+
const text = textOf(value);
|
|
227
|
+
const name = yield* nameOf(group, text);
|
|
228
|
+
const index = yield* indexes[folder];
|
|
229
|
+
const names = index.get(group) ?? new Set<string>();
|
|
230
|
+
if (names.has(name)) return;
|
|
231
|
+
const written = yield* fs
|
|
232
|
+
.makeDirectory(path.join(directory, folder), { recursive: true })
|
|
233
|
+
.pipe(
|
|
234
|
+
Effect.andThen(fs.writeFileString(path.join(directory, folder, name), text)),
|
|
235
|
+
Effect.as(true),
|
|
236
|
+
Effect.orElseSucceed(() => false),
|
|
237
|
+
);
|
|
238
|
+
if (written) index.set(group, names.add(name));
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const remove = (file: string) =>
|
|
242
|
+
fs.remove(file).pipe(
|
|
243
|
+
Effect.as(1),
|
|
244
|
+
Effect.orElseSucceed(() => 0),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
/** Replaces a group's files with one holding `value`, or with none. Succeeds with the files deleted. */
|
|
248
|
+
const fold = Effect.fn("AuditCache.fold")(function* (
|
|
249
|
+
folder: Folder,
|
|
250
|
+
group: string,
|
|
251
|
+
value: unknown,
|
|
252
|
+
) {
|
|
253
|
+
const index = yield* indexes[folder];
|
|
254
|
+
const names = [...(index.get(group) ?? [])];
|
|
255
|
+
const keep = value === undefined ? undefined : yield* nameOf(group, textOf(value));
|
|
256
|
+
if (value !== undefined) yield* add(folder, group, value);
|
|
257
|
+
let deleted = 0;
|
|
258
|
+
for (const name of names) {
|
|
259
|
+
if (name !== keep) deleted += yield* remove(path.join(directory, folder, name));
|
|
260
|
+
}
|
|
261
|
+
if (keep === undefined) index.delete(group);
|
|
262
|
+
else index.set(group, new Set([keep]));
|
|
263
|
+
return deleted;
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const relativeOf = (tally: Tally): Tally => ({
|
|
267
|
+
files: sorted(
|
|
268
|
+
Record.mapKeys(tally.files, (file) =>
|
|
269
|
+
path.isAbsolute(file) ? path.relative(root, file) : file,
|
|
270
|
+
),
|
|
271
|
+
),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const get = Effect.fn("AuditCache.get")(function* (contentHash: string, filePath: string) {
|
|
275
|
+
const entries = yield* readGroup("files", contentHash, decodeEntry);
|
|
276
|
+
if (entries.length > 0) return unionOf(entries.map((entry) => entry.answers));
|
|
277
|
+
// Before content keys, a file's entry sat under the hash of its absolute path.
|
|
278
|
+
const old = yield* read(decodeOldEntry, path.join(directory, yield* hash(filePath)));
|
|
279
|
+
if (old === undefined || old.hash !== contentHash) return {};
|
|
280
|
+
const answers = answersOf(old.judgments);
|
|
281
|
+
yield* add("files", contentHash, { answers: sorted(answers) });
|
|
282
|
+
return answers;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const tally = Effect.fn("AuditCache.tally")(function* (key: string) {
|
|
312
286
|
const tallies = yield* readGroup("tallies", key, decodeTally);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
287
|
+
if (tallies.length > 0) {
|
|
288
|
+
const { files } = tallyUnionOf(tallies);
|
|
289
|
+
return { files: Record.mapKeys(files, (file) => path.resolve(root, file)) };
|
|
290
|
+
}
|
|
291
|
+
// Before, a tally sat under the hash of its key, with absolute paths.
|
|
292
|
+
const old = yield* read(
|
|
293
|
+
decodeTally,
|
|
294
|
+
path.join(directory, yield* hash(`\u0000tally\u0000${key}`)),
|
|
317
295
|
);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
296
|
+
if (old !== undefined) yield* add("tallies", key, relativeOf(old));
|
|
297
|
+
return old;
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const prune = Effect.fn("AuditCache.prune")(function* (live: Live) {
|
|
301
|
+
// Tallies still in the layout before content keys move first, so deleting what is left of it loses nothing.
|
|
302
|
+
yield* Effect.forEach(live.tallies, tally, { discard: true });
|
|
303
|
+
let deleted = 0;
|
|
304
|
+
// Folding replaces or removes the group being visited, which a Map's iteration allows.
|
|
305
|
+
for (const group of (yield* indexes.files).keys()) {
|
|
306
|
+
const entries = live.hashes.has(group)
|
|
307
|
+
? yield* readGroup("files", group, decodeEntry)
|
|
308
|
+
: [];
|
|
309
|
+
const answers = unionOf(entries.map((entry) => entry.answers));
|
|
310
|
+
deleted += yield* fold(
|
|
311
|
+
"files",
|
|
312
|
+
group,
|
|
313
|
+
Object.keys(answers).length === 0 ? undefined : { answers: sorted(answers) },
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
for (const key of (yield* indexes.tallies).keys()) {
|
|
317
|
+
const tallies = yield* readGroup("tallies", key, decodeTally);
|
|
318
|
+
deleted += yield* fold(
|
|
319
|
+
"tallies",
|
|
320
|
+
key,
|
|
321
|
+
tallies.length === 0 ? undefined : relativeOf(tallyUnionOf(tallies)),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
const old = yield* fs.readDirectory(directory).pipe(Effect.orElseSucceed(() => []));
|
|
325
|
+
for (const name of old) {
|
|
326
|
+
if (OLD_NAME.test(name)) deleted += yield* remove(path.join(directory, name));
|
|
327
|
+
}
|
|
328
|
+
return deleted;
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return AuditCache.of({
|
|
332
|
+
get,
|
|
333
|
+
put: (contentHash, answers) => add("files", contentHash, { answers: sorted(answers) }),
|
|
334
|
+
tally,
|
|
335
|
+
putTally: (key, value) => add("tallies", key, relativeOf(value)),
|
|
336
|
+
prune,
|
|
337
|
+
});
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
export const AuditCacheLive = AuditCacheAt();
|
|
@@ -172,20 +172,27 @@ const scanDirs = Effect.fn("SourceWalker.scanDirs")(function* (
|
|
|
172
172
|
* Tests are listed even when no rule judges them, so a prune keeps what other runs
|
|
173
173
|
* know of them. Tests substitute their own tree.
|
|
174
174
|
* The scan root is the current working directory, so the audit reads the
|
|
175
|
-
* repository it is run in.
|
|
175
|
+
* repository it is run in. `roots` overrides the workspace trees read: `["."]`
|
|
176
|
+
* reads the whole working directory, for a trial that must not miss a file
|
|
177
|
+
* outside `agents/`, `apps/`, and `packages/`. `base` roots the walk
|
|
178
|
+
* elsewhere, for a trial of a checkout that is not the working directory.
|
|
176
179
|
*/
|
|
177
|
-
export const SourceWalkerLive = (
|
|
180
|
+
export const SourceWalkerLive = (
|
|
181
|
+
filter: ReadonlyArray<string> = [],
|
|
182
|
+
roots?: ReadonlyArray<string>,
|
|
183
|
+
base?: string,
|
|
184
|
+
) =>
|
|
178
185
|
Layer.effect(SourceWalker)(
|
|
179
186
|
Effect.gen(function* () {
|
|
180
187
|
const fs = yield* FileSystem.FileSystem;
|
|
181
188
|
const path = yield* Path.Path;
|
|
182
189
|
const config = yield* AdhereConfig;
|
|
183
|
-
|
|
184
|
-
const root = path.resolve();
|
|
190
|
+
const root = base === undefined ? path.resolve() : path.resolve(base);
|
|
185
191
|
const self = yield* path.fromFileUrl(new URL("../../", import.meta.url)).pipe(Effect.orDie);
|
|
186
192
|
const patterns = [...filter, ...(config.exclude ?? []).map((glob) => `!${glob}`)];
|
|
187
193
|
const walk = Effect.fn("SourceWalker.files")(function* () {
|
|
188
|
-
const dirs =
|
|
194
|
+
const dirs =
|
|
195
|
+
roots === undefined ? yield* scanDirs(fs, root).pipe(Effect.mapError(refused)) : roots;
|
|
189
196
|
const trees = yield* clearingStatus(
|
|
190
197
|
Effect.forEach(dirs, (dir) =>
|
|
191
198
|
listRoot(
|
package/src/wording.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { type RuleEntry, shownId } from "#rules.ts";
|
|
|
4
4
|
type Word = Example["word"];
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* A way a rule's wording differs from the
|
|
7
|
+
* A way a rule's wording differs from the docs' rule writing tips, which
|
|
8
8
|
* the eval measured: the rule shows code of both kinds, and its description
|
|
9
9
|
* says the words its code is under.
|
|
10
10
|
*/
|
|
@@ -22,7 +22,7 @@ export interface Stray {
|
|
|
22
22
|
readonly straying: ReadonlyArray<Straying>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export const TIPS_URL = "https://
|
|
25
|
+
export const TIPS_URL = "https://dm.sh/adhere/writing-good-rules/#rule-writing-tips";
|
|
26
26
|
|
|
27
27
|
/** Whether a description says a word, whole and in any case: "Must" does, "mustard" does not. */
|
|
28
28
|
const says = (description: string, word: Word): boolean =>
|
|
@@ -30,7 +30,7 @@ const says = (description: string, word: Word): boolean =>
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* How one rule strays from the tips. A rule with only code that breaks it
|
|
33
|
-
* lacks no example: the
|
|
33
|
+
* lacks no example: the docs keep that for a rule with no single correct
|
|
34
34
|
* form to show.
|
|
35
35
|
*/
|
|
36
36
|
export const strayingOf = (rule: Rule): ReadonlyArray<Straying> => {
|
package/src/workflows/audit.ts
CHANGED
|
@@ -385,6 +385,8 @@ const MAX_DEPENDENT_FILES = 50;
|
|
|
385
385
|
export interface PlanOptions {
|
|
386
386
|
/** Select at most this many files needing judge or cached-locate requests; zero plans only. */
|
|
387
387
|
readonly limit?: number | undefined;
|
|
388
|
+
/** The checkout Jev's paths are relative to. Defaults to the working directory. */
|
|
389
|
+
readonly root?: string | undefined;
|
|
388
390
|
}
|
|
389
391
|
|
|
390
392
|
export const planAudit = (
|
|
@@ -410,9 +412,10 @@ export const planAudit = (
|
|
|
410
412
|
};
|
|
411
413
|
const walker = yield* SourceWalker;
|
|
412
414
|
const files = yield* walker.files;
|
|
413
|
-
// Each file's path from the
|
|
415
|
+
// Each file's path from the checkout, `/`-separated, as Jev reads it.
|
|
416
|
+
const root = options.root ?? process.cwd();
|
|
414
417
|
const fromRoot = new Map(
|
|
415
|
-
files.map((file) => [file.path, relative(
|
|
418
|
+
files.map((file) => [file.path, relative(root, file.path).split(sep).join("/")]),
|
|
416
419
|
);
|
|
417
420
|
// Only the specifiers stay in memory: each file is read again when it is planned.
|
|
418
421
|
const dependentFiles = config.reads.includes("dependents")
|
package/src/workflows/format.ts
CHANGED
|
@@ -214,7 +214,7 @@ const excerpt = (finding: Finding, root: string | undefined): ReadonlyArray<Line
|
|
|
214
214
|
/**
|
|
215
215
|
* The warning on a finding whose file may not show enough to check its rule,
|
|
216
216
|
* which its context score in the header says how far, and help: how to add
|
|
217
|
-
* what the code relies on as a note Jev reads, in the form the
|
|
217
|
+
* what the code relies on as a note Jev reads, in the form the docs ask for.
|
|
218
218
|
*/
|
|
219
219
|
const insufficient = (finding: Finding, sufficiencyThreshold: number): ReadonlyArray<Line> =>
|
|
220
220
|
!lacksContext(finding, sufficiencyThreshold)
|