@drkmttr/adhere 0.12.0 → 0.13.1

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 (104) hide show
  1. package/README.md +194 -51
  2. package/package.json +9 -9
  3. package/presets/effect/basics/{instrument-with-pipe.md → instrument-with-pipe/RULE.md} +1 -1
  4. package/presets/effect/config/{business-logic-depends-on-config-service.md → business-logic-depends-on-config-service/RULE.md} +3 -3
  5. package/presets/effect/config/{secrets-are-redacted.md → secrets-are-redacted/RULE.md} +3 -3
  6. package/presets/effect/config/{validate-with-schema.md → validate-with-schema/RULE.md} +4 -4
  7. package/skills/adhere/SKILL.md +213 -118
  8. package/skills/adhere-fix/SKILL.md +9 -8
  9. package/skills/adhere-setup/SKILL.md +244 -0
  10. package/src/cli.ts +49 -33
  11. package/src/config.ts +64 -3
  12. package/src/index.ts +2 -0
  13. package/src/init.ts +36 -25
  14. package/src/install.ts +37 -26
  15. package/src/main.ts +1 -1
  16. package/src/markdown.ts +9 -1
  17. package/src/rules.ts +135 -56
  18. package/src/services/AdhereConfig.ts +15 -3
  19. package/src/services/Credentials.ts +4 -4
  20. package/src/services/Jev.http.ts +82 -10
  21. package/src/services/Jev.ts +41 -10
  22. package/src/workflows/audit.ts +33 -14
  23. package/src/workflows/format.ts +172 -35
  24. package/src/workspaces.ts +141 -0
  25. /package/presets/alchemy/apis/{match-remote-errors-by-tag.md → match-remote-errors-by-tag/RULE.md} +0 -0
  26. /package/presets/alchemy/apis/{schemas-at-trust-boundaries.md → schemas-at-trust-boundaries/RULE.md} +0 -0
  27. /package/presets/alchemy/data/{commit-migrations-then-deploy.md → commit-migrations-then-deploy/RULE.md} +0 -0
  28. /package/presets/alchemy/data/{hyperdrive-direct-origin.md → hyperdrive-direct-origin/RULE.md} +0 -0
  29. /package/presets/alchemy/data/{migrations-use-direct-url.md → migrations-use-direct-url/RULE.md} +0 -0
  30. /package/presets/alchemy/data/{never-disable-tls-verification.md → never-disable-tls-verification/RULE.md} +0 -0
  31. /package/presets/alchemy/data/{private-endpoint-at-runtime.md → private-endpoint-at-runtime/RULE.md} +0 -0
  32. /package/presets/alchemy/data/{single-migration-owner.md → single-migration-owner/RULE.md} +0 -0
  33. /package/presets/alchemy/durable/{idempotent-event-handlers.md → idempotent-event-handlers/RULE.md} +0 -0
  34. /package/presets/alchemy/durable/{io-inside-steps.md → io-inside-steps/RULE.md} +0 -0
  35. /package/presets/alchemy/durable/{no-self-triggering-bucket-writes.md → no-self-triggering-bucket-writes/RULE.md} +0 -0
  36. /package/presets/alchemy/durable/{stable-step-names.md → stable-step-names/RULE.md} +0 -0
  37. /package/presets/alchemy/durable/{webhook-state-is-truth.md → webhook-state-is-truth/RULE.md} +0 -0
  38. /package/presets/alchemy/providers/{diff-guards-unresolved-props.md → diff-guards-unresolved-props/RULE.md} +0 -0
  39. /package/presets/alchemy/providers/{idempotent-delete.md → idempotent-delete/RULE.md} +0 -0
  40. /package/presets/alchemy/providers/{lazy-layer-construction.md → lazy-layer-construction/RULE.md} +0 -0
  41. /package/presets/alchemy/providers/{reconcile-from-observed-state.md → reconcile-from-observed-state/RULE.md} +0 -0
  42. /package/presets/alchemy/resources/{anchor-paths-with-import-meta.md → anchor-paths-with-import-meta/RULE.md} +0 -0
  43. /package/presets/alchemy/resources/{idempotent-actions-and-seeds.md → idempotent-actions-and-seeds/RULE.md} +0 -0
  44. /package/presets/alchemy/resources/{previews-reference-shared-resources.md → previews-reference-shared-resources/RULE.md} +0 -0
  45. /package/presets/alchemy/resources/{protect-irreplaceable-data.md → protect-irreplaceable-data/RULE.md} +0 -0
  46. /package/presets/alchemy/resources/{remote-state-for-ci.md → remote-state-for-ci/RULE.md} +0 -0
  47. /package/presets/alchemy/resources/{stable-logical-ids.md → stable-logical-ids/RULE.md} +0 -0
  48. /package/presets/alchemy/resources/{stage-unique-physical-names.md → stage-unique-physical-names/RULE.md} +0 -0
  49. /package/presets/alchemy/runtime/{bind-tag-not-implementation.md → bind-tag-not-implementation/RULE.md} +0 -0
  50. /package/presets/alchemy/runtime/{deterministic-config-defaults.md → deterministic-config-defaults/RULE.md} +0 -0
  51. /package/presets/alchemy/runtime/{disposables-in-request-scope.md → disposables-in-request-scope/RULE.md} +0 -0
  52. /package/presets/alchemy/runtime/{lambda-no-post-response-work.md → lambda-no-post-response-work/RULE.md} +0 -0
  53. /package/presets/alchemy/runtime/{no-per-request-work-at-construction.md → no-per-request-work-at-construction/RULE.md} +0 -0
  54. /package/presets/alchemy/runtime/{resolve-config-and-bindings-at-construction.md → resolve-config-and-bindings-at-construction/RULE.md} +0 -0
  55. /package/presets/alchemy/secrets/{let-alchemy-mint-infra-secrets.md → let-alchemy-mint-infra-secrets/RULE.md} +0 -0
  56. /package/presets/alchemy/secrets/{none-in-browser-env.md → none-in-browser-env/RULE.md} +0 -0
  57. /package/presets/alchemy/secrets/{redacted-until-use.md → redacted-until-use/RULE.md} +0 -0
  58. /package/presets/alchemy/secrets/{unwrap-before-compare-or-send.md → unwrap-before-compare-or-send/RULE.md} +0 -0
  59. /package/presets/alchemy/security/{authorize-public-endpoints.md → authorize-public-endpoints/RULE.md} +0 -0
  60. /package/presets/alchemy/security/{github-secret-not-variable.md → github-secret-not-variable/RULE.md} +0 -0
  61. /package/presets/alchemy/security/{identity-from-verified-session.md → identity-from-verified-session/RULE.md} +0 -0
  62. /package/presets/alchemy/security/{keep-auth-protections-on.md → keep-auth-protections-on/RULE.md} +0 -0
  63. /package/presets/alchemy/security/{least-privilege-bindings.md → least-privilege-bindings/RULE.md} +0 -0
  64. /package/presets/alchemy/security/{least-privilege-ci-credentials.md → least-privilege-ci-credentials/RULE.md} +0 -0
  65. /package/presets/alchemy/security/{lookup-failure-is-not-anonymous.md → lookup-failure-is-not-anonymous/RULE.md} +0 -0
  66. /package/presets/effect/basics/{gen-for-sequencing.md → gen-for-sequencing/RULE.md} +0 -0
  67. /package/presets/effect/config/{tests-provide-values-directly.md → tests-provide-values-directly/RULE.md} +0 -0
  68. /package/presets/effect/data/{brand-meaningful-primitives.md → brand-meaningful-primitives/RULE.md} +0 -0
  69. /package/presets/effect/data/{variants-are-tagged-unions.md → variants-are-tagged-unions/RULE.md} +0 -0
  70. /package/presets/effect/errors/{catch-defects-at-boundaries-only.md → catch-defects-at-boundaries-only/RULE.md} +0 -0
  71. /package/presets/effect/errors/{defects-for-bugs.md → defects-for-bugs/RULE.md} +0 -0
  72. /package/presets/effect/errors/{wrap-external-errors.md → wrap-external-errors/RULE.md} +0 -0
  73. /package/presets/effect/services/{dependencies-through-layers.md → dependencies-through-layers/RULE.md} +0 -0
  74. /package/presets/effect/services/{fresh-layer-per-test.md → fresh-layer-per-test/RULE.md} +0 -0
  75. /package/presets/effect/services/{memoize-parameterized-layers.md → memoize-parameterized-layers/RULE.md} +0 -0
  76. /package/presets/effect/services/{no-mutable-state.md → no-mutable-state/RULE.md} +0 -0
  77. /package/presets/effect/services/{operations-have-no-requirements.md → operations-have-no-requirements/RULE.md} +0 -0
  78. /package/presets/effect/services/{test-layers-are-in-memory.md → test-layers-are-in-memory/RULE.md} +0 -0
  79. /package/presets/effect/testing/{test-clock-for-time.md → test-clock-for-time/RULE.md} +0 -0
  80. /package/presets/react/effects/{clean-up-what-they-start.md → clean-up-what-they-start/RULE.md} +0 -0
  81. /package/presets/react/effects/{event-logic-in-handlers.md → event-logic-in-handlers/RULE.md} +0 -0
  82. /package/presets/react/effects/{external-stores-use-sync-external-store.md → external-stores-use-sync-external-store/RULE.md} +0 -0
  83. /package/presets/react/effects/{fetches-ignore-stale-responses.md → fetches-ignore-stale-responses/RULE.md} +0 -0
  84. /package/presets/react/effects/{notify-parents-in-handlers.md → notify-parents-in-handlers/RULE.md} +0 -0
  85. /package/presets/react/server/{no-private-data-to-client.md → no-private-data-to-client/RULE.md} +0 -0
  86. /package/presets/react/server/{server-functions-authorize-and-validate.md → server-functions-authorize-and-validate/RULE.md} +0 -0
  87. /package/presets/react/state/{no-contradictory-state.md → no-contradictory-state/RULE.md} +0 -0
  88. /package/presets/react/state/{no-props-copied-into-state.md → no-props-copied-into-state/RULE.md} +0 -0
  89. /package/presets/react/state/{store-ids-not-copies.md → store-ids-not-copies/RULE.md} +0 -0
  90. /package/presets/security/{constant-time-secret-compare.md → constant-time-secret-compare/RULE.md} +0 -0
  91. /package/presets/security/{no-secrets-in-output.md → no-secrets-in-output/RULE.md} +0 -0
  92. /package/presets/security/{no-untrusted-input-in-commands-or-paths.md → no-untrusted-input-in-commands-or-paths/RULE.md} +0 -0
  93. /package/presets/security/{parameterized-queries.md → parameterized-queries/RULE.md} +0 -0
  94. /package/presets/typescript/async/{concurrent-independent-work.md → concurrent-independent-work/RULE.md} +0 -0
  95. /package/presets/typescript/async/{network-calls-have-timeouts.md → network-calls-have-timeouts/RULE.md} +0 -0
  96. /package/presets/typescript/design/{no-argument-mutation.md → no-argument-mutation/RULE.md} +0 -0
  97. /package/presets/typescript/errors/{no-swallowed-errors.md → no-swallowed-errors/RULE.md} +0 -0
  98. /package/presets/typescript/errors/{release-on-every-path.md → release-on-every-path/RULE.md} +0 -0
  99. /package/presets/typescript/testing/{assert-outcomes.md → assert-outcomes/RULE.md} +0 -0
  100. /package/presets/typescript/testing/{independent-tests.md → independent-tests/RULE.md} +0 -0
  101. /package/presets/typescript/types/{assertions-only-when-proven.md → assertions-only-when-proven/RULE.md} +0 -0
  102. /package/presets/typescript/types/{derive-dont-restate.md → derive-dont-restate/RULE.md} +0 -0
  103. /package/presets/typescript/types/{unrepresentable-invalid-states.md → unrepresentable-invalid-states/RULE.md} +0 -0
  104. /package/presets/typescript/types/{validate-external-data.md → validate-external-data/RULE.md} +0 -0
