@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
package/src/readme.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes the managed guide into a shared repo's README: only a marked block,
|
|
3
|
+
* so the rest of the README stays the author's. The block itself, its rules
|
|
4
|
+
* grouped by topic and its lint settings, is rendered by `#readme-guide.ts`.
|
|
5
|
+
*/
|
|
6
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { run } from "#create/agents.ts";
|
|
9
|
+
import { cloned, describeRulesAt } from "#install.ts";
|
|
10
|
+
import { lintEntries, renderReadmeIndex, type LintConfig } from "#readme-guide.ts";
|
|
11
|
+
import { SHARED_LINT_EXPORT, SHARED_LINT_RULES } from "#oxlint/shared.ts";
|
|
12
|
+
import { Effect, Option, Schema } from "effect";
|
|
13
|
+
|
|
14
|
+
export type { LintConfig, ReadmeRule } from "#readme-guide.ts";
|
|
15
|
+
export { renderReadmeIndex };
|
|
16
|
+
|
|
17
|
+
export const README_START = "<!-- adhere:rules:start -->";
|
|
18
|
+
export const README_END = "<!-- adhere:rules:end -->";
|
|
19
|
+
|
|
20
|
+
export class ReadmeUnavailable extends Schema.TaggedError<ReadmeUnavailable>()(
|
|
21
|
+
"ReadmeUnavailable",
|
|
22
|
+
{ message: Schema.String },
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
const LintConfigShape = Schema.Struct({
|
|
26
|
+
rules: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)),
|
|
27
|
+
jsPlugins: Schema.optionalKey(Schema.Array(Schema.Unknown)),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const Manifest = Schema.Struct({
|
|
31
|
+
exports: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** What a README index wrote, and how much it listed. */
|
|
35
|
+
export interface ReadmeRefresh {
|
|
36
|
+
readonly changed: boolean;
|
|
37
|
+
readonly message: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const readOptional = (file: string): Effect.Effect<string | undefined, ReadmeUnavailable> =>
|
|
41
|
+
Effect.tryPromise({
|
|
42
|
+
try: async () => {
|
|
43
|
+
try {
|
|
44
|
+
return { text: await readFile(file, "utf8") };
|
|
45
|
+
} catch (cause) {
|
|
46
|
+
if (
|
|
47
|
+
typeof cause === "object" &&
|
|
48
|
+
cause !== null &&
|
|
49
|
+
"code" in cause &&
|
|
50
|
+
cause.code === "ENOENT"
|
|
51
|
+
) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
throw cause;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
catch: (cause) =>
|
|
58
|
+
new ReadmeUnavailable({
|
|
59
|
+
message: `Could not read ${file}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
60
|
+
}),
|
|
61
|
+
}).pipe(Effect.map(({ text }) => text));
|
|
62
|
+
|
|
63
|
+
/** The shared lint config, `{}` when absent; invalid JSON refuses the refresh. */
|
|
64
|
+
const lintConfigAt = (root: string): Effect.Effect<LintConfig, ReadmeUnavailable> =>
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
const file = join(root, SHARED_LINT_RULES);
|
|
67
|
+
const text = yield* readOptional(file);
|
|
68
|
+
if (text === undefined) return {};
|
|
69
|
+
const parsed = yield* Effect.try({
|
|
70
|
+
try: () => JSON.parse(text) as unknown,
|
|
71
|
+
catch: () =>
|
|
72
|
+
new ReadmeUnavailable({
|
|
73
|
+
message: `${file} is not valid JSON; left the README unchanged.`,
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
const decoded = Schema.decodeUnknownOption(LintConfigShape)(parsed);
|
|
77
|
+
return Option.isSome(decoded)
|
|
78
|
+
? decoded.value
|
|
79
|
+
: yield* ReadmeUnavailable.make({
|
|
80
|
+
message: `${file} is not an object of rules and jsPlugins; left the README unchanged.`,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether `root` is a shared rules repo, by the metadata `init --shared`
|
|
86
|
+
* writes: a package.json whose exports name `./oxlint`. Its rules directory
|
|
87
|
+
* alone is every project's, so this is what tells a shared repo from one that
|
|
88
|
+
* merely has rules.
|
|
89
|
+
*/
|
|
90
|
+
export const isSharedRepo = (root: string): Effect.Effect<boolean> =>
|
|
91
|
+
Effect.promise(async () => {
|
|
92
|
+
try {
|
|
93
|
+
const parsed: unknown = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
94
|
+
const decoded = Schema.decodeUnknownOption(Manifest)(parsed);
|
|
95
|
+
return Option.isSome(decoded) && decoded.value.exports?.[SHARED_LINT_EXPORT] !== undefined;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Replace only the marked block; append it when absent. A README that is
|
|
103
|
+
* missing one marker, or repeats them, refuses before any write.
|
|
104
|
+
*/
|
|
105
|
+
export const applyReadmeIndex = (
|
|
106
|
+
previous: string,
|
|
107
|
+
block: string,
|
|
108
|
+
): Effect.Effect<string, ReadmeUnavailable> =>
|
|
109
|
+
Effect.gen(function* () {
|
|
110
|
+
const start = previous.indexOf(README_START);
|
|
111
|
+
const end = previous.indexOf(README_END);
|
|
112
|
+
if (
|
|
113
|
+
(start === -1) !== (end === -1) ||
|
|
114
|
+
(start !== -1 &&
|
|
115
|
+
(end < start ||
|
|
116
|
+
previous.indexOf(README_START, start + README_START.length) !== -1 ||
|
|
117
|
+
previous.indexOf(README_END, end + README_END.length) !== -1))
|
|
118
|
+
) {
|
|
119
|
+
return yield* ReadmeUnavailable.make({
|
|
120
|
+
message:
|
|
121
|
+
"README.md has malformed or duplicate adhere rules markers. Fix them before running adhere rules readme.",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const wrapped = `${README_START}\n${block}\n${README_END}`;
|
|
125
|
+
return start === -1
|
|
126
|
+
? `${previous}${previous === "" ? "" : previous.endsWith("\n\n") ? "" : previous.endsWith("\n") ? "\n" : "\n\n"}${wrapped}\n`
|
|
127
|
+
: `${previous.slice(0, start)}${wrapped}${previous.slice(end + README_END.length)}`;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check that `root`'s README accepts an index refresh, without writing: a
|
|
132
|
+
* local writer calls this before it writes a rule or plugin, so malformed
|
|
133
|
+
* markers fail before a partial change lands.
|
|
134
|
+
*/
|
|
135
|
+
export const validateReadme = (root: string): Effect.Effect<void, ReadmeUnavailable> =>
|
|
136
|
+
Effect.gen(function* () {
|
|
137
|
+
const previous = (yield* readOptional(join(root, "README.md"))) ?? "";
|
|
138
|
+
yield* applyReadmeIndex(previous, "");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Refresh the README at `root` from its rules and lint config. `root` is
|
|
143
|
+
* taken as shared because init knows it is; commands that start in a
|
|
144
|
+
* directory check `isSharedRepo` first.
|
|
145
|
+
*/
|
|
146
|
+
export const refreshReadme = (root: string): Effect.Effect<ReadmeRefresh, ReadmeUnavailable> =>
|
|
147
|
+
Effect.gen(function* () {
|
|
148
|
+
const rules = yield* describeRulesAt(root).pipe(
|
|
149
|
+
Effect.mapError(
|
|
150
|
+
(cause) =>
|
|
151
|
+
new ReadmeUnavailable({
|
|
152
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
153
|
+
}),
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
for (const rule of rules) {
|
|
157
|
+
const text = `${rule.description}\n${rule.body ?? ""}`;
|
|
158
|
+
if (text.includes(README_START) || text.includes(README_END)) {
|
|
159
|
+
return yield* ReadmeUnavailable.make({
|
|
160
|
+
message: `${rule.id} has an adhere rules marker in its description or body; left the README unchanged.`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const config = yield* lintConfigAt(root);
|
|
165
|
+
const entries = lintEntries(config);
|
|
166
|
+
const block = renderReadmeIndex(rules, config);
|
|
167
|
+
const target = join(root, "README.md");
|
|
168
|
+
const previous = (yield* readOptional(target)) ?? "";
|
|
169
|
+
const next = yield* applyReadmeIndex(previous, block);
|
|
170
|
+
const changed = next !== previous;
|
|
171
|
+
if (changed) {
|
|
172
|
+
yield* Effect.tryPromise({
|
|
173
|
+
try: () => writeFile(target, next, "utf8"),
|
|
174
|
+
catch: (cause) =>
|
|
175
|
+
new ReadmeUnavailable({
|
|
176
|
+
message: `Could not write ${target}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const settings = entries.native.length + entries.custom.length;
|
|
181
|
+
return {
|
|
182
|
+
changed,
|
|
183
|
+
message: `${changed ? "Updated" : "Unchanged"} README.md: ${rules.length} ${rules.length === 1 ? "rule" : "rules"}, ${settings} enabled lint ${settings === 1 ? "setting" : "settings"}.`,
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Refresh `root`'s index only when it is a shared repo, as an org push
|
|
189
|
+
* discovered it. A legacy rules-only repo is left exactly as it was: no README
|
|
190
|
+
* is invented. Returns whether the root is shared, so the caller stages
|
|
191
|
+
* README.md only then. Malformed markers propagate and stop the push.
|
|
192
|
+
*/
|
|
193
|
+
export const refreshIndexIfShared = (root: string): Effect.Effect<boolean, ReadmeUnavailable> =>
|
|
194
|
+
Effect.gen(function* () {
|
|
195
|
+
if (!(yield* isSharedRepo(root))) return false;
|
|
196
|
+
yield* refreshReadme(root);
|
|
197
|
+
return true;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
/** Refresh a shared repo's README, refusing a directory that is not one. */
|
|
201
|
+
export const refreshSharedReadme = (
|
|
202
|
+
root: string,
|
|
203
|
+
): Effect.Effect<ReadmeRefresh, ReadmeUnavailable> =>
|
|
204
|
+
Effect.gen(function* () {
|
|
205
|
+
if (!(yield* isSharedRepo(root))) {
|
|
206
|
+
return yield* ReadmeUnavailable.make({
|
|
207
|
+
message: `${root} is not a shared rules repo: package.json must export ${SHARED_LINT_EXPORT}, as adhere init --shared writes it.`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return yield* refreshReadme(root);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const GITHUB_URL = /^(?:[^@/\s]+@)?github\.com(?::\d+)?[:/]([^/\s]+)\/([^/\s]+)$/;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The shared rules repo a GitHub origin implies, as `owner/standards`, from the
|
|
217
|
+
* forms an origin takes: `https://github.com/owner/repo.git`,
|
|
218
|
+
* `git@github.com:owner/repo.git`, and `ssh://git@github.com/owner/repo.git`.
|
|
219
|
+
* An origin on another host, or one that names no owner and repo, implies
|
|
220
|
+
* none. The origin's repo is read for its owner alone: an org's shared rules
|
|
221
|
+
* live in that org's `standards`, never in the consumer repo the origin names.
|
|
222
|
+
*/
|
|
223
|
+
export const standardsRepoOf = (url: string): string | undefined => {
|
|
224
|
+
const path = url
|
|
225
|
+
.trim()
|
|
226
|
+
.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "")
|
|
227
|
+
.replace(/\/+$/, "");
|
|
228
|
+
const [, owner, named] = GITHUB_URL.exec(path) ?? [];
|
|
229
|
+
const repo = named?.replace(/\.git$/, "");
|
|
230
|
+
if (owner === undefined || repo === undefined) return undefined;
|
|
231
|
+
return /^[\w.-]+$/.test(owner) && /^[\w.-]+$/.test(repo) ? `${owner}/standards` : undefined;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* This checkout's `origin` URL, trimmed, or undefined when git names none: no
|
|
236
|
+
* origin, or a directory git does not see as a repo. Read from the config, not
|
|
237
|
+
* `git remote get-url`, which applies `url.<base>.insteadOf` rewrites and so
|
|
238
|
+
* would not say which GitHub repo the origin names.
|
|
239
|
+
*/
|
|
240
|
+
export const originUrlAt = (root: string): Effect.Effect<string | undefined> =>
|
|
241
|
+
Effect.promise(() =>
|
|
242
|
+
run("git", ["config", "--get", "remote.origin.url"], root, "").then(
|
|
243
|
+
(url) => url.trim(),
|
|
244
|
+
() => undefined,
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const git = (cwd: string, args: ReadonlyArray<string>) =>
|
|
249
|
+
Effect.tryPromise({
|
|
250
|
+
try: () => run("git", args, cwd, ""),
|
|
251
|
+
catch: (cause) =>
|
|
252
|
+
new ReadmeUnavailable({ message: cause instanceof Error ? cause.message : String(cause) }),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
/** What a remote refresh did: its README result, the repo, and the branch it pushed. */
|
|
256
|
+
export interface RemoteReadmeRefresh extends ReadmeRefresh {
|
|
257
|
+
readonly repo: string;
|
|
258
|
+
/** The review branch pushed with the new index, or undefined when it was unchanged. */
|
|
259
|
+
readonly branch?: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Refresh the README of `repo` from a clone of its default branch, and push the
|
|
264
|
+
* change on a review branch for a pull request. Only README.md is committed,
|
|
265
|
+
* nothing is pushed when the index is already current, and the clone is
|
|
266
|
+
* temporary, so the checkout this was asked from is untouched. No `./oxlint`
|
|
267
|
+
* export is needed: a rules-only repo has a README index too.
|
|
268
|
+
*/
|
|
269
|
+
export const refreshRemoteReadme = (
|
|
270
|
+
repo: string,
|
|
271
|
+
): Effect.Effect<RemoteReadmeRefresh, ReadmeUnavailable> =>
|
|
272
|
+
Effect.scoped(
|
|
273
|
+
Effect.gen(function* () {
|
|
274
|
+
const clone = yield* cloned({ repo, rule: "" }).pipe(
|
|
275
|
+
Effect.mapError(
|
|
276
|
+
(cause) =>
|
|
277
|
+
new ReadmeUnavailable({
|
|
278
|
+
message: `${cause instanceof Error ? cause.message : String(cause)}. Check that ${repo} exists and git can read it, or name another repo: adhere rules readme org/repo.`,
|
|
279
|
+
}),
|
|
280
|
+
),
|
|
281
|
+
);
|
|
282
|
+
const refreshed = yield* refreshReadme(clone);
|
|
283
|
+
if (!refreshed.changed) return { ...refreshed, repo };
|
|
284
|
+
const branch = `adhere/readme-${Date.now()}`;
|
|
285
|
+
yield* git(clone, ["switch", "--quiet", "--create", branch]);
|
|
286
|
+
yield* git(clone, ["add", "--", "README.md"]);
|
|
287
|
+
yield* git(clone, ["commit", "--quiet", "--message", "Refresh the shared README index"]);
|
|
288
|
+
yield* git(clone, ["push", "--quiet", "origin", branch]);
|
|
289
|
+
return { ...refreshed, repo, branch };
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
AdhereConfig as AdhereConfigSchema,
|
|
3
3
|
CONFIG_FILES,
|
|
4
4
|
ConfigUnavailable,
|
|
5
|
+
declaresNativeReads,
|
|
5
6
|
decodeConfig,
|
|
6
7
|
type Loaded,
|
|
7
8
|
type Flags,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
type ResolvedConfig,
|
|
11
12
|
resolveConfig,
|
|
12
13
|
RULES_DIRECTORY,
|
|
14
|
+
typeAwareOf,
|
|
13
15
|
} from "#config.ts";
|
|
14
16
|
import { type PresetName, presetOf, presets, wholePresetOf } from "#presets.ts";
|
|
15
17
|
import {
|
|
@@ -45,7 +47,10 @@ export const loadConfigFile = Effect.fn("AdhereConfig.load")(function* (file: st
|
|
|
45
47
|
);
|
|
46
48
|
});
|
|
47
49
|
|
|
48
|
-
const loadPreset = Effect.fn("AdhereConfig.loadPreset")(function* (
|
|
50
|
+
export const loadPreset = Effect.fn("AdhereConfig.loadPreset")(function* (
|
|
51
|
+
name: PresetName,
|
|
52
|
+
base: string,
|
|
53
|
+
) {
|
|
49
54
|
const { topic, ...preset } = presetOf(name);
|
|
50
55
|
const all = yield* materializeRules(preset.rules, base);
|
|
51
56
|
const rules =
|
|
@@ -109,9 +114,17 @@ export const AdhereConfigLive = (flags: Flags) =>
|
|
|
109
114
|
? discoveredEntries
|
|
110
115
|
: globalRuleSet(yield* materializeRules(config.rules ?? {}, cwd), cwd);
|
|
111
116
|
const rules = applicableRules(cwd, projectEntries);
|
|
112
|
-
const
|
|
117
|
+
const loadedPresets = yield* Effect.forEach(presetsOf(config, flags), (name) =>
|
|
113
118
|
loadPreset(name, cwd),
|
|
114
119
|
).pipe(Effect.map(Record.fromEntries));
|
|
120
|
+
// A bundled rule that declares the compiler's native reads is left out
|
|
121
|
+
// unless this run enables them; a project's own rule stays and warns.
|
|
122
|
+
const registry = typeAwareOf(config, flags)
|
|
123
|
+
? loadedPresets
|
|
124
|
+
: Record.map(loadedPresets, (preset) => ({
|
|
125
|
+
...preset,
|
|
126
|
+
rules: Record.filter(preset.rules, (rule) => !declaresNativeReads(rule)),
|
|
127
|
+
}));
|
|
115
128
|
const resolved = resolveConfig({ ...config, rules }, flags, registry);
|
|
116
129
|
const presetEntries = Object.entries(registry).flatMap(([name, preset]) =>
|
|
117
130
|
globalRuleSet(preset.rules, cwd).map((entry) => ({
|
|
@@ -127,7 +140,8 @@ export const AdhereConfigLive = (flags: Flags) =>
|
|
|
127
140
|
const loaded = new Set(entries.map(shownId));
|
|
128
141
|
const elsewhere = Object.keys(overrides).filter((id) => !loaded.has(id));
|
|
129
142
|
if (elsewhere.length > 0) {
|
|
130
|
-
// A rule of a preset this run leaves out
|
|
143
|
+
// A rule of a preset this run leaves out, or skips without type awareness,
|
|
144
|
+
// may be overridden; one no rule has is a typo.
|
|
131
145
|
const every = yield* everyPresetRule(cwd);
|
|
132
146
|
const unknown = elsewhere.filter((id) => !every.has(id));
|
|
133
147
|
if (unknown.length > 0) {
|
|
@@ -137,6 +151,25 @@ export const AdhereConfigLive = (flags: Flags) =>
|
|
|
137
151
|
}
|
|
138
152
|
}
|
|
139
153
|
const scopedRules = yield* withOverrides(entries, overrides);
|
|
154
|
+
for (const entry of scopedRules) {
|
|
155
|
+
const missing = (entry.rule.reads ?? []).filter((read) => !resolved.reads.includes(read));
|
|
156
|
+
if (missing.length === 0) continue;
|
|
157
|
+
const workspaceMissing = missing.filter(
|
|
158
|
+
(read) => read === "workspacePackages" || read === "dependents",
|
|
159
|
+
);
|
|
160
|
+
const nativeMissing = missing.filter((read) => read === "symbols" || read === "references");
|
|
161
|
+
const enable = [
|
|
162
|
+
...(workspaceMissing.length === 0
|
|
163
|
+
? []
|
|
164
|
+
: [
|
|
165
|
+
`add ${workspaceMissing.map((read) => `"${read}"`).join(", ")} to the config's reads`,
|
|
166
|
+
]),
|
|
167
|
+
...(nativeMissing.length === 0 ? [] : ["enable typeAware: true (or pass --type-aware)"]),
|
|
168
|
+
].join(" and ");
|
|
169
|
+
yield* Effect.logWarning(
|
|
170
|
+
`${shownId(entry)} declares reads ${missing.join(", ")}, which this run does not enable; the rule may be less accurate until enabled. ${enable}.`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
140
173
|
// Read on every run so workspace edits are seen, but only when the config enables this context.
|
|
141
174
|
const dependents = resolved.reads.includes("dependents");
|
|
142
175
|
const workspacePackages =
|