@drkmttr/adhere 0.11.0 → 0.13.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 +184 -37
- package/package.json +9 -9
- package/presets/effect/basics/{instrument-with-pipe.md → instrument-with-pipe/RULE.md} +1 -1
- package/presets/effect/config/{business-logic-depends-on-config-service.md → business-logic-depends-on-config-service/RULE.md} +3 -3
- package/presets/effect/config/{secrets-are-redacted.md → secrets-are-redacted/RULE.md} +3 -3
- package/presets/effect/config/{validate-with-schema.md → validate-with-schema/RULE.md} +4 -4
- package/skills/adhere/SKILL.md +206 -118
- package/skills/adhere-fix/SKILL.md +9 -8
- package/skills/adhere-setup/SKILL.md +244 -0
- package/src/cli.ts +114 -25
- package/src/config.ts +50 -3
- package/src/index.ts +2 -0
- package/src/init.ts +193 -27
- package/src/install.ts +158 -0
- package/src/main.ts +1 -1
- package/src/rules.ts +135 -56
- package/src/services/AdhereConfig.ts +2 -2
- package/src/services/Credentials.ts +4 -4
- package/src/services/Jev.http.ts +76 -10
- package/src/services/Jev.ts +39 -10
- package/src/workflows/audit.ts +23 -12
- package/src/workflows/format.ts +172 -35
- /package/presets/alchemy/apis/{match-remote-errors-by-tag.md → match-remote-errors-by-tag/RULE.md} +0 -0
- /package/presets/alchemy/apis/{schemas-at-trust-boundaries.md → schemas-at-trust-boundaries/RULE.md} +0 -0
- /package/presets/alchemy/data/{commit-migrations-then-deploy.md → commit-migrations-then-deploy/RULE.md} +0 -0
- /package/presets/alchemy/data/{hyperdrive-direct-origin.md → hyperdrive-direct-origin/RULE.md} +0 -0
- /package/presets/alchemy/data/{migrations-use-direct-url.md → migrations-use-direct-url/RULE.md} +0 -0
- /package/presets/alchemy/data/{never-disable-tls-verification.md → never-disable-tls-verification/RULE.md} +0 -0
- /package/presets/alchemy/data/{private-endpoint-at-runtime.md → private-endpoint-at-runtime/RULE.md} +0 -0
- /package/presets/alchemy/data/{single-migration-owner.md → single-migration-owner/RULE.md} +0 -0
- /package/presets/alchemy/durable/{idempotent-event-handlers.md → idempotent-event-handlers/RULE.md} +0 -0
- /package/presets/alchemy/durable/{io-inside-steps.md → io-inside-steps/RULE.md} +0 -0
- /package/presets/alchemy/durable/{no-self-triggering-bucket-writes.md → no-self-triggering-bucket-writes/RULE.md} +0 -0
- /package/presets/alchemy/durable/{stable-step-names.md → stable-step-names/RULE.md} +0 -0
- /package/presets/alchemy/durable/{webhook-state-is-truth.md → webhook-state-is-truth/RULE.md} +0 -0
- /package/presets/alchemy/providers/{diff-guards-unresolved-props.md → diff-guards-unresolved-props/RULE.md} +0 -0
- /package/presets/alchemy/providers/{idempotent-delete.md → idempotent-delete/RULE.md} +0 -0
- /package/presets/alchemy/providers/{lazy-layer-construction.md → lazy-layer-construction/RULE.md} +0 -0
- /package/presets/alchemy/providers/{reconcile-from-observed-state.md → reconcile-from-observed-state/RULE.md} +0 -0
- /package/presets/alchemy/resources/{anchor-paths-with-import-meta.md → anchor-paths-with-import-meta/RULE.md} +0 -0
- /package/presets/alchemy/resources/{idempotent-actions-and-seeds.md → idempotent-actions-and-seeds/RULE.md} +0 -0
- /package/presets/alchemy/resources/{previews-reference-shared-resources.md → previews-reference-shared-resources/RULE.md} +0 -0
- /package/presets/alchemy/resources/{protect-irreplaceable-data.md → protect-irreplaceable-data/RULE.md} +0 -0
- /package/presets/alchemy/resources/{remote-state-for-ci.md → remote-state-for-ci/RULE.md} +0 -0
- /package/presets/alchemy/resources/{stable-logical-ids.md → stable-logical-ids/RULE.md} +0 -0
- /package/presets/alchemy/resources/{stage-unique-physical-names.md → stage-unique-physical-names/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{bind-tag-not-implementation.md → bind-tag-not-implementation/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{deterministic-config-defaults.md → deterministic-config-defaults/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{disposables-in-request-scope.md → disposables-in-request-scope/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{lambda-no-post-response-work.md → lambda-no-post-response-work/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{no-per-request-work-at-construction.md → no-per-request-work-at-construction/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{resolve-config-and-bindings-at-construction.md → resolve-config-and-bindings-at-construction/RULE.md} +0 -0
- /package/presets/alchemy/secrets/{let-alchemy-mint-infra-secrets.md → let-alchemy-mint-infra-secrets/RULE.md} +0 -0
- /package/presets/alchemy/secrets/{none-in-browser-env.md → none-in-browser-env/RULE.md} +0 -0
- /package/presets/alchemy/secrets/{redacted-until-use.md → redacted-until-use/RULE.md} +0 -0
- /package/presets/alchemy/secrets/{unwrap-before-compare-or-send.md → unwrap-before-compare-or-send/RULE.md} +0 -0
- /package/presets/alchemy/security/{authorize-public-endpoints.md → authorize-public-endpoints/RULE.md} +0 -0
- /package/presets/alchemy/security/{github-secret-not-variable.md → github-secret-not-variable/RULE.md} +0 -0
- /package/presets/alchemy/security/{identity-from-verified-session.md → identity-from-verified-session/RULE.md} +0 -0
- /package/presets/alchemy/security/{keep-auth-protections-on.md → keep-auth-protections-on/RULE.md} +0 -0
- /package/presets/alchemy/security/{least-privilege-bindings.md → least-privilege-bindings/RULE.md} +0 -0
- /package/presets/alchemy/security/{least-privilege-ci-credentials.md → least-privilege-ci-credentials/RULE.md} +0 -0
- /package/presets/alchemy/security/{lookup-failure-is-not-anonymous.md → lookup-failure-is-not-anonymous/RULE.md} +0 -0
- /package/presets/effect/basics/{gen-for-sequencing.md → gen-for-sequencing/RULE.md} +0 -0
- /package/presets/effect/config/{tests-provide-values-directly.md → tests-provide-values-directly/RULE.md} +0 -0
- /package/presets/effect/data/{brand-meaningful-primitives.md → brand-meaningful-primitives/RULE.md} +0 -0
- /package/presets/effect/data/{variants-are-tagged-unions.md → variants-are-tagged-unions/RULE.md} +0 -0
- /package/presets/effect/errors/{catch-defects-at-boundaries-only.md → catch-defects-at-boundaries-only/RULE.md} +0 -0
- /package/presets/effect/errors/{defects-for-bugs.md → defects-for-bugs/RULE.md} +0 -0
- /package/presets/effect/errors/{wrap-external-errors.md → wrap-external-errors/RULE.md} +0 -0
- /package/presets/effect/services/{dependencies-through-layers.md → dependencies-through-layers/RULE.md} +0 -0
- /package/presets/effect/services/{fresh-layer-per-test.md → fresh-layer-per-test/RULE.md} +0 -0
- /package/presets/effect/services/{memoize-parameterized-layers.md → memoize-parameterized-layers/RULE.md} +0 -0
- /package/presets/effect/services/{no-mutable-state.md → no-mutable-state/RULE.md} +0 -0
- /package/presets/effect/services/{operations-have-no-requirements.md → operations-have-no-requirements/RULE.md} +0 -0
- /package/presets/effect/services/{test-layers-are-in-memory.md → test-layers-are-in-memory/RULE.md} +0 -0
- /package/presets/effect/testing/{test-clock-for-time.md → test-clock-for-time/RULE.md} +0 -0
- /package/presets/react/effects/{clean-up-what-they-start.md → clean-up-what-they-start/RULE.md} +0 -0
- /package/presets/react/effects/{event-logic-in-handlers.md → event-logic-in-handlers/RULE.md} +0 -0
- /package/presets/react/effects/{external-stores-use-sync-external-store.md → external-stores-use-sync-external-store/RULE.md} +0 -0
- /package/presets/react/effects/{fetches-ignore-stale-responses.md → fetches-ignore-stale-responses/RULE.md} +0 -0
- /package/presets/react/effects/{notify-parents-in-handlers.md → notify-parents-in-handlers/RULE.md} +0 -0
- /package/presets/react/server/{no-private-data-to-client.md → no-private-data-to-client/RULE.md} +0 -0
- /package/presets/react/server/{server-functions-authorize-and-validate.md → server-functions-authorize-and-validate/RULE.md} +0 -0
- /package/presets/react/state/{no-contradictory-state.md → no-contradictory-state/RULE.md} +0 -0
- /package/presets/react/state/{no-props-copied-into-state.md → no-props-copied-into-state/RULE.md} +0 -0
- /package/presets/react/state/{store-ids-not-copies.md → store-ids-not-copies/RULE.md} +0 -0
- /package/presets/security/{constant-time-secret-compare.md → constant-time-secret-compare/RULE.md} +0 -0
- /package/presets/security/{no-secrets-in-output.md → no-secrets-in-output/RULE.md} +0 -0
- /package/presets/security/{no-untrusted-input-in-commands-or-paths.md → no-untrusted-input-in-commands-or-paths/RULE.md} +0 -0
- /package/presets/security/{parameterized-queries.md → parameterized-queries/RULE.md} +0 -0
- /package/presets/typescript/async/{concurrent-independent-work.md → concurrent-independent-work/RULE.md} +0 -0
- /package/presets/typescript/async/{network-calls-have-timeouts.md → network-calls-have-timeouts/RULE.md} +0 -0
- /package/presets/typescript/design/{no-argument-mutation.md → no-argument-mutation/RULE.md} +0 -0
- /package/presets/typescript/errors/{no-swallowed-errors.md → no-swallowed-errors/RULE.md} +0 -0
- /package/presets/typescript/errors/{release-on-every-path.md → release-on-every-path/RULE.md} +0 -0
- /package/presets/typescript/testing/{assert-outcomes.md → assert-outcomes/RULE.md} +0 -0
- /package/presets/typescript/testing/{independent-tests.md → independent-tests/RULE.md} +0 -0
- /package/presets/typescript/types/{assertions-only-when-proven.md → assertions-only-when-proven/RULE.md} +0 -0
- /package/presets/typescript/types/{derive-dont-restate.md → derive-dont-restate/RULE.md} +0 -0
- /package/presets/typescript/types/{unrepresentable-invalid-states.md → unrepresentable-invalid-states/RULE.md} +0 -0
- /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
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import type { Flags } from "#config.ts";
|
|
24
24
|
import { findContradictions, formatContradictions } from "#contradictions.ts";
|
|
25
25
|
import { type Install, initProject } from "#init.ts";
|
|
26
|
+
import { installRules, listRules, parseSource } from "#install.ts";
|
|
26
27
|
import { formatLinterCheck, tallyProjectRules } from "#mechanical.ts";
|
|
27
28
|
import { type PresetName, presetNames } from "#presets.ts";
|
|
28
29
|
import { globalRuleSet } from "#rules.ts";
|
|
@@ -42,14 +43,15 @@ import {
|
|
|
42
43
|
Stdio,
|
|
43
44
|
Stream,
|
|
44
45
|
} from "effect";
|
|
45
|
-
import { Argument, Command, Flag, Prompt } from "effect/
|
|
46
|
-
import { FetchHttpClient } from "effect/
|
|
46
|
+
import { Argument, Command, Flag, Prompt } from "effect/cli";
|
|
47
|
+
import { FetchHttpClient } from "effect/http";
|
|
47
48
|
// A Bun text import (https://bun.sh/docs/bundler/loaders#text): the file's
|
|
48
49
|
// contents become a string at bundle time, so the compiled binary carries the
|
|
49
50
|
// skill without an --asset flag. `skills/` sits beside `src/`, outside the `#`
|
|
50
51
|
// import map, hence the relative path.
|
|
51
52
|
import skill from "../skills/adhere/SKILL.md" with { type: "text" };
|
|
52
53
|
import fixSkill from "../skills/adhere-fix/SKILL.md" with { type: "text" };
|
|
54
|
+
import setupSkill from "../skills/adhere-setup/SKILL.md" with { type: "text" };
|
|
53
55
|
|
|
54
56
|
class FindingsReported extends Schema.TaggedError<FindingsReported>()("FindingsReported", {
|
|
55
57
|
count: Schema.Finite,
|
|
@@ -74,7 +76,7 @@ const presetsIn = (values: ReadonlyArray<string>): ReadonlyArray<string> =>
|
|
|
74
76
|
const isPresetName = (name: string): name is PresetName =>
|
|
75
77
|
(presetNames as ReadonlyArray<string>).includes(name);
|
|
76
78
|
|
|
77
|
-
const preset = Flag.
|
|
79
|
+
const preset = Flag.String("preset").pipe(
|
|
78
80
|
Flag.atLeast(0),
|
|
79
81
|
Flag.filterMap(
|
|
80
82
|
(values): Option.Option<ReadonlyArray<PresetName>> => {
|
|
@@ -92,21 +94,21 @@ const preset = Flag.string("preset").pipe(
|
|
|
92
94
|
),
|
|
93
95
|
);
|
|
94
96
|
|
|
95
|
-
const threshold = Flag.
|
|
97
|
+
const threshold = Flag.Finite("threshold").pipe(
|
|
96
98
|
Flag.optional,
|
|
97
99
|
Flag.withDescription(
|
|
98
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.",
|
|
99
101
|
),
|
|
100
102
|
);
|
|
101
103
|
|
|
102
|
-
const sufficiencyThreshold = Flag.
|
|
104
|
+
const sufficiencyThreshold = Flag.Finite("sufficiency-threshold").pipe(
|
|
103
105
|
Flag.optional,
|
|
104
106
|
Flag.withDescription(
|
|
105
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.",
|
|
106
108
|
),
|
|
107
109
|
);
|
|
108
110
|
|
|
109
|
-
const limit = Flag.
|
|
111
|
+
const limit = Flag.Int("limit").pipe(
|
|
110
112
|
Flag.filter(
|
|
111
113
|
(checks) => checks >= 0,
|
|
112
114
|
(checks) => `--limit is a number of checks, 0 or more, not ${checks}.`,
|
|
@@ -117,7 +119,7 @@ const limit = Flag.integer("limit").pipe(
|
|
|
117
119
|
),
|
|
118
120
|
);
|
|
119
121
|
|
|
120
|
-
const rpm = Flag.
|
|
122
|
+
const rpm = Flag.Int("rpm").pipe(
|
|
121
123
|
Flag.filter(
|
|
122
124
|
(requests) => requests > 0,
|
|
123
125
|
(requests) => `--rpm is a number of requests a minute, more than 0, not ${requests}.`,
|
|
@@ -126,19 +128,19 @@ const rpm = Flag.integer("rpm").pipe(
|
|
|
126
128
|
Flag.withDescription("Send at most this many requests to Jev a minute, evenly spaced."),
|
|
127
129
|
);
|
|
128
130
|
|
|
129
|
-
const filter = Flag.
|
|
131
|
+
const filter = Flag.String("filter").pipe(
|
|
130
132
|
Flag.atLeast(0),
|
|
131
133
|
Flag.withDescription(
|
|
132
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.",
|
|
133
135
|
),
|
|
134
136
|
);
|
|
135
137
|
|
|
136
|
-
const denyWarnings = Flag.
|
|
138
|
+
const denyWarnings = Flag.Boolean("deny-warnings").pipe(
|
|
137
139
|
Flag.withDefault(false),
|
|
138
140
|
Flag.withDescription("Fail the run on warnings as well as errors."),
|
|
139
141
|
);
|
|
140
142
|
|
|
141
|
-
const yes = Flag.
|
|
143
|
+
const yes = Flag.Boolean("yes").pipe(
|
|
142
144
|
Flag.withAlias("y"),
|
|
143
145
|
Flag.withDefault(false),
|
|
144
146
|
Flag.withDescription(
|
|
@@ -211,7 +213,7 @@ const statusLayer = Layer.effect(Status)(
|
|
|
211
213
|
}),
|
|
212
214
|
);
|
|
213
215
|
|
|
214
|
-
const force = Flag.
|
|
216
|
+
const force = Flag.Boolean("force").pipe(
|
|
215
217
|
Flag.withDefault(false),
|
|
216
218
|
Flag.withDescription("Overwrite existing scaffold files."),
|
|
217
219
|
);
|
|
@@ -238,7 +240,7 @@ export const lintCommand = Command.make(
|
|
|
238
240
|
const interactive = (yield* stdio.stdinIsTerminal) && (yield* stdio.stdoutIsTerminal);
|
|
239
241
|
if (plan.requests > 0 && interactive && !input.yes) {
|
|
240
242
|
const send = yield* Prompt.run(
|
|
241
|
-
Prompt.
|
|
243
|
+
Prompt.Confirm({ message: sendQuestion(plan), initial: true }),
|
|
242
244
|
);
|
|
243
245
|
if (!send) {
|
|
244
246
|
yield* toStderr("Nothing sent.\n");
|
|
@@ -255,7 +257,14 @@ export const lintCommand = Command.make(
|
|
|
255
257
|
const color = yield* stdio.stdoutIsTerminal;
|
|
256
258
|
const path = yield* Path.Path;
|
|
257
259
|
// One write: separate Console.log calls have interleaved out of order here.
|
|
258
|
-
yield* Console.log(
|
|
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
|
+
);
|
|
259
268
|
// Only errors fail the run, unless --deny-warnings: a warning is for a nit or a noisy rule.
|
|
260
269
|
const failed = failing(result.findings, input.denyWarnings);
|
|
261
270
|
if (failed.length > 0) {
|
|
@@ -264,7 +273,7 @@ export const lintCommand = Command.make(
|
|
|
264
273
|
}),
|
|
265
274
|
).pipe(
|
|
266
275
|
Command.withDescription(
|
|
267
|
-
"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.",
|
|
268
277
|
),
|
|
269
278
|
Command.provide((input) =>
|
|
270
279
|
auditLayer(
|
|
@@ -326,7 +335,7 @@ export const validateCommand = Command.make("validate", { preset }, () =>
|
|
|
326
335
|
const installLine = (install: Install): string => {
|
|
327
336
|
switch (install.status) {
|
|
328
337
|
case "installed":
|
|
329
|
-
return `installed:
|
|
338
|
+
return `installed: ${install.what} with ${install.packageManager}`;
|
|
330
339
|
case "listed":
|
|
331
340
|
return "installed: none, @drkmttr/adhere is already in package.json";
|
|
332
341
|
case "no-package-json":
|
|
@@ -334,11 +343,33 @@ const installLine = (install: Install): string => {
|
|
|
334
343
|
}
|
|
335
344
|
};
|
|
336
345
|
|
|
346
|
+
const shared = Flag.String("shared").pipe(
|
|
347
|
+
Flag.filter(
|
|
348
|
+
(repo) => /^[\w.-]+\/[\w.-]+$/.test(repo),
|
|
349
|
+
(repo) => `--shared names a GitHub repo as org/repo, not ${repo}.`,
|
|
350
|
+
),
|
|
351
|
+
Flag.optional,
|
|
352
|
+
Flag.withDescription(
|
|
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.",
|
|
354
|
+
),
|
|
355
|
+
);
|
|
356
|
+
|
|
337
357
|
/** `adhere init`: the scaffold. */
|
|
338
|
-
export const initCommand = Command.make("init", { force }, (input) =>
|
|
358
|
+
export const initCommand = Command.make("init", { force, shared }, (input) =>
|
|
339
359
|
Effect.gen(function* () {
|
|
340
360
|
const path = yield* Path.Path;
|
|
341
|
-
const
|
|
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;
|
|
368
|
+
const result = yield* initProject(path.resolve(), {
|
|
369
|
+
force: input.force,
|
|
370
|
+
visibility: makePrivate ? "private" : "public",
|
|
371
|
+
...Option.match(input.shared, { onNone: () => ({}), onSome: (repo) => ({ shared: repo }) }),
|
|
372
|
+
});
|
|
342
373
|
const lines = [
|
|
343
374
|
result.created.length > 0 ? `created: ${result.created.join(", ")}` : "created: none",
|
|
344
375
|
result.skipped.length > 0 ? `skipped: ${result.skipped.join(", ")}` : "skipped: none",
|
|
@@ -352,6 +383,58 @@ export const initCommand = Command.make("init", { force }, (input) =>
|
|
|
352
383
|
),
|
|
353
384
|
);
|
|
354
385
|
|
|
386
|
+
const source = Argument.String("source").pipe(
|
|
387
|
+
Argument.filterMap(
|
|
388
|
+
(spec) => Option.fromNullishOr(parseSource(spec)),
|
|
389
|
+
(spec) => `org/repo, then optionally a topic or rule and #ref, not ${spec}`,
|
|
390
|
+
),
|
|
391
|
+
Argument.withDescription(
|
|
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.",
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
/** `adhere list org/repo`: the rules another repo's `.adhere/rules/` holds. */
|
|
397
|
+
export const listCommand = Command.make("list", { source }, (input) =>
|
|
398
|
+
Effect.gen(function* () {
|
|
399
|
+
const rules = yield* listRules(input.source);
|
|
400
|
+
const width = Math.max(...rules.map(({ id }) => id.length));
|
|
401
|
+
yield* Console.log(
|
|
402
|
+
rules.map(({ id, description }) => `${id.padEnd(width)} ${description}`).join("\n"),
|
|
403
|
+
);
|
|
404
|
+
}),
|
|
405
|
+
).pipe(
|
|
406
|
+
Command.withDescription(
|
|
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.",
|
|
408
|
+
),
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
/** `adhere install org/repo`: another repo's rules, copied into this one's `.adhere/rules/`. */
|
|
412
|
+
export const installCommand = Command.make(
|
|
413
|
+
"install",
|
|
414
|
+
{
|
|
415
|
+
source,
|
|
416
|
+
force: Flag.Boolean("force").pipe(
|
|
417
|
+
Flag.withDefault(false),
|
|
418
|
+
Flag.withDescription("Replace rules already in .adhere/rules/, directory and all."),
|
|
419
|
+
),
|
|
420
|
+
},
|
|
421
|
+
(input) =>
|
|
422
|
+
Effect.gen(function* () {
|
|
423
|
+
const path = yield* Path.Path;
|
|
424
|
+
const result = yield* installRules(path.resolve(), input.source, { force: input.force });
|
|
425
|
+
yield* Console.log(
|
|
426
|
+
[
|
|
427
|
+
result.created.length > 0 ? `created: ${result.created.join(", ")}` : "created: none",
|
|
428
|
+
result.skipped.length > 0 ? `skipped: ${result.skipped.join(", ")}` : "skipped: none",
|
|
429
|
+
].join("\n"),
|
|
430
|
+
);
|
|
431
|
+
}),
|
|
432
|
+
).pipe(
|
|
433
|
+
Command.withDescription(
|
|
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.",
|
|
435
|
+
),
|
|
436
|
+
);
|
|
437
|
+
|
|
355
438
|
/**
|
|
356
439
|
* The key typed at a masked prompt, or piped in: `adhere login < key.txt`.
|
|
357
440
|
* Whitespace around it, like the newline a pipe ends with, is dropped.
|
|
@@ -359,7 +442,7 @@ export const initCommand = Command.make("init", { force }, (input) =>
|
|
|
359
442
|
const readApiKey = Effect.fn("login.readApiKey")(function* () {
|
|
360
443
|
const stdio = yield* Stdio.Stdio;
|
|
361
444
|
const typed: string = (yield* stdio.stdinIsTerminal)
|
|
362
|
-
? Redacted.value(yield* Prompt.run(Prompt.
|
|
445
|
+
? Redacted.value(yield* Prompt.run(Prompt.Password({ message: "TypeSafe AI API key" })))
|
|
363
446
|
: yield* stdio.stdin.pipe(
|
|
364
447
|
Stream.decodeText(),
|
|
365
448
|
Stream.mkString,
|
|
@@ -410,27 +493,27 @@ export const logoutCommand = Command.make("logout", {}, () =>
|
|
|
410
493
|
);
|
|
411
494
|
|
|
412
495
|
/** The agent skills the binary carries, by the name `adhere skill` takes. */
|
|
413
|
-
const skills = {
|
|
496
|
+
const skills = { docs: skill, setup: setupSkill, fix: fixSkill } as const;
|
|
414
497
|
|
|
415
|
-
const skillName = Argument.
|
|
416
|
-
Argument.withDefault("
|
|
498
|
+
const skillName = Argument.Literals("skill", ["docs", "setup", "fix"] as const).pipe(
|
|
499
|
+
Argument.withDefault("docs" as const),
|
|
417
500
|
Argument.withDescription(
|
|
418
|
-
"
|
|
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.",
|
|
419
502
|
),
|
|
420
503
|
);
|
|
421
504
|
|
|
422
|
-
/** `adhere skill [
|
|
505
|
+
/** `adhere skill [docs|setup|fix]`: an agent skill, as shipped in the binary. */
|
|
423
506
|
export const skillCommand = Command.make("skill", { name: skillName }, ({ name }) =>
|
|
424
507
|
Console.log(skills[name].trimEnd()),
|
|
425
508
|
).pipe(
|
|
426
509
|
Command.withDescription(
|
|
427
|
-
"Print an agent skill: by default the one that
|
|
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.",
|
|
428
511
|
),
|
|
429
512
|
);
|
|
430
513
|
|
|
431
514
|
/** The whole CLI. Bare `adhere` prints its help. */
|
|
432
515
|
export const cli = Command.make("adhere").pipe(
|
|
433
|
-
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."),
|
|
434
517
|
Command.withExamples([
|
|
435
518
|
{ command: "adhere init", description: "Scaffold .adhere/config.ts and two example rules" },
|
|
436
519
|
{
|
|
@@ -439,6 +522,10 @@ export const cli = Command.make("adhere").pipe(
|
|
|
439
522
|
},
|
|
440
523
|
{ command: "adhere login", description: "Save your TypeSafe AI API key for later runs" },
|
|
441
524
|
{ command: "adhere lint", description: "Audit the working directory" },
|
|
525
|
+
{
|
|
526
|
+
command: "adhere install org/repo",
|
|
527
|
+
description: "Copy another repo's .adhere/rules/ into this one",
|
|
528
|
+
},
|
|
442
529
|
{
|
|
443
530
|
command: "adhere lint --preset effect",
|
|
444
531
|
description: "Audit against the built-in Effect rules",
|
|
@@ -448,6 +535,8 @@ export const cli = Command.make("adhere").pipe(
|
|
|
448
535
|
lintCommand,
|
|
449
536
|
validateCommand,
|
|
450
537
|
initCommand,
|
|
538
|
+
listCommand,
|
|
539
|
+
installCommand,
|
|
451
540
|
loginCommand,
|
|
452
541
|
logoutCommand,
|
|
453
542
|
skillCommand,
|
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,16 @@ 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
|
+
* A hook on the state Jev reads for the rule, in a rule written in
|
|
72
|
+
* TypeScript (see `AppendState`). The rule's questions go in requests of
|
|
73
|
+
* their own, so only they read what it adds.
|
|
74
|
+
*/
|
|
75
|
+
appendState: Schema.optionalKey(
|
|
76
|
+
Schema.declare((value: unknown): value is AppendState => typeof value === "function", {
|
|
77
|
+
expected: "a function",
|
|
78
|
+
}),
|
|
79
|
+
),
|
|
42
80
|
});
|
|
43
81
|
|
|
44
82
|
/** Before 0.7, a rule's examples were `reference` and `avoid`: read as `must` and `never`. */
|
|
@@ -137,8 +175,9 @@ export const AdhereConfig = Schema.Struct({
|
|
|
137
175
|
Schema.withDecodingDefaultKey(Effect.succeed([])),
|
|
138
176
|
),
|
|
139
177
|
/**
|
|
140
|
-
* A record of rules, or a directory path relative to the config file
|
|
141
|
-
*
|
|
178
|
+
* A record of rules, or a directory path relative to the config file,
|
|
179
|
+
* which holds its rules as `.adhere/rules/` does. Unset: the rules in
|
|
180
|
+
* `.adhere/rules/`, and in any nested `.adhere/rules/`.
|
|
142
181
|
*/
|
|
143
182
|
rules: Schema.optionalKey(Schema.Union([Schema.Record(Schema.String, Rule), Schema.String])),
|
|
144
183
|
/**
|
|
@@ -161,8 +200,10 @@ export const AdhereConfig = Schema.Struct({
|
|
|
161
200
|
includeComments: Schema.optionalKey(Schema.Boolean),
|
|
162
201
|
});
|
|
163
202
|
|
|
164
|
-
/** Where a repo keeps
|
|
203
|
+
/** Where a repo keeps what adhere reads and writes: its config, its rules, and the cache. */
|
|
165
204
|
export const ADHERE_DIRECTORY = ".adhere";
|
|
205
|
+
/** Where a repo keeps its rules by default, one to a directory: `<id>/RULE.md` or `<id>/RULE.ts`. */
|
|
206
|
+
export const RULES_DIRECTORY = `${ADHERE_DIRECTORY}/rules`;
|
|
166
207
|
export const CACHE_DIRECTORY = `${ADHERE_DIRECTORY}/cache`;
|
|
167
208
|
/** Where a config may live, relative to the working directory. Exactly one may exist. */
|
|
168
209
|
export const CONFIG_FILES = [
|
|
@@ -210,6 +251,12 @@ export type Config = typeof AdhereConfig.Encoded;
|
|
|
210
251
|
|
|
211
252
|
export const defineConfig = (config: Config) => config;
|
|
212
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Types a rule written in TypeScript, which a `.adhere/` file default-exports,
|
|
256
|
+
* and returns it as it is. It takes what a config's inline rule does.
|
|
257
|
+
*/
|
|
258
|
+
export const defineRule = (rule: typeof Rule.Encoded) => rule;
|
|
259
|
+
|
|
213
260
|
/** A built-in rule set. Same shape as a config, minus `presets`. */
|
|
214
261
|
export interface Preset {
|
|
215
262
|
/**
|