@@ -0,0 +1,244 @@
1
+ ---
2
+ name: adhere-setup
3
+ description: Set adhere up in a repository with the user. Scan the repo for conventions a normal linter cannot check and draft them as rules, add fitting presets and rules from a shared org repo, let the user preview and choose which to import, validate them, walk through the first lint, and add adhere to CI. Use when a repo has no adhere rules yet, or the user asks to set up, adopt, or onboard adhere.
4
+ ---
5
+
6
+ # Set up adhere in a repo
7
+
8
+ You and the user go from no rules to adhere passing in CI. The user decides
9
+ what gets imported, what it may cost, and what reaches GitHub; you do the
10
+ reading, drafting, and running. The adhere skill, which `adhere skill`
11
+ prints, is the reference for the rule format, the config, and tuning: read it
12
+ before step 3.
13
+
14
+ At each point marked **Ask**, stop and wait for the answer. Use a structured
15
+ question tool if you have one; otherwise ask in plain text with numbered
16
+ choices.
17
+
18
+ ## 1. Check the tools
19
+
20
+ Use the package manager the repo already uses, for every command in this
21
+ skill; never bring in another. Tell it by the lockfile beside `package.json`,
22
+ as `adhere init` does: `bun.lock` or `bun.lockb` for bun, `pnpm-lock.yaml` for
23
+ pnpm, `yarn.lock` for yarn, `package-lock.json` for npm. Without a lockfile,
24
+ the `packageManager` field in `package.json` names it; with neither, **Ask**.
25
+
26
+ | Manager | Add adhere | Run adhere |
27
+ | ------- | -------------------------------- | ------------------ |
28
+ | bun | `bun add -D @drkmttr/adhere` | `bunx adhere` |
29
+ | pnpm | `pnpm add -D @drkmttr/adhere` | `pnpm exec adhere` |
30
+ | yarn | `yarn add -D @drkmttr/adhere` | `yarn adhere` |
31
+ | npm | `npm install -D @drkmttr/adhere` | `npx adhere` |
32
+
33
+ In a repo with a `package.json`, add adhere as a dev dependency with its
34
+ manager, so the repo pins the version the team and CI run. From then on,
35
+ wherever this skill says `adhere`, run it the manager's way, as in
36
+ `pnpm exec adhere lint`.
37
+
38
+ With pnpm, at a workspace root, add `--ignore-workspace-root-check`.
39
+
40
+ In a repo without a `package.json`, use the `adhere` on PATH (`adhere
41
+ --version`), or **Ask** before installing it globally with the manager the
42
+ user has, such as `bun add --global @drkmttr/adhere`.
43
+
44
+ Validate and lint need a TypeSafe AI API key: `TYPESAFE_API_KEY`, or one saved
45
+ by `adhere login`, which the user runs; never ask for the key in chat or print
46
+ it.
47
+
48
+ ## 2. Find candidates
49
+
50
+ Gather candidates from three sources. Keep a note, for each, of where it came
51
+ from and the evidence.
52
+
53
+ **The repo.** Read, noting every "we always" and "never":
54
+
55
+ - `AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md`, convention sections of the
56
+ README, `docs/`, ADRs.
57
+ - Lint and type configuration, to learn what is already enforced exactly.
58
+ - Review feedback, when `gh` works: `gh pr list --state merged --limit 20`,
59
+ then `gh pr view <n> --comments` on a few. Repeated review comments are
60
+ unwritten rules.
61
+ - The code: the module others copy, the canonical service, error type, and
62
+ test.
63
+
64
+ Keep only conventions that need judgment: about intent, shape, or placement.
65
+ Drop any a regex, an import check, or the type checker could flag every time,
66
+ and note where it belongs instead. Drop any a preset below already covers.
67
+
68
+ **Presets.** Match the stack: `typescript` for TypeScript, `react` for React,
69
+ `security` for code that handles secrets, SQL, shell commands, or paths,
70
+ `effect` for Effect, `alchemy` for alchemy stacks. Read their rules under
71
+ `presets/` in the installed package or at
72
+ https://github.com/darkmatter/adhere/tree/main/presets, to describe them and
73
+ to avoid drafting duplicates.
74
+
75
+ **A shared repo.** **Ask** whether the organization keeps adhere rules in a
76
+ repo. If it does, `adhere list org/repo` prints its rules with their
77
+ descriptions.
78
+
79
+ ## 3. Draft the repo's rules
80
+
81
+ Write each repo candidate as a rule file, worded as the adhere skill says: one
82
+ sentence with "must" and "never" (or "should" and "should not"), one `must`
83
+ block of real code from the repo, and one `never` block, ideally a violation
84
+ from the repo or its history.
85
+
86
+ Write the drafts outside the repo, in a scratch directory such as
87
+ `$(mktemp -d)/<topic>/<slug>/RULE.md`, so nothing lands in `.adhere/` before the
88
+ user picks it.
89
+
90
+ ## 4. Preview and choose
91
+
92
+ Show one numbered list of every candidate, grouped by source:
93
+
94
+ ```text
95
+ From this repo
96
+ 1. errors/tagged-errors Domain errors must be Schema.TaggedError classes, never thrown Error.
97
+ Evidence: AGENTS.md "Errors"; src/billing/errors.ts
98
+ 2. services/config-service Business logic must read config through ConfigService, never Config.* directly.
99
+ Evidence: review comments on #412, #398
100
+ Presets
101
+ 3. typescript 11 rules: runtime checks at boundaries, no swallowed errors, ...
102
+ 4. security 4 rules: no secrets in output, parameterized SQL, ...
103
+ From acme/standards
104
+ 5. data/brand-ports A port must be a branded, range-checked integer, never a bare number.
105
+ Dropped as a linter's job
106
+ - "no default exports": eslint import/no-default-export
107
+ ```
108
+
109
+ For a preset, offer the whole preset or its topics. Show a draft's full text
110
+ when the user asks for it.
111
+
112
+ **Ask** which to import: all, some by number, or none, and whether to change
113
+ the wording of any. Revise drafts the user wants changed and show them again.
114
+
115
+ ## 5. Import the chosen rules
116
+
117
+ - Without a config, run `adhere init`. It also writes two example rules under
118
+ `.adhere/rules/style/`; delete them unless the user chose them.
119
+ - Copy each chosen draft to `.adhere/rules/<topic>/<slug>/RULE.md`.
120
+ - Add chosen presets, or topics, to `presets` in `.adhere/config.ts`.
121
+ - Install chosen shared rules: `adhere install org/repo/<id>` for each, or
122
+ `adhere install org/repo` for all of them. They land in `.adhere/rules/org/repo/`.
123
+ - Add `exclude` globs for generated or vendored code the default skips miss.
124
+
125
+ ## 6. Validate
126
+
127
+ ```sh
128
+ adhere validate
129
+ ```
130
+
131
+ It reports rules worded against the tips, rules a linter could check, and
132
+ contradictions between rules that apply to the same files. Fix the wording
133
+ yourself. For a linter-checkable rule or a contradiction, **Ask** the user
134
+ which to keep, and apply it. Run it again until it reports no contradictions.
135
+
136
+ ## 7. The first lint
137
+
138
+ ```sh
139
+ adhere lint --limit 0
140
+ ```
141
+
142
+ This shows the plan and judges nothing: files, rules, requests, and the
143
+ estimated cost. Tell the user those numbers. On a large repo, offer to start
144
+ with a subtree, such as `--filter 'src/**'`, or a `--limit`. **Ask** before
145
+ sending, then run it:
146
+
147
+ ```sh
148
+ adhere lint --yes
149
+ ```
150
+
151
+ Walk the user through the report:
152
+
153
+ 1. How to read a finding: the rule id, the description, its confidence and
154
+ context scores, the underlined line, and the warning when the file may not
155
+ show enough.
156
+ 2. Findings grouped by rule, the highest probabilities first. Check a few from
157
+ each rule against the code, and say which look real and which do not.
158
+ 3. For each noisy rule, propose one change from the adhere skill's "Tuning a
159
+ rule": narrow the wording, scope it, raise its threshold, make it a
160
+ warning, or turn it off. **Ask**, apply what the user accepts, and run
161
+ lint again. Only the edited rules are judged again.
162
+
163
+ CI fails on any error finding, so lint must exit 0 before step 8. **Ask** how
164
+ to get there: fix the real findings now with the adhere-fix skill
165
+ (`adhere skill fix`), make a rule a warning until its findings are fixed,
166
+ or suppress false findings with `adhere-ignore` comments that give the reason.
167
+
168
+ Commit `.adhere/` with the cache, so the team and CI reuse these judgments.
169
+ Offer to add `.adhere/cache/** linguist-generated -diff` to `.gitattributes`.
170
+
171
+ ## 8. Add adhere to CI
172
+
173
+ **Ask** before writing a workflow and before touching the repo's secrets.
174
+
175
+ If the repo's CI already installs its dependencies, add adhere as a job or a
176
+ step there, with the same setup, so it runs the pinned adhere the way the
177
+ rest of CI runs its tools. Otherwise, for GitHub Actions, add
178
+ `.github/workflows/adhere.yaml`:
179
+
180
+ ```yaml
181
+ name: adhere
182
+
183
+ on:
184
+ pull_request:
185
+ push:
186
+ branches: [main]
187
+
188
+ jobs:
189
+ lint:
190
+ runs-on: ubuntu-latest
191
+ steps:
192
+ - uses: actions/checkout@v4
193
+ # Set up the repo's package manager and install, from the table below.
194
+ - run: pnpm exec adhere lint --yes # the manager's way of running adhere
195
+ env:
196
+ TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
197
+ ```
198
+
199
+ | Manager | Setup steps | Install |
200
+ | ------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------ |
201
+ | bun | `uses: oven-sh/setup-bun@v2` | `bun install --frozen-lockfile` |
202
+ | pnpm | `uses: pnpm/action-setup@v4`, then `uses: actions/setup-node@v4` with `cache: pnpm` | `pnpm install --frozen-lockfile` |
203
+ | yarn | `uses: actions/setup-node@v4` with `cache: yarn`, then `run: corepack enable` | `yarn install --immutable`, or `--frozen-lockfile` on Yarn 1 |
204
+ | npm | `uses: actions/setup-node@v4` with `cache: npm` | `npm ci` |
205
+
206
+ Pin the versions the repo already uses: bun's `bun-version`, Node's
207
+ `node-version` from `.nvmrc` or `engines`, and pnpm's from `packageManager`,
208
+ which `pnpm/action-setup` reads. In a repo without a `package.json`, set up
209
+ the manager the user has and run adhere at the version they ran locally, as
210
+ in `bunx @drkmttr/adhere@0.11.0 lint --yes`.
211
+
212
+ Add `--deny-warnings` if the user wants warnings to fail the job too. Match
213
+ the branch to the repo's default branch. For another CI system, write the
214
+ same job: check out, install, run adhere's `lint --yes` with the key from a
215
+ secret.
216
+
217
+ The user adds the key as a secret: `gh secret set TYPESAFE_API_KEY`, which
218
+ prompts for it, or in the repo's settings. Pull requests from forks get no
219
+ secrets, so lint fails there on any file not in the cache.
220
+
221
+ Where lint gates merges, anyone who can commit can also commit a cache file
222
+ saying their code passes. To lint a pull request against the cache as merged,
223
+ fetch the base branch and restore its cache before linting:
224
+
225
+ ```yaml
226
+ - uses: actions/checkout@v4
227
+ with:
228
+ fetch-depth: 0
229
+ - if: github.event_name == 'pull_request'
230
+ run: rm -rf .adhere/cache && git checkout origin/${{ github.base_ref }} -- .adhere/cache
231
+ ```
232
+
233
+ This needs the cache committed on the base branch first.
234
+
235
+ ## 9. Report
236
+
237
+ Tell the user:
238
+
239
+ - the rules imported, by source, and where they are;
240
+ - the presets and shared repo named in the config or installed;
241
+ - the candidates dropped as a linter's job, with where each belongs;
242
+ - the last lint's findings, and what was fixed, made a warning, or suppressed;
243
+ - the CI workflow and whether the secret is set;
244
+ - what is left for them: pushing, setting the secret, fixing remaining findings.
package/src/cli.ts CHANGED
@@ -43,14 +43,15 @@ import {
43
43
  Stdio,
44
44
  Stream,
45
45
  } from "effect";
