@drkmttr/adhere 0.12.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 +149 -51
- 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 +49 -33
- package/src/config.ts +50 -3
- package/src/index.ts +2 -0
- package/src/init.ts +36 -25
- package/src/install.ts +37 -26
- 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
|
@@ -43,14 +43,15 @@ import {
|
|
|
43
43
|
Stdio,
|
|
44
44
|
Stream,
|
|
45
45
|
} from "effect";
|
|
46
|
-
import { Argument, Command, Flag, Prompt } from "effect/
|
|
47
|
-
import { FetchHttpClient } from "effect/
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
416
|
+
force: Flag.Boolean("force").pipe(
|
|
401
417
|
Flag.withDefault(false),
|
|
402
|
-
Flag.withDescription("
|
|
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.
|
|
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.
|
|
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 = {
|
|
496
|
+
const skills = { docs: skill, setup: setupSkill, fix: fixSkill } as const;
|
|
481
497
|
|
|
482
|
-
const skillName = Argument.
|
|
483
|
-
Argument.withDefault("
|
|
498
|
+
const skillName = Argument.Literals("skill", ["docs", "setup", "fix"] as const).pipe(
|
|
499
|
+
Argument.withDefault("docs" as const),
|
|
484
500
|
Argument.withDescription(
|
|
485
|
-
"
|
|
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 [
|
|
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
|
|
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/
|
|
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,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
|
/**
|