@drkmttr/adhere 0.18.1 → 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 +62 -1575
- package/package.json +18 -15
- package/presets/README.md +140 -0
- 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 -29
- package/skills/adhere-fix/SKILL.md +1 -1
- package/skills/adhere-setup/SKILL.md +60 -26
- package/src/cli.ts +334 -36
- package/src/config.ts +173 -142
- package/src/create/agents.ts +149 -0
- package/src/create/ask.ts +363 -0
- package/src/create/examples.ts +94 -0
- package/src/create/look.ts +191 -0
- 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 +466 -0
- package/src/index.ts +2 -0
- package/src/init.ts +171 -41
- package/src/install.ts +296 -94
- package/src/instructions.ts +98 -0
- package/src/markdown.ts +15 -11
- 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/rules.ts +42 -45
- package/src/services/AdhereConfig.ts +67 -21
- package/src/services/AuditCache.ts +176 -169
- package/src/services/Credentials.ts +14 -8
- package/src/services/Jev.reads.test.ts +22 -0
- package/src/services/Jev.ts +35 -13
- package/src/services/SourceWalker.ts +12 -5
- package/src/wording.ts +4 -4
- package/src/workflows/audit.ts +85 -14
- package/src/workflows/format.ts +98 -1
- package/src/workspaces.test.ts +37 -1
- package/src/workspaces.ts +121 -12
package/src/init.ts
CHANGED
|
@@ -2,6 +2,17 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, relative } from "node:path";
|
|
4
4
|
import { CONFIG_FILES } from "#config.ts";
|
|
5
|
+
import {
|
|
6
|
+
SHARED_LINT_DIRECTORY,
|
|
7
|
+
SHARED_LINT_INDEX,
|
|
8
|
+
SHARED_LINT_RULES,
|
|
9
|
+
SHARED_LINT_RULES_JSON,
|
|
10
|
+
SHARED_LINT_TYPES,
|
|
11
|
+
SHARED_LINT_TYPES_SOURCE,
|
|
12
|
+
sharedPackageJson,
|
|
13
|
+
sharedPackageName,
|
|
14
|
+
} from "#oxlint/shared.ts";
|
|
15
|
+
import { README_END, README_START, refreshReadme } from "#readme.ts";
|
|
5
16
|
import { Effect, Schema } from "effect";
|
|
6
17
|
|
|
7
18
|
export interface InitResult {
|
|
@@ -87,6 +98,16 @@ export const renderInvoice = (invoice: Invoice) => Html.table(invoice.lines);
|
|
|
87
98
|
\`\`\`
|
|
88
99
|
`;
|
|
89
100
|
|
|
101
|
+
const INSTRUCTIONS = `# Working with adhere
|
|
102
|
+
|
|
103
|
+
- Run \`adhere lint\` before you commit, and fix what it reports.
|
|
104
|
+
- A rule belongs in \`.adhere/rules/\`: one sentence saying what code must be,
|
|
105
|
+
with code that shows it.
|
|
106
|
+
- Run \`adhere validate\` after changing a rule. When a finding is wrong for
|
|
107
|
+
this code alone, say why in a suppression comment instead of weakening the
|
|
108
|
+
rule.
|
|
109
|
+
`;
|
|
110
|
+
|
|
90
111
|
const NAME_EFFECTS = `---
|
|
91
112
|
description: A function must be named after the domain action it performs, never after a generic verb like handle.
|
|
92
113
|
threshold: 0.8
|
|
@@ -116,35 +137,135 @@ export const handle = (id: string) =>
|
|
|
116
137
|
|
|
117
138
|
const sharedReadme = (org: string, repo: string) => `# ${repo}
|
|
118
139
|
|
|
119
|
-
Shared [adhere](https://github.com/darkmatter/adhere) rules
|
|
120
|
-
repositories.
|
|
121
|
-
|
|
122
|
-
|
|
140
|
+
Shared [adhere](https://github.com/darkmatter/adhere) rules and lint settings
|
|
141
|
+
for ${org}'s repositories. \`.adhere/rules/\` holds the rules Jev judges files
|
|
142
|
+
against, \`.adhere/instructions/\` holds instruction files for \`AGENTS.md\`, and
|
|
143
|
+
\`oxlint/\` is a plain ESM Oxlint config package for rules a linter can check
|
|
144
|
+
exactly.
|
|
123
145
|
|
|
124
146
|
## Use these rules
|
|
125
147
|
|
|
126
|
-
List the rules, then copy them into a repo
|
|
148
|
+
List the rules, then copy them into a repo: every one, one topic, or one rule.
|
|
127
149
|
|
|
128
150
|
\`\`\`sh
|
|
129
151
|
adhere list ${org}/${repo}
|
|
130
|
-
adhere install ${org}/${repo}
|
|
152
|
+
adhere install ${org}/${repo} # choose at a terminal
|
|
153
|
+
adhere install ${org}/${repo} --all # everything, without a prompt
|
|
131
154
|
adhere install ${org}/${repo}/style
|
|
132
155
|
adhere install ${org}/${repo}/style/prefer-small-files
|
|
133
156
|
\`\`\`
|
|
134
157
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
158
|
+
At a terminal, a bare install lists what it can copy: instruction files
|
|
159
|
+
first, then rules grouped by topic. \`--all\` copies every instruction file and
|
|
160
|
+
rule the source names without asking and needs no terminal. Without either, a
|
|
161
|
+
run without a terminal fails and names the matches. A topic is the same list
|
|
162
|
+
filtered to that topic, and an exact rule's path copies just that rule;
|
|
163
|
+
neither copies instructions.
|
|
164
|
+
|
|
165
|
+
Rules land in \`.adhere/rules/${org}/${repo}/\` and instructions in
|
|
166
|
+
\`.adhere/instructions/${org}/${repo}/\`, and are that repo's own from then on,
|
|
167
|
+
to edit or to start other rules from. To take a later version, install again
|
|
168
|
+
with \`--force\`, which overwrites local edits; deselecting a file never
|
|
169
|
+
deletes it.
|
|
170
|
+
|
|
171
|
+
## Use its lint settings
|
|
172
|
+
|
|
173
|
+
The lint package is not published. Pin this repo as a git dependency, at a
|
|
174
|
+
tag: tags are repo-wide, covering the rules, the instructions, and \`oxlint/\`
|
|
175
|
+
together.
|
|
176
|
+
|
|
177
|
+
\`\`\`sh
|
|
178
|
+
bun add -D "github:${org}/${repo}#v1.0.0"
|
|
179
|
+
\`\`\`
|
|
180
|
+
|
|
181
|
+
Then spread it into Vite+'s config, or plain Oxlint's:
|
|
182
|
+
|
|
183
|
+
\`\`\`ts
|
|
184
|
+
import sharedLint from "${sharedPackageName(org, repo)}/oxlint";
|
|
185
|
+
import { defineConfig } from "vite-plus";
|
|
186
|
+
|
|
187
|
+
export default defineConfig({
|
|
188
|
+
lint: {
|
|
189
|
+
...sharedLint,
|
|
190
|
+
jsPlugins: [...sharedLint.jsPlugins, "./.adhere/oxlint/local.mjs"],
|
|
191
|
+
rules: {
|
|
192
|
+
...sharedLint.rules,
|
|
193
|
+
"local/my-rule": "error",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
\`\`\`
|
|
198
|
+
|
|
199
|
+
\`\`\`ts
|
|
200
|
+
// oxlint.config.ts, with plain Oxlint: oxlint -c oxlint.config.ts
|
|
201
|
+
import sharedLint from "${sharedPackageName(org, repo)}/oxlint";
|
|
202
|
+
|
|
203
|
+
export default { ...sharedLint };
|
|
204
|
+
\`\`\`
|
|
205
|
+
|
|
206
|
+
\`oxlint/index.js\` reads \`oxlint/rules.json\` and resolves the shared package's
|
|
207
|
+
plugin paths, so spreading its export works from any directory. Paths you add
|
|
208
|
+
stay relative to your config. Your project's own JS plugins live in
|
|
209
|
+
\`.adhere/oxlint/\` as \`.mjs\` files.
|
|
210
|
+
|
|
211
|
+
## Rules and lint settings
|
|
212
|
+
|
|
213
|
+
The guide below is generated from \`.adhere/rules/\` and \`oxlint/rules.json\` by
|
|
214
|
+
\`adhere rules readme\`. Run it after adding, renaming, or deleting a rule, or
|
|
215
|
+
after changing a lint setting; it replaces only the block between its markers.
|
|
216
|
+
Rules are grouped by topic, and each entry opens its own statement, examples,
|
|
217
|
+
and explanation, with a link to the file it came from.
|
|
218
|
+
|
|
219
|
+
${README_START}
|
|
220
|
+
${README_END}
|
|
138
221
|
|
|
139
222
|
## Add a rule
|
|
140
223
|
|
|
141
224
|
Add a directory under \`.adhere/rules/<topic>/\` with the rule in a \`RULE.md\`;
|
|
142
225
|
its path is the rule's id. See
|
|
143
|
-
[Rules as Markdown files](https://
|
|
144
|
-
and the [rule writing tips](https://
|
|
226
|
+
[Rules as Markdown files](https://dm.sh/adhere/rule-format/#rules-as-markdown-files)
|
|
227
|
+
and the [rule writing tips](https://dm.sh/adhere/writing-good-rules/#rule-writing-tips).
|
|
145
228
|
\`adhere validate\` checks each rule's wording and asks Jev whether any two
|
|
146
229
|
contradict. CI runs it on every push to main and every pull request.
|
|
147
230
|
|
|
231
|
+
## Add a lint setting or plugin
|
|
232
|
+
|
|
233
|
+
\`oxlint/rules.json\` starts \`{}\` and takes \`rules\`, \`jsPlugins\`, \`plugins\`, and
|
|
234
|
+
\`options\`. \`options\` is Oxlint's own options object; Vite+ uses the same
|
|
235
|
+
\`lint.options\` shape, and type-aware rules need \`oxlint-tsgolint\`, which Vite+
|
|
236
|
+
includes. Turn on a built-in rule by naming it:
|
|
237
|
+
|
|
238
|
+
\`\`\`json
|
|
239
|
+
{ "rules": { "typescript/no-explicit-any": "error" } }
|
|
240
|
+
\`\`\`
|
|
241
|
+
|
|
242
|
+
A rule from a built-in plugin that Oxlint leaves off by default (\`react\`,
|
|
243
|
+
\`import\`, \`jsdoc\`, \`jest\`, \`vitest\`, \`jsx-a11y\`, \`nextjs\`, \`react-perf\`,
|
|
244
|
+
\`promise\`, \`node\`, \`vue\`) also needs that plugin in \`plugins\`. The list
|
|
245
|
+
replaces Oxlint's default set (\`eslint\`, \`typescript\`, \`unicorn\`, \`oxc\`), so
|
|
246
|
+
name every plugin you still want:
|
|
247
|
+
|
|
248
|
+
\`\`\`json
|
|
249
|
+
{
|
|
250
|
+
"plugins": ["eslint", "typescript", "unicorn", "oxc", "react"],
|
|
251
|
+
"rules": { "react/jsx-no-target-blank": "error" }
|
|
252
|
+
}
|
|
253
|
+
\`\`\`
|
|
254
|
+
|
|
255
|
+
A custom plugin is a plain ESM file in \`oxlint/plugins/\`; register it by a path
|
|
256
|
+
relative to \`oxlint/\`, and enable one of its rules by the plugin's name:
|
|
257
|
+
|
|
258
|
+
\`\`\`json
|
|
259
|
+
{
|
|
260
|
+
"jsPlugins": ["./plugins/${repo}.js"],
|
|
261
|
+
"rules": { "${repo}/no-foo": "error" }
|
|
262
|
+
}
|
|
263
|
+
\`\`\`
|
|
264
|
+
|
|
265
|
+
Keep the keys you are not changing. A project writes its own \`.mjs\` plugins to
|
|
266
|
+
\`.adhere/oxlint/\` and registers them in its \`vite.config.ts\` or
|
|
267
|
+
\`oxlint.config.ts\` instead; see the lint settings above.
|
|
268
|
+
|
|
148
269
|
## Setup
|
|
149
270
|
|
|
150
271
|
\`alchemy.run.ts\` creates this repository on GitHub, or adopts it when it
|
|
@@ -159,6 +280,17 @@ git remote add origin git@github.com:${org}/${repo}.git && git push -u origin ma
|
|
|
159
280
|
\`\`\`
|
|
160
281
|
|
|
161
282
|
Deploying again updates the secret, as after rotating the key.
|
|
283
|
+
|
|
284
|
+
## Tag a release
|
|
285
|
+
|
|
286
|
+
Tag the whole repo after committing rules, instructions, or lint changes. One
|
|
287
|
+
tag covers all of them, and consumers pin that tag; there are no per-rule
|
|
288
|
+
tags.
|
|
289
|
+
|
|
290
|
+
\`\`\`sh
|
|
291
|
+
git tag v1.0.0
|
|
292
|
+
git push origin v1.0.0
|
|
293
|
+
\`\`\`
|
|
162
294
|
`;
|
|
163
295
|
|
|
164
296
|
const SHARED_WORKFLOW = `name: adhere
|
|
@@ -173,7 +305,8 @@ jobs:
|
|
|
173
305
|
runs-on: ubuntu-latest
|
|
174
306
|
steps:
|
|
175
307
|
- uses: actions/checkout@v4
|
|
176
|
-
- run:
|
|
308
|
+
- run: curl -fsSL https://github.com/darkmatter/adhere/raw/main/scripts/install.sh | sh
|
|
309
|
+
- run: ~/.local/bin/adhere validate
|
|
177
310
|
env:
|
|
178
311
|
TYPESAFE_API_KEY: \${{ secrets.TYPESAFE_API_KEY }}
|
|
179
312
|
`;
|
|
@@ -213,29 +346,6 @@ export default Alchemy.Stack(
|
|
|
213
346
|
);
|
|
214
347
|
`;
|
|
215
348
|
|
|
216
|
-
/**
|
|
217
|
-
* alchemy's CLI runs on effect and its Node platform. They are pinned, as is
|
|
218
|
-
* the platform's own dependency, since a range of prereleases takes the newest:
|
|
219
|
-
* effect 4.0.0-rc.118 moved modules alchemy 2.0.0-beta.79 imports (checked
|
|
220
|
-
* 2026-09-28).
|
|
221
|
-
*/
|
|
222
|
-
const sharedPackage = (repo: string) =>
|
|
223
|
-
`${JSON.stringify(
|
|
224
|
-
{
|
|
225
|
-
name: repo,
|
|
226
|
-
private: true,
|
|
227
|
-
type: "module",
|
|
228
|
-
devDependencies: {
|
|
229
|
-
"@effect/platform-node": "4.0.0-rc.117",
|
|
230
|
-
alchemy: "2.0.0-beta.79",
|
|
231
|
-
effect: "4.0.0-rc.117",
|
|
232
|
-
},
|
|
233
|
-
overrides: { "@effect/platform-node-shared": "4.0.0-rc.117" },
|
|
234
|
-
},
|
|
235
|
-
null,
|
|
236
|
-
2,
|
|
237
|
-
)}\n`;
|
|
238
|
-
|
|
239
349
|
const SHARED_GITIGNORE = `node_modules/
|
|
240
350
|
.alchemy/
|
|
241
351
|
`;
|
|
@@ -245,30 +355,38 @@ const CONFIG_PATH = CONFIG_FILES[0];
|
|
|
245
355
|
/** The project of the config at `.adhere/config.ts`, and of the rules written in TypeScript beside it. */
|
|
246
356
|
const CONFIG_PROJECT = ".adhere/tsconfig.json";
|
|
247
357
|
|
|
248
|
-
const
|
|
358
|
+
const RULE_EXAMPLES = [
|
|
249
359
|
[".adhere/rules/style/prefer-small-files/RULE.md", SMALL_FILES],
|
|
250
360
|
[".adhere/rules/style/name-domain-actions/RULE.md", NAME_EFFECTS],
|
|
251
361
|
] as const;
|
|
252
362
|
|
|
363
|
+
const SHARED_EXAMPLES = [
|
|
364
|
+
[".adhere/instructions/00-workflow.md", INSTRUCTIONS],
|
|
365
|
+
...RULE_EXAMPLES,
|
|
366
|
+
] as const;
|
|
367
|
+
|
|
253
368
|
/**
|
|
254
|
-
* A shared repo has no config: \`adhere install\` copies
|
|
255
|
-
* \`validate\` reads \`.adhere/rules/\` without one.
|
|
369
|
+
* A shared repo has no config: \`adhere install\` copies its rules and
|
|
370
|
+
* instructions, and \`validate\` reads \`.adhere/rules/\` without one.
|
|
256
371
|
*/
|
|
257
372
|
const filesFor = (
|
|
258
373
|
shared: string | undefined,
|
|
259
374
|
visibility: Visibility,
|
|
260
375
|
): ReadonlyArray<readonly [string, string]> => {
|
|
261
376
|
if (shared === undefined) {
|
|
262
|
-
return [[CONFIG_PATH, CONFIG], [CONFIG_PROJECT, TSCONFIG], ...
|
|
377
|
+
return [[CONFIG_PATH, CONFIG], [CONFIG_PROJECT, TSCONFIG], ...RULE_EXAMPLES];
|
|
263
378
|
}
|
|
264
379
|
const [org = "", repo = ""] = shared.split("/");
|
|
265
380
|
return [
|
|
266
381
|
["README.md", sharedReadme(org, repo)],
|
|
267
382
|
[".github/workflows/adhere.yaml", SHARED_WORKFLOW],
|
|
268
383
|
["alchemy.run.ts", sharedStack(org, repo, visibility)],
|
|
269
|
-
["package.json",
|
|
384
|
+
["package.json", sharedPackageJson(org, repo)],
|
|
385
|
+
[`${SHARED_LINT_DIRECTORY}/index.js`, SHARED_LINT_INDEX],
|
|
386
|
+
[SHARED_LINT_TYPES, SHARED_LINT_TYPES_SOURCE],
|
|
387
|
+
[SHARED_LINT_RULES, SHARED_LINT_RULES_JSON],
|
|
270
388
|
[".gitignore", SHARED_GITIGNORE],
|
|
271
|
-
...
|
|
389
|
+
...SHARED_EXAMPLES,
|
|
272
390
|
];
|
|
273
391
|
};
|
|
274
392
|
|
|
@@ -405,6 +523,15 @@ export const initProject = (
|
|
|
405
523
|
}
|
|
406
524
|
// A config at another path is in the project's own tsconfig, or not ours to place.
|
|
407
525
|
if (path === CONFIG_PROJECT && existing !== undefined) continue;
|
|
526
|
+
// A shared README holds the author's prose; refresh its managed block instead.
|
|
527
|
+
if (
|
|
528
|
+
options.shared !== undefined &&
|
|
529
|
+
path === "README.md" &&
|
|
530
|
+
(yield* Effect.promise(() => exists(join(root, path))))
|
|
531
|
+
) {
|
|
532
|
+
skipped.push(relative(root, join(root, path)));
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
408
535
|
const status = yield* writeScaffoldFile(root, path, contents, options);
|
|
409
536
|
if (status === "created") {
|
|
410
537
|
created.push(relative(root, join(root, path)));
|
|
@@ -412,6 +539,9 @@ export const initProject = (
|
|
|
412
539
|
skipped.push(relative(root, join(root, path)));
|
|
413
540
|
}
|
|
414
541
|
}
|
|
542
|
+
if (options.shared !== undefined) {
|
|
543
|
+
yield* refreshReadme(root).pipe(Effect.mapError((cause) => new Error(cause.message)));
|
|
544
|
+
}
|
|
415
545
|
const install = yield* installPackages(root, options.shared !== undefined);
|
|
416
546
|
return { created, skipped, install };
|
|
417
547
|
});
|