46
- import { Argument, Command, Flag, Prompt } from "effect/unstable/cli";
47
- import { FetchHttpClient } from "effect/unstable/http";
46
+ import { Argument, Command, Flag, Prompt } from "effect/cli";
47
+ import { FetchHttpClient } from "effect/http";
48
48
  // A Bun text import (https://bun.sh/docs/bundler/loaders#text): the file's
49
49
  // contents become a string at bundle time, so the compiled binary carries the
50
50
  // skill without an --asset flag. `skills/` sits beside `src/`, outside the `#`
51
51
  // import map, hence the relative path.
52
52
  import skill from "../skills/adhere/SKILL.md" with { type: "text" };
53
53
  import fixSkill from "../skills/adhere-fix/SKILL.md" with { type: "text" };
54
+ import setupSkill from "../skills/adhere-setup/SKILL.md" with { type: "text" };
54
55
 
55
56
  class FindingsReported extends Schema.TaggedError<FindingsReported>()("FindingsReported", {
56
57
  count: Schema.Finite,
@@ -75,7 +76,7 @@ const presetsIn = (values: ReadonlyArray<string>): ReadonlyArray<string> =>
75
76
  const isPresetName = (name: string): name is PresetName =>
76
77
  (presetNames as ReadonlyArray<string>).includes(name);
77
78
 
78
- const preset = Flag.string("preset").pipe(
79
+ const preset = Flag.String("preset").pipe(
79
80
  Flag.atLeast(0),
80
81
  Flag.filterMap(
81
82
  (values): Option.Option<ReadonlyArray<PresetName>> => {
@@ -93,21 +94,21 @@ const preset = Flag.string("preset").pipe(
93
94
  ),
94
95
  );
95
96
 
96
- const threshold = Flag.float("threshold").pipe(
97
+ const threshold = Flag.Finite("threshold").pipe(
97
98
  Flag.optional,
98
99
  Flag.withDescription(
99
100
  "Report a rule when Jev's probability is above this value, 0 to 1. Replaces the config's threshold; per-rule thresholds still apply.",
100
101
  ),
101
102
  );
102
103
 
103
- const sufficiencyThreshold = Flag.float("sufficiency-threshold").pipe(
104
+ const sufficiencyThreshold = Flag.Finite("sufficiency-threshold").pipe(
104
105
  Flag.optional,
105
106
  Flag.withDescription(
106
107
  "Warn on a finding when Jev's probability that the file shows enough to decide it is below this value, 0 to 1. Replaces the config's sufficiencyThreshold; the default is 0.6.",
107
108
  ),
108
109
  );
109
110
 
110
- const limit = Flag.integer("limit").pipe(
111
+ const limit = Flag.Int("limit").pipe(
111
112
  Flag.filter(
112
113
  (checks) => checks >= 0,
113
114
  (checks) => `--limit is a number of checks, 0 or more, not ${checks}.`,
@@ -118,7 +119,7 @@ const limit = Flag.integer("limit").pipe(
118
119
  ),
119
120
  );
120
121
 
121
- const rpm = Flag.integer("rpm").pipe(
122
+ const rpm = Flag.Int("rpm").pipe(
122
123
  Flag.filter(
123
124
  (requests) => requests > 0,
124
125
  (requests) => `--rpm is a number of requests a minute, more than 0, not ${requests}.`,
@@ -127,19 +128,19 @@ const rpm = Flag.integer("rpm").pipe(
127
128
  Flag.withDescription("Send at most this many requests to Jev a minute, evenly spaced."),
128
129
  );
129
130
 
130
- const filter = Flag.string("filter").pipe(
131
+ const filter = Flag.String("filter").pipe(
131
132
  Flag.atLeast(0),
132
133
  Flag.withDescription(
133
134
  "Read only files whose path from the working directory matches this glob, such as 'src/**' or '**/*.service.ts'. Repeat it for more; a pattern starting with ! leaves out what it matches.",
134
135
  ),
135
136
  );
136
137
 
137
- const denyWarnings = Flag.boolean("deny-warnings").pipe(
138
+ const denyWarnings = Flag.Boolean("deny-warnings").pipe(
138
139
  Flag.withDefault(false),
139
140
  Flag.withDescription("Fail the run on warnings as well as errors."),
140
141
  );
141
142
 
142
- const yes = Flag.boolean("yes").pipe(
143
+ const yes = Flag.Boolean("yes").pipe(
143
144
  Flag.withAlias("y"),
144
145
  Flag.withDefault(false),
145
146
  Flag.withDescription(
@@ -212,7 +213,7 @@ const statusLayer = Layer.effect(Status)(
212
213
  }),
213
214
  );
214
215
 
215
- const force = Flag.boolean("force").pipe(
216
+ const force = Flag.Boolean("force").pipe(
216
217
  Flag.withDefault(false),
217
218
  Flag.withDescription("Overwrite existing scaffold files."),
218
219
  );
@@ -239,7 +240,7 @@ export const lintCommand = Command.make(
239
240
  const interactive = (yield* stdio.stdinIsTerminal) && (yield* stdio.stdoutIsTerminal);
240
241
  if (plan.requests > 0 && interactive && !input.yes) {
241
242
  const send = yield* Prompt.run(
242
- Prompt.confirm({ message: sendQuestion(plan), initial: true }),
243
+ Prompt.Confirm({ message: sendQuestion(plan), initial: true }),
243
244
  );
244
245
  if (!send) {
245
246
  yield* toStderr("Nothing sent.\n");
@@ -256,7 +257,14 @@ export const lintCommand = Command.make(
256
257
  const color = yield* stdio.stdoutIsTerminal;
257
258
  const path = yield* Path.Path;
258
259
  // One write: separate Console.log calls have interleaved out of order here.
259
- yield* Console.log(render(result, { color, root: path.resolve() }).join("\n"));
260
+ yield* Console.log(
261
+ render(result, {
262
+ color,
263
+ root: path.resolve(),
264
+ threshold: plan.threshold,
265
+ sufficiencyThreshold: plan.sufficiencyThreshold,
266
+ }).join("\n"),
267
+ );
260
268
  // Only errors fail the run, unless --deny-warnings: a warning is for a nit or a noisy rule.
261
269
  const failed = failing(result.findings, input.denyWarnings);
262
270
  if (failed.length > 0) {
@@ -265,7 +273,7 @@ export const lintCommand = Command.make(
265
273
  }),
266
274
  ).pipe(
267
275
  Command.withDescription(
268
- "Audit the working directory against the rules in .adhere/ or a preset, judged by Jev.",
276
+ "Audit the working directory against the rules in .adhere/rules/ or a preset, judged by Jev.",
269
277
  ),
270
278
  Command.provide((input) =>
271
279
  auditLayer(
@@ -335,14 +343,14 @@ const installLine = (install: Install): string => {
335
343
  }
336
344
  };
337
345
 
338
- const shared = Flag.string("shared").pipe(
346
+ const shared = Flag.String("shared").pipe(
339
347
  Flag.filter(
340
348
  (repo) => /^[\w.-]+\/[\w.-]+$/.test(repo),
341
349
  (repo) => `--shared names a GitHub repo as org/repo, not ${repo}.`,
342
350
  ),
343
351
  Flag.optional,
344
352
  Flag.withDescription(
345
- "Scaffold a repo of rules other repos copy with adhere install, named org/repo on GitHub: example rules, a README, a CI workflow that runs adhere validate, and an alchemy.run.ts that creates the repo and its TYPESAFE_API_KEY secret.",
353
+ "Scaffold a repo of rules other repos copy with adhere install, named org/repo on GitHub: example rules, a README, a CI workflow that runs adhere validate, and an alchemy.run.ts that creates the repo, public unless you make it private when asked, and its TYPESAFE_API_KEY secret.",
346
354
  ),
347
355
  );
348
356
 
@@ -350,8 +358,16 @@ const shared = Flag.string("shared").pipe(
350
358
  export const initCommand = Command.make("init", { force, shared }, (input) =>
351
359
  Effect.gen(function* () {
352
360
  const path = yield* Path.Path;
361
+ const stdio = yield* Stdio.Stdio;
362
+ // A shared repo is public unless the user, asked at a terminal, makes it private.
363
+ const interactive = (yield* stdio.stdinIsTerminal) && (yield* stdio.stdoutIsTerminal);
364
+ const makePrivate =
365
+ Option.isSome(input.shared) && interactive
366
+ ? yield* Prompt.run(Prompt.Confirm({ message: "Make the repo private?", initial: false }))
367
+ : false;
353
368
  const result = yield* initProject(path.resolve(), {
354
369
  force: input.force,
370
+ visibility: makePrivate ? "private" : "public",
355
371
  ...Option.match(input.shared, { onNone: () => ({}), onSome: (repo) => ({ shared: repo }) }),
356
372
  });
357
373
  const lines = [
@@ -367,17 +383,17 @@ export const initCommand = Command.make("init", { force, shared }, (input) =>
367
383
  ),
368
384
  );
369
385
 
370
- const source = Argument.string("source").pipe(
386
+ const source = Argument.String("source").pipe(
371
387
  Argument.filterMap(
372
388
  (spec) => Option.fromNullishOr(parseSource(spec)),
373
389
  (spec) => `org/repo, then optionally a topic or rule and #ref, not ${spec}`,
374
390
  ),
375
391
  Argument.withDescription(
376
- "A GitHub repo with rules in .adhere/, as org/repo, optionally followed by a topic or a rule, as in org/repo/data/brand-ports, and by #branch or #tag.",
392
+ "A GitHub repo with rules in .adhere/rules/, as org/repo, optionally followed by a topic or a rule, as in org/repo/data/brand-ports, and by #branch or #tag.",
377
393
  ),
378
394
  );
379
395
 
380
- /** `adhere list org/repo`: the rules another repo's `.adhere/` holds. */
396
+ /** `adhere list org/repo`: the rules another repo's `.adhere/rules/` holds. */
381
397
  export const listCommand = Command.make("list", { source }, (input) =>
382
398
  Effect.gen(function* () {
383
399
  const rules = yield* listRules(input.source);
@@ -388,18 +404,18 @@ export const listCommand = Command.make("list", { source }, (input) =>
388
404
  }),
389
405
  ).pipe(
390
406
  Command.withDescription(
391
- "List the rules in a GitHub repo's .adhere/, each with its description, for adhere install. Clones with git, so a private repo needs git's credentials for it.",
407
+ "List the rules in a GitHub repo's .adhere/rules/, each with its description, for adhere install. Clones with git, so a private repo needs git's credentials for it.",
392
408
  ),
393
409
  );
394
410
 
395
- /** `adhere install org/repo`: another repo's rules, copied into this one's `.adhere/`. */
411
+ /** `adhere install org/repo`: another repo's rules, copied into this one's `.adhere/rules/`. */
396
412
  export const installCommand = Command.make(
397
413
  "install",
398
414
  {
399
415
  source,
400
- force: Flag.boolean("force").pipe(
416
+ force: Flag.Boolean("force").pipe(
401
417
  Flag.withDefault(false),
402
- Flag.withDescription("Overwrite rule files already in .adhere/."),
418
+ Flag.withDescription("Replace rules already in .adhere/rules/, directory and all."),
403
419
  ),
404
420
  },
405
421
  (input) =>
@@ -415,7 +431,7 @@ export const installCommand = Command.make(
415
431
  }),
416
432
  ).pipe(
417
433
  Command.withDescription(
418
- "Copy the rules in a GitHub repo's .adhere/, or one topic or rule of them, into this repo's .adhere/<org>/<repo>/. They are this repo's own rules from then on, to edit or keep in step by hand. Existing files are skipped unless --force.",
434
+ "Copy the rules in a GitHub repo's .adhere/rules/, or one topic or rule of them, each its whole directory, into this repo's .adhere/rules/<org>/<repo>/. They are this repo's own rules from then on, to edit or keep in step by hand, and a copied RULE.ts runs whenever lint does. A rule already there is skipped unless --force.",
419
435
  ),
420
436
  );
421
437
 
@@ -426,7 +442,7 @@ export const installCommand = Command.make(
426
442
  const readApiKey = Effect.fn("login.readApiKey")(function* () {
427
443
  const stdio = yield* Stdio.Stdio;
428
444
  const typed: string = (yield* stdio.stdinIsTerminal)
429
- ? Redacted.value(yield* Prompt.run(Prompt.password({ message: "TypeSafe AI API key" })))
445
+ ? Redacted.value(yield* Prompt.run(Prompt.Password({ message: "TypeSafe AI API key" })))
430
446
  : yield* stdio.stdin.pipe(
431
447
  Stream.decodeText(),
432
448
  Stream.mkString,
@@ -477,27 +493,27 @@ export const logoutCommand = Command.make("logout", {}, () =>
477
493
  );
478
494
 
479
495
  /** The agent skills the binary carries, by the name `adhere skill` takes. */
480
- const skills = { rules: skill, fix: fixSkill } as const;
496
+ const skills = { docs: skill, setup: setupSkill, fix: fixSkill } as const;
481
497
 
482
- const skillName = Argument.choice("skill", ["rules", "fix"] as const).pipe(
483
- Argument.withDefault("rules" as const),
498
+ const skillName = Argument.Literals("skill", ["docs", "setup", "fix"] as const).pipe(
499
+ Argument.withDefault("docs" as const),
484
500
  Argument.withDescription(
485
- "rules, the default, to gather a repo's conventions into rules and configure adhere; fix, to verify and fix the findings lint reports.",
501
+ "docs, the default, the reference for rules, the config, and reports; setup, to set adhere up in a repo with the user, through its first lint and CI; fix, to verify and fix the findings lint reports.",
486
502
  ),
487
503
  );
488
504
 
489
- /** `adhere skill [rules|fix]`: an agent skill, as shipped in the binary. */
505
+ /** `adhere skill [docs|setup|fix]`: an agent skill, as shipped in the binary. */
490
506
  export const skillCommand = Command.make("skill", { name: skillName }, ({ name }) =>
491
507
  Console.log(skills[name].trimEnd()),
492
508
  ).pipe(
493
509
  Command.withDescription(
494
- "Print an agent skill: by default the one that gathers a repo's conventions into rules and configures adhere, or with fix, the one that verifies and fixes lint's findings. Pipe it into .agents/skills/<name>/SKILL.md or hand it to an agent.",
510
+ "Print an agent skill: by default adhere's reference, for writing rules, configuring adhere, and reading reports; with setup, the one that sets adhere up in a repo with the user, from choosing rules through the first lint to CI; with fix, the one that verifies and fixes lint's findings. Pipe it into .agents/skills/<name>/SKILL.md or hand it to an agent.",
495
511
  ),
496
512
  );
497
513
 
498
514
  /** The whole CLI. Bare `adhere` prints its help. */
499
515
  export const cli = Command.make("adhere").pipe(
500
- Command.withDescription("Lint a repository against the rules in .adhere/, judged by Jev."),
516
+ Command.withDescription("Lint a repository against the rules in .adhere/rules/, judged by Jev."),
501
517
  Command.withExamples([
502
518
  { command: "adhere init", description: "Scaffold .adhere/config.ts and two example rules" },
503
519
  {
@@ -508,7 +524,7 @@ export const cli = Command.make("adhere").pipe(
508
524
  { command: "adhere lint", description: "Audit the working directory" },
509
525
  {
510
526
  command: "adhere install org/repo",
511
- description: "Copy another repo's .adhere/ rules into this one",
527
+ description: "Copy another repo's .adhere/rules/ into this one",
512
528
  },
513
529
  {
514
530
  command: "adhere lint --preset effect",
package/src/config.ts CHANGED
@@ -4,6 +4,34 @@ import { Effect, Schema, SchemaTransformation } from "effect";
4
4
 
5
5
  export type RuleId = string;
6
6
 
7
+ /** The state a request to Jev carries: the file's code as numbered sections, which every question reads. */
8
+ export interface JevState {
9
+ readonly code: Readonly<Record<string, string>>;
10
+ }
11
+
12
+ /** The file a request is about, as a rule's hook gets it: its absolute path, and its text as written. */
13
+ export interface JudgedFile {
14
+ readonly path: string;
15
+ readonly contents: string;
16
+ }
17
+
18
+ /**
19
+ * Bun's API, typed when the project has Bun's types. Read off `globalThis`,
20
+ * so this package's source type-checks in a project without them.
21
+ */
22
+ type BunApi = typeof globalThis extends { Bun: infer B } ? B : unknown;
23
+
24
+ /**
25
+ * A rule's hook on what Jev reads for it, run right before each request for
26
+ * the rule goes out: what it returns is spread over the request's state.
27
+ * Nothing it returns is checked, so it can replace `code` itself.
28
+ */
29
+ export type AppendState = (
30
+ state: JevState,
31
+ file: JudgedFile,
32
+ bun: BunApi,
33
+ ) => Readonly<Record<string, unknown>> | Promise<Readonly<Record<string, unknown>>>;
34
+
7
35
  /**
8
36
  * A rule's examples, named in RFC 2119's words, which Jev reads as written. A
9
37
  * requirement shows code that `must` be written and code that must `never`
@@ -39,6 +67,24 @@ const RuleFields = Schema.Struct({
39
67
  * that breaks the rule is as it describes, and a yes to any drops the finding.
40
68
  */
41
69
  excludeIf: Schema.optionalKey(Schema.Array(Schema.String)),
70
+ /**
71
+ * Whether Jev reads, beside the code, the workspace's packages as
72
+ * `workspacePackages`: each one's name with its directory, so the rule can
73
+ * tell an import of the repository's own package from an installed one's.
74
+ * The rule's questions go in requests of their own, and a change to the
75
+ * packages judges it again.
76
+ */
77
+ includeWorkspacePackages: Schema.optionalKey(Schema.Boolean),
78
+ /**
79
+ * A hook on the state Jev reads for the rule, in a rule written in
80
+ * TypeScript (see `AppendState`). The rule's questions go in requests of
81
+ * their own, so only they read what it adds.
82
+ */
83
+ appendState: Schema.optionalKey(
84
+ Schema.declare((value: unknown): value is AppendState => typeof value === "function", {
85
+ expected: "a function",
86
+ }),
87
+ ),
42
88
  });
43
89
 
44
90
  /** Before 0.7, a rule's examples were `reference` and `avoid`: read as `must` and `never`. */
@@ -137,8 +183,9 @@ export const AdhereConfig = Schema.Struct({
137
183
  Schema.withDecodingDefaultKey(Effect.succeed([])),
138
184
  ),
139
185
  /**
140
- * A record of rules, or a directory path relative to the config file.
141
- * Unset: `.adhere/` when that directory exists, otherwise no rules.
186
+ * A record of rules, or a directory path relative to the config file,
187
+ * which holds its rules as `.adhere/rules/` does. Unset: the rules in
188
+ * `.adhere/rules/`, and in any nested `.adhere/rules/`.
142
189
  */
143
190
  rules: Schema.optionalKey(Schema.Union([Schema.Record(Schema.String, Rule), Schema.String])),
144
191
  /**
@@ -161,8 +208,10 @@ export const AdhereConfig = Schema.Struct({
161
208
  includeComments: Schema.optionalKey(Schema.Boolean),
162
209
  });
163
210
 
164
- /** Where a repo keeps its rule files by default. The cache lives under it. */
211
+ /** Where a repo keeps what adhere reads and writes: its config, its rules, and the cache. */
165
212
  export const ADHERE_DIRECTORY = ".adhere";
213
+ /** Where a repo keeps its rules by default, one to a directory: `<id>/RULE.md` or `<id>/RULE.ts`. */
214
+ export const RULES_DIRECTORY = `${ADHERE_DIRECTORY}/rules`;
166
215
  export const CACHE_DIRECTORY = `${ADHERE_DIRECTORY}/cache`;
167
216
  /** Where a config may live, relative to the working directory. Exactly one may exist. */
168
217
  export const CONFIG_FILES = [
@@ -210,6 +259,12 @@ export type Config = typeof AdhereConfig.Encoded;
210
259
 
211
260
  export const defineConfig = (config: Config) => config;
212
261
 
262
+ /**
263
+ * Types a rule written in TypeScript, which a `.adhere/` file default-exports,
264
+ * and returns it as it is. It takes what a config's inline rule does.
265
+ */
266
+ export const defineRule = (rule: typeof Rule.Encoded) => rule;
267
+
213
268
  /** A built-in rule set. Same shape as a config, minus `presets`. */
214
269
  export interface Preset {
215
270
  /**
@@ -249,6 +304,12 @@ export interface ResolvedConfig {
249
304
  readonly exclude?: ReadonlyArray<string>;
250
305
  /** Whether Jev reads every comment; unset, only the `@adhere` notes. */
251
306
  readonly includeComments?: boolean;
307
+ /**
308
+ * The workspace's packages, each name with its directory from the working
309
+ * directory, read from its manifests on every run, when a rule asks for
310
+ * them with `includeWorkspacePackages`.
311
+ */
312
+ readonly workspacePackages?: Readonly<Record<string, string>>;
252
313
  }
253
314
 
254
315
  /** Command-line values that apply on top of the config file. */
package/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export {
2
2
  type AdhereConfig,
3
+ type AppendState,
3
4
  type Config,
4
5
  ConfigUnavailable,
5
6
  defineConfig,
7
+ defineRule,
6
8
  type Preset,
7
9
  type Rule,
8
10
  type RuleId,