@drkmttr/adhere 0.6.0 → 0.7.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 +75 -39
- package/package.json +7 -7
- package/presets/effect/basics/external-calls-are-resilient.md +6 -2
- package/presets/effect/basics/fn-for-named-effects.md +10 -2
- package/presets/effect/basics/gen-for-sequencing.md +10 -2
- package/presets/effect/basics/instrument-with-pipe.md +9 -2
- package/presets/effect/cli/handlers-delegate.md +13 -2
- package/presets/effect/config/business-logic-depends-on-config-service.md +9 -2
- package/presets/effect/config/secrets-are-redacted.md +6 -2
- package/presets/effect/config/tests-provide-values-directly.md +8 -2
- package/presets/effect/config/validate-with-schema.md +9 -2
- package/presets/effect/data/brand-meaningful-primitives.md +11 -2
- package/presets/effect/data/decode-json-with-schema.md +6 -2
- package/presets/effect/data/records-are-schema-classes.md +9 -2
- package/presets/effect/data/variants-are-tagged-unions.md +9 -2
- package/presets/effect/errors/catch-defects-at-boundaries-only.md +6 -2
- package/presets/effect/errors/defects-for-bugs.md +13 -2
- package/presets/effect/errors/domain-errors-are-tagged.md +6 -2
- package/presets/effect/errors/wrap-external-errors.md +7 -2
- package/presets/effect/platform/os-access-through-platform-services.md +8 -2
- package/presets/effect/services/fresh-layer-per-test.md +9 -2
- package/presets/effect/services/memoize-parameterized-layers.md +9 -2
- package/presets/effect/services/methods-have-no-requirements.md +9 -2
- package/presets/effect/services/no-mutable-state.md +9 -2
- package/presets/effect/services/provide-at-entry.md +7 -2
- package/presets/effect/services/test-layers-are-in-memory.md +12 -2
- package/presets/effect/testing/test-clock-for-time.md +11 -2
- package/presets/effect/testing/test-random.md +9 -2
- package/skills/adhere/SKILL.md +24 -17
- package/src/cli.ts +2 -2
- package/src/config.ts +67 -13
- package/src/init.ts +10 -8
- package/src/markdown.ts +59 -25
- package/src/services/Jev.http.ts +36 -11
- package/src/services/Jev.ts +75 -36
- package/src/workflows/audit.ts +34 -21
- package/src/workflows/format.ts +7 -3
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Each it.effect
|
|
2
|
+
description: Each it.effect must provide its own layer. it.layer must be used only to share an expensive resource across a suite, never a cheap one.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
```ts
|
|
5
|
+
```ts must
|
|
6
6
|
it.effect("starts at zero", () =>
|
|
7
7
|
Effect.gen(function* () {
|
|
8
8
|
const counter = yield* Counter;
|
|
@@ -18,3 +18,10 @@ it.effect("increments without leaking", () =>
|
|
|
18
18
|
}).pipe(Effect.provide(Counter.layer)),
|
|
19
19
|
);
|
|
20
20
|
```
|
|
21
|
+
|
|
22
|
+
```ts never
|
|
23
|
+
it.layer(Counter.layer)("Counter", (it) => {
|
|
24
|
+
it.effect("starts at zero", () => Effect.gen(function* () { /* ... */ }));
|
|
25
|
+
it.effect("increments", () => Effect.gen(function* () { /* ... */ }));
|
|
26
|
+
});
|
|
27
|
+
```
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: The result of a parameterized layer constructor
|
|
2
|
+
description: The result of a parameterized layer constructor must be stored in a module constant before it is used in more than one place, never constructed again for each use.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
```ts
|
|
5
|
+
```ts must
|
|
6
6
|
const postgresLayer = Postgres.layer({ url: "postgres://localhost/mydb", poolSize: 10 });
|
|
7
7
|
|
|
8
8
|
const goodAppLayer = Layer.merge(
|
|
@@ -10,3 +10,10 @@ const goodAppLayer = Layer.merge(
|
|
|
10
10
|
OrderRepo.layer.pipe(Layer.provide(postgresLayer)),
|
|
11
11
|
);
|
|
12
12
|
```
|
|
13
|
+
|
|
14
|
+
```ts never
|
|
15
|
+
const Live = Layer.merge(
|
|
16
|
+
Sessions.layer.pipe(Layer.provide(Redis.layer({ host: "localhost" }))),
|
|
17
|
+
RateLimits.layer.pipe(Layer.provide(Redis.layer({ host: "localhost" }))),
|
|
18
|
+
);
|
|
19
|
+
```
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Service method signatures have no requirements. Dependencies
|
|
2
|
+
description: Service method signatures must have no requirements. Dependencies must be acquired inside the layer, never declared on the method.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
```ts
|
|
5
|
+
```ts must
|
|
6
6
|
class Database extends Context.Service<
|
|
7
7
|
Database,
|
|
8
8
|
{
|
|
@@ -11,3 +11,10 @@ class Database extends Context.Service<
|
|
|
11
11
|
}
|
|
12
12
|
>()("@app/Database") {}
|
|
13
13
|
```
|
|
14
|
+
|
|
15
|
+
```ts never
|
|
16
|
+
class Orders extends Context.Service<
|
|
17
|
+
Orders,
|
|
18
|
+
{ readonly save: (order: Order) => Effect.Effect<void, SaveError, Database> }
|
|
19
|
+
>()("@app/Orders") {}
|
|
20
|
+
```
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: A service
|
|
2
|
+
description: A service must expose only readonly members, never mutable state.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
```ts
|
|
5
|
+
```ts must
|
|
6
6
|
class Logger extends Context.Service<
|
|
7
7
|
Logger,
|
|
8
8
|
{
|
|
@@ -10,3 +10,10 @@ class Logger extends Context.Service<
|
|
|
10
10
|
}
|
|
11
11
|
>()("@app/Logger") {}
|
|
12
12
|
```
|
|
13
|
+
|
|
14
|
+
```ts never
|
|
15
|
+
class Counter extends Context.Service<
|
|
16
|
+
Counter,
|
|
17
|
+
{ count: number; readonly increment: () => Effect.Effect<void> }
|
|
18
|
+
>()("@app/Counter") {}
|
|
19
|
+
```
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Layers
|
|
2
|
+
description: Layers must be provided once at the program entry. A module that is not an entry point must never call Effect.provide.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
```ts
|
|
5
|
+
```ts must
|
|
6
6
|
const appLayer = userServiceLayer.pipe(
|
|
7
7
|
Layer.provideMerge(databaseLayer),
|
|
8
8
|
Layer.provideMerge(loggerLayer),
|
|
@@ -16,3 +16,8 @@ const program = Effect.gen(function* () {
|
|
|
16
16
|
|
|
17
17
|
const main = program.pipe(Effect.provide(appLayer));
|
|
18
18
|
```
|
|
19
|
+
|
|
20
|
+
```ts never
|
|
21
|
+
export const getUser = (id: string) =>
|
|
22
|
+
UserRepo.find(id).pipe(Effect.provide(UserRepo.layer));
|
|
23
|
+
```
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: A test implementation of a service
|
|
2
|
+
description: A test implementation of a service must be built with Layer.sync or Layer.succeed over in-memory state, never over a real database, network, or file.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
```ts
|
|
5
|
+
```ts must
|
|
6
6
|
static readonly testLayer = Layer.sync(Cache, () => {
|
|
7
7
|
const store = new Map<string, string>()
|
|
8
8
|
|
|
@@ -12,3 +12,13 @@ static readonly testLayer = Layer.sync(Cache, () => {
|
|
|
12
12
|
return { get, set }
|
|
13
13
|
})
|
|
14
14
|
```
|
|
15
|
+
|
|
16
|
+
```ts never
|
|
17
|
+
const EmailTest = Layer.effect(
|
|
18
|
+
Email,
|
|
19
|
+
Effect.gen(function* () {
|
|
20
|
+
const smtp = yield* SmtpClient;
|
|
21
|
+
return Email.of({ send: (message) => smtp.deliver(message) });
|
|
22
|
+
}),
|
|
23
|
+
).pipe(Layer.provide(SmtpClient.layer({ host: "localhost", port: 1025 })));
|
|
24
|
+
```
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: A test that depends on time
|
|
2
|
+
description: A test that depends on time must use TestClock. Real sleeps and it.live must be used only when real time is required.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
```ts
|
|
5
|
+
```ts must
|
|
6
6
|
it.effect("time-based test", () =>
|
|
7
7
|
Effect.gen(function* () {
|
|
8
8
|
const fiber = yield* Effect.delay(Effect.succeed("done"), "10 seconds").pipe(Effect.forkChild);
|
|
@@ -12,3 +12,12 @@ it.effect("time-based test", () =>
|
|
|
12
12
|
}),
|
|
13
13
|
);
|
|
14
14
|
```
|
|
15
|
+
|
|
16
|
+
```ts never
|
|
17
|
+
it.live("retries after a delay", () =>
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
yield* Effect.sleep("2 seconds");
|
|
20
|
+
expect(yield* attempts).toBe(2);
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
```
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: A test that depends on randomness
|
|
2
|
+
description: A test that depends on randomness must control it with a fixed seed through Random.withSeed, never with Math.random or an unseeded Random.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
```ts
|
|
5
|
+
```ts must
|
|
6
6
|
const program = Effect.gen(function* () {
|
|
7
7
|
const value1 = yield* Random.next;
|
|
8
8
|
const value2 = yield* Random.next;
|
|
@@ -13,3 +13,10 @@ await Effect.runPromise(
|
|
|
13
13
|
Effect.all([program.pipe(Random.withSeed("my-seed")), program.pipe(Random.withSeed("my-seed"))]),
|
|
14
14
|
); // => [[0.018368576514773527, 0.4010840628128671], [0.018368576514773527, 0.4010840628128671]]
|
|
15
15
|
```
|
|
16
|
+
|
|
17
|
+
```ts never
|
|
18
|
+
it("creates unique ids", () => {
|
|
19
|
+
const id = Math.random().toString(36).slice(2);
|
|
20
|
+
expect(id).toHaveLength(11);
|
|
21
|
+
});
|
|
22
|
+
```
|
package/skills/adhere/SKILL.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: adhere
|
|
3
|
-
description: Gather a repository's non-deterministic coding conventions into adhere rules (Markdown files with a description and
|
|
3
|
+
description: Gather a repository's non-deterministic coding conventions into adhere rules (Markdown files with a description and code that must, or must never, be written, judged by Jev), configure adhere, run it, and calibrate thresholds. Use when setting up adhere in a repo, when the user asks to turn conventions, AGENTS.md guidance, ADRs, or review feedback into lint rules a normal linter cannot express, or when tuning adhere findings.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Gather adhere rules from a repo
|
|
7
7
|
|
|
8
8
|
adhere is a linter for rules a normal linter cannot check. Each rule is one
|
|
9
|
-
sentence
|
|
10
|
-
and returns a probability. Your job is to find the conventions a repo already
|
|
9
|
+
sentence in RFC 2119's words plus code that must be written and code that must
|
|
10
|
+
never be; Jev judges every source file against it and returns a probability. Your job is to find the conventions a repo already
|
|
11
11
|
has, write the ones that need judgment as rule files, and tune until the
|
|
12
12
|
report is trustworthy.
|
|
13
13
|
|
|
@@ -52,32 +52,39 @@ The path without `.md` is the rule id.
|
|
|
52
52
|
|
|
53
53
|
````md
|
|
54
54
|
---
|
|
55
|
-
description: One sentence
|
|
55
|
+
description: One sentence saying what code must be, then what it must never be.
|
|
56
56
|
threshold: 0.8
|
|
57
57
|
---
|
|
58
58
|
|
|
59
59
|
Optional prose for readers on GitHub. adhere ignores it.
|
|
60
60
|
|
|
61
|
-
```ts
|
|
61
|
+
```ts must
|
|
62
62
|
// Correct code lifted from this repo, trimmed to the pattern.
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
```ts
|
|
65
|
+
```ts never
|
|
66
66
|
// Optional: the incorrect form this rule catches, as it appeared in the repo.
|
|
67
67
|
```
|
|
68
68
|
````
|
|
69
69
|
|
|
70
70
|
Rules for the rule:
|
|
71
71
|
|
|
72
|
-
- `description` is one sentence,
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
- `description` is one sentence, specific, in the same words as the fences:
|
|
73
|
+
what code must be, and what it must never be. Jev reads the description
|
|
74
|
+
with the code, and "must" in both is one demand; vague words ("properly",
|
|
75
|
+
"correctly") give it nothing to judge.
|
|
76
|
+
- The `must` block is real code from the repo, not invented, and only correct
|
|
75
77
|
code: Jev compares files to it, so incorrect code there teaches the wrong
|
|
76
|
-
thing. Incorrect code goes in a fence tagged `
|
|
77
|
-
a violation looks like. Add one
|
|
78
|
-
ideally one found in the repo's history
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
thing. Incorrect code goes in a fence tagged `never`, which Jev reads as what
|
|
79
|
+
a violation looks like. Add one whenever violations have a recognizable
|
|
80
|
+
shape, ideally one found in the repo's history: over the effect preset, a
|
|
81
|
+
`never` block raised how well Jev told violations from compliant code under
|
|
82
|
+
every wording tried. A rule with no single correct form can be only a
|
|
83
|
+
`never` block.
|
|
84
|
+
- A convention that is a guideline rather than a requirement says "should" in
|
|
85
|
+
its description and tags its fences `should` and `should not`. A rule is one
|
|
86
|
+
or the other; it cannot mix the two.
|
|
87
|
+
- One pattern per file. Two patterns in one `must` block blur the probability.
|
|
81
88
|
- `threshold` is optional. Start without it; set it in step 5 if needed.
|
|
82
89
|
|
|
83
90
|
## 4. Configure
|
|
@@ -110,11 +117,11 @@ adhere lint --threshold 0.9 # re-reads the cache, sends nothing
|
|
|
110
117
|
|
|
111
118
|
Then, per rule, read the findings and decide:
|
|
112
119
|
|
|
113
|
-
- Many hits at 0.
|
|
120
|
+
- Many hits at 0.80 to 0.90, mostly not violations: the description is too
|
|
114
121
|
broad. Narrow it (say what is out of scope) or raise that rule's
|
|
115
122
|
`threshold`. Prefer narrowing; a threshold hides, a sentence explains.
|
|
116
|
-
- Hits on code that follows the pattern through a different API: the
|
|
117
|
-
|
|
123
|
+
- Hits on code that follows the pattern through a different API: the `must`
|
|
124
|
+
block is too specific. Use the repo's most general correct example.
|
|
118
125
|
- Zero hits: plant a deliberate violation in a scratch file, run, confirm the
|
|
119
126
|
rule fires, remove the file. A rule that cannot fire is not a rule.
|
|
120
127
|
- Hits at 0.9 and above: read them first. They are usually real.
|
package/src/cli.ts
CHANGED
|
@@ -154,7 +154,7 @@ export const lintCommand = Command.make("lint", { preset, threshold, yes }, (inp
|
|
|
154
154
|
}),
|
|
155
155
|
).pipe(
|
|
156
156
|
Command.withDescription(
|
|
157
|
-
"Audit the working directory against
|
|
157
|
+
"Audit the working directory against the rules in .adhere/ or a preset, judged by Jev.",
|
|
158
158
|
),
|
|
159
159
|
Command.provide((input) =>
|
|
160
160
|
auditLayer({
|
|
@@ -274,7 +274,7 @@ export const skillCommand = Command.make("skill", {}, () => Console.log(skill.tr
|
|
|
274
274
|
|
|
275
275
|
/** The whole CLI. Bare `adhere` prints its help. */
|
|
276
276
|
export const cli = Command.make("adhere").pipe(
|
|
277
|
-
Command.withDescription("Lint a repository against
|
|
277
|
+
Command.withDescription("Lint a repository against the rules in .adhere/, judged by Jev."),
|
|
278
278
|
Command.withExamples([
|
|
279
279
|
{ command: "adhere init", description: "Scaffold .adhere/config.ts and two example rules" },
|
|
280
280
|
{ command: "adhere validate", description: "Check the config and rules without calling Jev" },
|
package/src/config.ts
CHANGED
|
@@ -1,25 +1,79 @@
|
|
|
1
1
|
import { type PresetName, presetNames } from "#presets.ts";
|
|
2
2
|
import type { RuleSet } from "#rules.ts";
|
|
3
|
-
import { Effect, Schema } from "effect";
|
|
3
|
+
import { Effect, Schema, SchemaTransformation } from "effect";
|
|
4
4
|
|
|
5
5
|
export type RuleId = string;
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* A rule's examples, named in RFC 2119's words, which Jev reads as written. A
|
|
9
|
+
* requirement shows code that `must` be written and code that must `never`
|
|
10
|
+
* be. A guideline, which asks for less, shows code that `should` be written
|
|
11
|
+
* and code that `shouldNot` be.
|
|
12
|
+
*/
|
|
13
|
+
const RuleFields = Schema.Struct({
|
|
8
14
|
description: Schema.String,
|
|
9
|
-
|
|
15
|
+
must: Schema.optionalKey(Schema.String),
|
|
16
|
+
never: Schema.optionalKey(Schema.String),
|
|
17
|
+
should: Schema.optionalKey(Schema.String),
|
|
18
|
+
shouldNot: Schema.optionalKey(Schema.String),
|
|
19
|
+
threshold: Schema.optionalKey(Schema.Finite),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** Before 0.7, a rule's examples were `reference` and `avoid`: read as `must` and `never`. */
|
|
23
|
+
export const Rule = Schema.Struct({
|
|
24
|
+
...RuleFields.fields,
|
|
10
25
|
reference: Schema.optionalKey(Schema.String),
|
|
11
|
-
/** Incorrect code: what a violation looks like. */
|
|
12
26
|
avoid: Schema.optionalKey(Schema.String),
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
)
|
|
27
|
+
})
|
|
28
|
+
.pipe(
|
|
29
|
+
Schema.decodeTo(
|
|
30
|
+
RuleFields,
|
|
31
|
+
SchemaTransformation.transform({
|
|
32
|
+
decode: ({ reference, avoid, ...rule }) => ({
|
|
33
|
+
...rule,
|
|
34
|
+
...(rule.must === undefined && reference !== undefined ? { must: reference } : {}),
|
|
35
|
+
...(rule.never === undefined && avoid !== undefined ? { never: avoid } : {}),
|
|
36
|
+
}),
|
|
37
|
+
encode: (rule) => rule,
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
.check(
|
|
42
|
+
Schema.makeFilter((rule) => {
|
|
43
|
+
const requirement = rule.must !== undefined || rule.never !== undefined;
|
|
44
|
+
const guideline = rule.should !== undefined || rule.shouldNot !== undefined;
|
|
45
|
+
return requirement && guideline
|
|
46
|
+
? "a rule is a requirement (must, never) or a guideline (should, shouldNot), not both"
|
|
47
|
+
: requirement || guideline
|
|
48
|
+
? undefined
|
|
49
|
+
: "a rule needs an example: must, never, should, or shouldNot";
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
21
52
|
export interface Rule extends Schema.Schema.Type<typeof Rule> {}
|
|
22
53
|
|
|
54
|
+
/** One of a rule's examples, under the word it is written with. */
|
|
55
|
+
export interface Example {
|
|
56
|
+
readonly word: "must" | "never" | "should" | "should not";
|
|
57
|
+
readonly code: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A rule's code to write and code not to write, each when the rule has it. */
|
|
61
|
+
export interface Examples {
|
|
62
|
+
readonly good?: Example;
|
|
63
|
+
readonly bad?: Example;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A guideline's examples are `should` and `should not`; any other rule's, `must` and `never`. */
|
|
67
|
+
export const examplesOf = (rule: Rule): Examples => {
|
|
68
|
+
const guideline = rule.should !== undefined || rule.shouldNot !== undefined;
|
|
69
|
+
const good = guideline ? rule.should : rule.must;
|
|
70
|
+
const bad = guideline ? rule.shouldNot : rule.never;
|
|
71
|
+
return {
|
|
72
|
+
...(good === undefined ? {} : { good: { word: guideline ? "should" : "must", code: good } }),
|
|
73
|
+
...(bad === undefined ? {} : { bad: { word: guideline ? "should not" : "never", code: bad } }),
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
23
77
|
export type Rules = Readonly<Record<RuleId, Rule>>;
|
|
24
78
|
|
|
25
79
|
/** Rules inline, or a directory of Markdown rule files (see `markdown.ts`). */
|
|
@@ -86,7 +140,7 @@ export type Loaded<T extends { readonly rules?: unknown }> = Omit<T, "rules"> &
|
|
|
86
140
|
};
|
|
87
141
|
|
|
88
142
|
export const DEFAULT_MODEL = "jev-latest";
|
|
89
|
-
export const DEFAULT_THRESHOLD = 0.
|
|
143
|
+
export const DEFAULT_THRESHOLD = 0.8;
|
|
90
144
|
|
|
91
145
|
/** A config with its presets folded in and every default applied. */
|
|
92
146
|
export interface ResolvedConfig {
|
package/src/init.ts
CHANGED
|
@@ -15,39 +15,41 @@ export interface InitOptions {
|
|
|
15
15
|
const CONFIG = `import type { Config } from "@drkmttr/adhere";
|
|
16
16
|
|
|
17
17
|
export default {
|
|
18
|
-
threshold: 0.
|
|
18
|
+
threshold: 0.8,
|
|
19
19
|
} satisfies Config;
|
|
20
20
|
`;
|
|
21
21
|
|
|
22
22
|
const SMALL_FILES = `---
|
|
23
|
-
description:
|
|
23
|
+
description: A file should be small and focused, with one primary responsibility.
|
|
24
24
|
threshold: 0.8
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
A guideline, so its description and its code say should. A rule that must
|
|
28
|
+
hold says must and never instead, as name-domain-actions does. Use this rule
|
|
29
|
+
as a template for repository-specific conventions.
|
|
28
30
|
|
|
29
|
-
\`\`\`ts
|
|
31
|
+
\`\`\`ts should
|
|
30
32
|
export const parsePort = (value: string) =>
|
|
31
33
|
Port.make(Number.parseInt(value, 10));
|
|
32
34
|
\`\`\`
|
|
33
35
|
`;
|
|
34
36
|
|
|
35
37
|
const NAME_EFFECTS = `---
|
|
36
|
-
description:
|
|
38
|
+
description: A function must be named after the domain action it performs, never after a generic verb like handle.
|
|
37
39
|
threshold: 0.8
|
|
38
40
|
---
|
|
39
41
|
|
|
40
|
-
\`\`\`ts
|
|
42
|
+
\`\`\`ts must
|
|
41
43
|
export const loadCustomerProfile = (customerId: CustomerId) =>
|
|
42
44
|
Effect.gen(function* () {
|
|
43
45
|
return yield* CustomerStore.find(customerId);
|
|
44
46
|
});
|
|
45
47
|
\`\`\`
|
|
46
48
|
|
|
47
|
-
A fence tagged \`
|
|
49
|
+
A fence tagged \`never\` shows what a violation looks like. It is optional, and
|
|
48
50
|
a rule can have one without the block above.
|
|
49
51
|
|
|
50
|
-
\`\`\`ts
|
|
52
|
+
\`\`\`ts never
|
|
51
53
|
export const handle = (id: string) =>
|
|
52
54
|
Effect.gen(function* () {
|
|
53
55
|
return yield* CustomerStore.find(id);
|
package/src/markdown.ts
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
import { ConfigUnavailable, type Rule } from "#config.ts";
|
|
1
|
+
import { ConfigUnavailable, type Example, type Rule } from "#config.ts";
|
|
2
2
|
import { Effect, Schema } from "effect";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A rule as a Markdown file:
|
|
6
6
|
*
|
|
7
7
|
* ---
|
|
8
|
-
* description: One sentence
|
|
8
|
+
* description: One sentence saying what code must be, or never be.
|
|
9
9
|
* threshold: 0.8
|
|
10
10
|
* ---
|
|
11
11
|
* Any prose, rendered on GitHub and ignored by adhere.
|
|
12
12
|
*
|
|
13
|
-
* ```ts
|
|
14
|
-
* const
|
|
13
|
+
* ```ts must
|
|
14
|
+
* const written = "code that must be written this way"
|
|
15
15
|
* ```
|
|
16
16
|
*
|
|
17
|
-
* ```ts
|
|
18
|
-
* const avoided = "
|
|
17
|
+
* ```ts never
|
|
18
|
+
* const avoided = "code that must never be written this way"
|
|
19
19
|
* ```
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* fence
|
|
21
|
+
* A fence's info string names its code with RFC 2119's words: `must` and
|
|
22
|
+
* `never` for a requirement, `should` and `should not` for a guideline. The
|
|
23
|
+
* first fence under each word is that example, and a rule has either or both.
|
|
24
|
+
* An untagged fence is the code to write, and without a fence the whole body
|
|
25
|
+
* is. `avoid`, the word before 0.7, means `never`. `threshold` is optional.
|
|
24
26
|
*/
|
|
25
27
|
const FrontMatter = Schema.Struct({
|
|
26
28
|
description: Schema.String,
|
|
@@ -29,7 +31,6 @@ const FrontMatter = Schema.Struct({
|
|
|
29
31
|
|
|
30
32
|
const FRONT_MATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
31
33
|
const FENCE = /^ {0,3}(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)\n {0,3}\1[ \t]*$/gm;
|
|
32
|
-
const AVOID = "avoid";
|
|
33
34
|
|
|
34
35
|
const unquote = (value: string): string => {
|
|
35
36
|
const trimmed = value.trim();
|
|
@@ -50,17 +51,27 @@ const parseFrontMatter = (block: string): Record<string, string> => {
|
|
|
50
51
|
return fields;
|
|
51
52
|
};
|
|
52
53
|
|
|
54
|
+
type Word = Example["word"];
|
|
55
|
+
|
|
53
56
|
interface Fence {
|
|
54
|
-
|
|
57
|
+
/** The word the info string names the code with; none for an untagged fence. */
|
|
58
|
+
readonly word: Word | undefined;
|
|
55
59
|
readonly code: string;
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
/** `must not`, like `never` and the older `avoid`, names code never to write. */
|
|
63
|
+
const wordOf = (info: string): Word | undefined => {
|
|
64
|
+
const words = info.trim().toLowerCase().split(/\s+/);
|
|
65
|
+
const not = words.includes("not");
|
|
66
|
+
if (words.includes("never") || words.includes("avoid")) return "never";
|
|
67
|
+
if (words.includes("should")) return not ? "should not" : "should";
|
|
68
|
+
if (words.includes("must")) return not ? "never" : "must";
|
|
69
|
+
return undefined;
|
|
70
|
+
};
|
|
71
|
+
|
|
58
72
|
const fencesOf = (body: string): ReadonlyArray<Fence> =>
|
|
59
73
|
Array.from(body.matchAll(FENCE), (match) => ({
|
|
60
|
-
|
|
61
|
-
.trim()
|
|
62
|
-
.split(/\s+/)
|
|
63
|
-
.some((word) => word.toLowerCase() === AVOID),
|
|
74
|
+
word: wordOf(match[2] ?? ""),
|
|
64
75
|
code: (match[3] ?? "").trim(),
|
|
65
76
|
}));
|
|
66
77
|
|
|
@@ -68,16 +79,32 @@ const fencesOf = (body: string): ReadonlyArray<Fence> =>
|
|
|
68
79
|
const nonEmpty = (code: string | undefined): string | undefined =>
|
|
69
80
|
code === undefined || code.length === 0 ? undefined : code;
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
type Code = Pick<Rule, "must" | "never" | "should" | "shouldNot">;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Each example the body has, under its word. An untagged fence is the code
|
|
86
|
+
* to write: `should` in a guideline, which a `should` or `should not` fence
|
|
87
|
+
* makes the rule, and `must` otherwise.
|
|
88
|
+
*/
|
|
89
|
+
const codeOf = (body: string): Code => {
|
|
73
90
|
const fences = fencesOf(body);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
91
|
+
if (fences.length === 0) {
|
|
92
|
+
const must = nonEmpty(body.trim());
|
|
93
|
+
return must === undefined ? {} : { must };
|
|
94
|
+
}
|
|
95
|
+
const first = (word: Word | undefined) =>
|
|
96
|
+
nonEmpty(fences.find((fence) => fence.word === word)?.code);
|
|
97
|
+
const guideline = fences.some((fence) => fence.word === "should" || fence.word === "should not");
|
|
98
|
+
const untagged = first(undefined);
|
|
99
|
+
const must = first("must") ?? (guideline ? undefined : untagged);
|
|
100
|
+
const never = first("never");
|
|
101
|
+
const should = first("should") ?? (guideline ? untagged : undefined);
|
|
102
|
+
const shouldNot = first("should not");
|
|
78
103
|
return {
|
|
79
|
-
...(
|
|
80
|
-
...(
|
|
104
|
+
...(must === undefined ? {} : { must }),
|
|
105
|
+
...(never === undefined ? {} : { never }),
|
|
106
|
+
...(should === undefined ? {} : { should }),
|
|
107
|
+
...(shouldNot === undefined ? {} : { shouldNot }),
|
|
81
108
|
};
|
|
82
109
|
};
|
|
83
110
|
|
|
@@ -98,9 +125,16 @@ export const parseRuleMarkdown = (
|
|
|
98
125
|
),
|
|
99
126
|
);
|
|
100
127
|
const code = codeOf(match[2]);
|
|
101
|
-
|
|
128
|
+
const requirement = code.must !== undefined || code.never !== undefined;
|
|
129
|
+
const guideline = code.should !== undefined || code.shouldNot !== undefined;
|
|
130
|
+
if (requirement && guideline) {
|
|
131
|
+
return yield* ConfigUnavailable.make({
|
|
132
|
+
message: `${file}: a rule is a requirement (must, never) or a guideline (should, should not), not both`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (!requirement && !guideline) {
|
|
102
136
|
return yield* ConfigUnavailable.make({
|
|
103
|
-
message: `${file}: the body needs
|
|
137
|
+
message: `${file}: the body needs code, in a fence tagged must, never, should, or should not, or untagged`,
|
|
104
138
|
});
|
|
105
139
|
}
|
|
106
140
|
return { ...front, ...code };
|