@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.
Files changed (43) hide show
  1. package/README.md +62 -1575
  2. package/package.json +18 -15
  3. package/presets/README.md +140 -0
  4. package/presets/alchemy/runtime/bind-tag-not-implementation/RULE.md +1 -0
  5. package/presets/effect/data/brand-meaningful-primitives/RULE.md +1 -0
  6. package/presets/effect/data/variants-are-tagged-unions/RULE.md +1 -0
  7. package/presets/typescript/types/derive-dont-restate/RULE.md +1 -0
  8. package/skills/adhere/SKILL.md +194 -29
  9. package/skills/adhere-fix/SKILL.md +1 -1
  10. package/skills/adhere-setup/SKILL.md +60 -26
  11. package/src/cli.ts +334 -36
  12. package/src/config.ts +173 -142
  13. package/src/create/agents.ts +149 -0
  14. package/src/create/ask.ts +363 -0
  15. package/src/create/examples.ts +94 -0
  16. package/src/create/look.ts +191 -0
  17. package/src/create/oxlint-trial.ts +464 -0
  18. package/src/create/oxlint.ts +1012 -0
  19. package/src/create/trial-review.ts +562 -0
  20. package/src/create/trial.ts +611 -0
  21. package/src/create/wizard.ts +466 -0
  22. package/src/index.ts +2 -0
  23. package/src/init.ts +171 -41
  24. package/src/install.ts +296 -94
  25. package/src/instructions.ts +98 -0
  26. package/src/markdown.ts +15 -11
  27. package/src/oxlint/catalog.json +7844 -0
  28. package/src/oxlint/catalog.ts +72 -0
  29. package/src/oxlint/shared.ts +140 -0
  30. package/src/readme-guide.ts +431 -0
  31. package/src/readme.ts +291 -0
  32. package/src/rules.ts +42 -45
  33. package/src/services/AdhereConfig.ts +67 -21
  34. package/src/services/AuditCache.ts +176 -169
  35. package/src/services/Credentials.ts +14 -8
  36. package/src/services/Jev.reads.test.ts +22 -0
  37. package/src/services/Jev.ts +35 -13
  38. package/src/services/SourceWalker.ts +12 -5
  39. package/src/wording.ts +4 -4
  40. package/src/workflows/audit.ts +85 -14
  41. package/src/workflows/format.ts +98 -1
  42. package/src/workspaces.test.ts +37 -1
  43. 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 for ${org}'s
120
- repositories. Each rule is a directory in \`.adhere/rules/\` that holds a
121
- \`RULE.md\`, or a \`RULE.ts\`, and a repo that copies it in has Jev judge its
122
- files against it when it runs \`adhere lint\`.
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, all of them or one topic or rule:
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
- The copies land in \`.adhere/rules/${org}/${repo}/\` and are that repo's own from then
136
- on, to edit or to start other rules from. To take a later version of a rule,
137
- install it again with \`--force\`, which overwrites local edits.
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://github.com/darkmatter/adhere#rules-as-markdown-files)
144
- and the [rule writing tips](https://github.com/darkmatter/adhere#rule-writing-tips).
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: npx --yes @drkmttr/adhere validate
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 EXAMPLES = [
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 only rules, and
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], ...EXAMPLES];
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", sharedPackage(repo)],
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
- ...EXAMPLES,
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
  });