@drkmttr/adhere 0.14.0 → 0.15.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 +22 -17
- package/package.json +6 -6
- package/presets/effect/config/tests-provide-values-directly/RULE.md +12 -1
- package/presets/effect/data/brand-meaningful-primitives/RULE.md +14 -2
- package/presets/effect/data/variants-are-tagged-unions/RULE.md +17 -4
- package/presets/effect/services/dependencies-through-layers/RULE.md +16 -3
- package/presets/effect/services/no-mutable-state/RULE.md +7 -1
- package/presets/effect/services/operations-have-no-requirements/RULE.md +0 -50
- package/presets/effect/services/test-layers-are-in-memory/RULE.md +0 -29
package/README.md
CHANGED
|
@@ -764,7 +764,7 @@ type UserId = typeof UserId.Type
|
|
|
764
764
|
| [`typescript`](./presets/typescript/) | 11 | any TypeScript project | Data from outside checked at runtime, invalid states unrepresentable, errors never swallowed, resources released on every path, arguments not mutated, and tests that assert outcomes and stand alone. |
|
|
765
765
|
| [`react`](./presets/react/) | 10 | components and hooks | Logic for an event in its handler rather than an effect, effects that clean up and ignore stale fetches, `useSyncExternalStore` for outside stores, state that neither copies props nor contradicts itself, and Server Functions and Server Components that guard what crosses to the client. |
|
|
766
766
|
| [`security`](./presets/security/) | 4 | any project | Secrets kept out of logs, error messages, and responses; parameterized SQL; no untrusted input in shell commands, `eval`, or file paths; and secrets compared in constant time. From OWASP's cheat sheets. |
|
|
767
|
-
| [`effect`](./presets/effect/) |
|
|
767
|
+
| [`effect`](./presets/effect/) | 16 | code written with Effect | Steps sequenced with `Effect.gen`, instrumentation attached with `.pipe`, config read through a service and validated, secrets redacted, branded primitives and tagged unions, defects kept apart from typed errors, services built by layers, and tests with their own layers and `TestClock`. |
|
|
768
768
|
| [`alchemy`](./presets/alchemy/) | 41 | code that deploys with [alchemy](https://alchemy.run) | Where Config and bindings are read, which resources keep their data, how secrets stay out of bundles and logs, authorization on public URLs, migrations, durable workflows, and custom providers. |
|
|
769
769
|
|
|
770
770
|
- Name presets in the config's `presets`, or on the command line with
|
|
@@ -850,16 +850,21 @@ type UserId = typeof UserId.Type
|
|
|
850
850
|
[effect-solutions](https://github.com/kitlangton/effect-solutions) docs and
|
|
851
851
|
the [effect/platform](https://effect.website/docs/platform/introduction/)
|
|
852
852
|
docs.
|
|
853
|
-
- **Tests.**
|
|
854
|
-
`testing/test-clock-for-time`, `services/fresh-layer-per-test`,
|
|
855
|
-
`services/test-layers-are-in-memory`, and
|
|
853
|
+
- **Tests.** Three rules say `tests: only`, so they judge tests and nothing else:
|
|
854
|
+
`testing/test-clock-for-time`, `services/fresh-layer-per-test`, and
|
|
856
855
|
`config/tests-provide-values-directly`.
|
|
857
|
-
- **Guidelines.**
|
|
858
|
-
|
|
856
|
+
- **Guidelines.** One rule says "should": a test of code that consumes config
|
|
857
|
+
provides it through a layer.
|
|
858
|
+
- **Domain types.** The data rules judge the types services exchange, return,
|
|
859
|
+
or store. A shape that mirrors a payload until it is mapped, and a view's
|
|
860
|
+
props or state, are out of their scope.
|
|
859
861
|
- **What the rules allow.** Config validation accepts `Config.mapEffect` as well
|
|
860
862
|
as `Config.schema`, and the variants rule does not rule out a `switch`.
|
|
861
863
|
- **Gone.** The rule that a command handler only parses input: the docs show
|
|
862
|
-
that pattern but do not ask for it.
|
|
864
|
+
that pattern but do not ask for it. The rule that test layers are in memory,
|
|
865
|
+
which flagged tests of real services: none of its findings labeled on the eval
|
|
866
|
+
or on darkmatter/agents was real. And the rule that service operations have no
|
|
867
|
+
requirements, which `leakingRequirements` below checks exactly.
|
|
863
868
|
- **Left to a linter.** Effect's language service,
|
|
864
869
|
[`@effect/tsgo`](https://github.com/Effect-TS/tsgo) on TypeScript 7, checks
|
|
865
870
|
seven conventions exactly, which the preset checked through 0.7. Most are off
|
|
@@ -891,8 +896,7 @@ type UserId = typeof UserId.Type
|
|
|
891
896
|
- **What the linter misses.** `leakingRequirements` sees a requirement in an
|
|
892
897
|
operation's type, but not a service built by a factory function that takes its
|
|
893
898
|
dependencies as arguments, whose types have no requirements to find. The
|
|
894
|
-
preset's `services/dependencies-through-layers` asks for that
|
|
895
|
-
`services/operations-have-no-requirements` for the style it goes with.
|
|
899
|
+
preset's `services/dependencies-through-layers` asks for that.
|
|
896
900
|
|
|
897
901
|
</details>
|
|
898
902
|
|
|
@@ -1300,14 +1304,15 @@ about three bytes a token.
|
|
|
1300
1304
|
|
|
1301
1305
|
Names and layouts from older versions:
|
|
1302
1306
|
|
|
1303
|
-
| Version | What it had
|
|
1304
|
-
| ----------------- |
|
|
1305
|
-
| before 0.7 | A rule's examples were `reference` and `avoid` in a config, and a fence could be tagged `avoid`.
|
|
1306
|
-
| through 0.7 | The `effect` preset checked seven conventions a linter checks exactly.
|
|
1307
|
-
| before 0.13 | A rule was any `*.md` file in `.adhere/`, as `.adhere/data/brand-ports.md`.
|
|
1308
|
-
| before 0.13 | `.adhere/tsconfig.json` included only `config.ts`.
|
|
1309
|
-
| 0.13.1 | The workspace's packages had a key of their own, `includeWorkspacePackages: true`.
|
|
1310
|
-
| 0.13.1 and before | Prose around a `RULE.md`'s code was ignored.
|
|
1307
|
+
| Version | What it had | What happens now |
|
|
1308
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1309
|
+
| before 0.7 | A rule's examples were `reference` and `avoid` in a config, and a fence could be tagged `avoid`. | They still read as `must` and `never`. |
|
|
1310
|
+
| through 0.7 | The `effect` preset checked seven conventions a linter checks exactly. | They are left to `@effect/tsgo`. The notes on `effect` under [Presets](#presets) say how to turn them on. |
|
|
1311
|
+
| before 0.13 | A rule was any `*.md` file in `.adhere/`, as `.adhere/data/brand-ports.md`. | A `*.md` file outside every rule's directory, but a `README.md`, refuses the run. The refusal says where each such file goes to keep its id, so `adhere-ignore` comments and `overrides` still name it. |
|
|
1312
|
+
| before 0.13 | `.adhere/tsconfig.json` included only `config.ts`. | Make its `include` `["**/*.ts"]` to give rules written in TypeScript their types. |
|
|
1313
|
+
| 0.13.1 | The workspace's packages had a key of their own, `includeWorkspacePackages: true`. | adhere no longer reads it, and a rule that still has it gets no packages. Write `reads: ["workspacePackages"]` in its place. |
|
|
1314
|
+
| 0.13.1 and before | Prose around a `RULE.md`'s code was ignored. | Jev reads it after the description, as the rule's details, so a rule with prose is judged again once. Keep prose about the rule: why it holds, or where it does not apply. |
|
|
1315
|
+
| 0.14.0 and before | The `effect` preset had `services/test-layers-are-in-memory` and `services/operations-have-no-requirements`. | They are gone, and an `overrides` entry naming either refuses the run: remove it. `@effect/tsgo`'s `leakingRequirements` checks the second. `adhere-ignore` comments naming them suppress nothing. |
|
|
1311
1316
|
|
|
1312
1317
|
## Development
|
|
1313
1318
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drkmttr/adhere",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Lints a repository against the rules in .adhere/, judged by Jev.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,10 +68,10 @@
|
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
"optionalDependencies": {
|
|
71
|
-
"@drkmttr/adhere-darwin-arm64": "0.
|
|
72
|
-
"@drkmttr/adhere-darwin-x64": "0.
|
|
73
|
-
"@drkmttr/adhere-linux-arm64": "0.
|
|
74
|
-
"@drkmttr/adhere-linux-x64": "0.
|
|
75
|
-
"@drkmttr/adhere-win32-x64": "0.
|
|
71
|
+
"@drkmttr/adhere-darwin-arm64": "0.15.0",
|
|
72
|
+
"@drkmttr/adhere-darwin-x64": "0.15.0",
|
|
73
|
+
"@drkmttr/adhere-linux-arm64": "0.15.0",
|
|
74
|
+
"@drkmttr/adhere-linux-x64": "0.15.0",
|
|
75
|
+
"@drkmttr/adhere-win32-x64": "0.15.0"
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: A test should supply
|
|
2
|
+
description: A test of code that consumes configuration should supply it with Layer.succeed on the config service, and should not set environment variables or a ConfigProvider to do it.
|
|
3
3
|
tests: only
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
Code that reads its settings from the environment in a test reads them from
|
|
7
|
+
the environment in production too, so a test that sets them hides that the
|
|
8
|
+
code skips the config service.
|
|
9
|
+
|
|
10
|
+
A test whose subject is how configuration is read, such as its decoding,
|
|
11
|
+
defaults, precedence, or fallback to the environment, has to supply raw
|
|
12
|
+
inputs and is out of scope. So are values a test plants to check that they
|
|
13
|
+
never leak, and the bindings of a separately deployed Worker. A test or
|
|
14
|
+
fixture that sets the environment so the code under test reads its settings
|
|
15
|
+
from there is in scope, even in a file that also tests a loader.
|
|
16
|
+
|
|
6
17
|
## Should
|
|
7
18
|
|
|
8
19
|
```ts
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: A primitive with semantic meaning, such as an id, email, URL, port, or
|
|
2
|
+
description: A primitive with semantic meaning in a domain type, such as an id, email, URL, or port in a service's contract, an event, or a stored record, must be declared as a branded schema, never as a bare string or number.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
Two ids of different things are both strings, so nothing stops one being
|
|
6
|
+
passed where the other belongs. A brand makes that mix-up a type error where
|
|
7
|
+
the value crosses from one module to another.
|
|
8
|
+
|
|
9
|
+
The rule judges where a domain type is declared: a schema field, interface, or
|
|
10
|
+
type alias for what services exchange, return, or store. A shape that mirrors
|
|
11
|
+
an external payload or a table row until it is mapped, a view's props or view
|
|
12
|
+
model, and a literal, local variable, or parameter whose type is declared
|
|
13
|
+
elsewhere are not domain types. A type the code's own modules pass between
|
|
14
|
+
them, such as a reference to a thread, an issue, or a delivery, is a domain
|
|
15
|
+
type even in a file that also mirrors a payload.
|
|
16
|
+
|
|
5
17
|
## Must
|
|
6
18
|
|
|
7
19
|
```ts
|
|
@@ -25,6 +37,6 @@ type AccountId = string;
|
|
|
25
37
|
|
|
26
38
|
const Session = Schema.Struct({
|
|
27
39
|
accountId: Schema.String,
|
|
28
|
-
|
|
40
|
+
email: Schema.String,
|
|
29
41
|
});
|
|
30
42
|
```
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: Variants of a domain type, alternatives that carry fields and that a service returns, stores, or decodes, must be tagged schemas such as Schema.TaggedClass or Schema.TaggedStruct members of a Schema.Union, never TypeScript object types joined by a tag field.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
A tagged schema decodes each variant at the boundary and lets `Match.tag`
|
|
6
|
+
check every case, where a hand-written union is neither validated nor checked
|
|
7
|
+
for a case added later. Simple alternatives without fields may be
|
|
8
|
+
Schema.Literals.
|
|
9
|
+
|
|
10
|
+
The rule judges declared union types. A union used only inside one module,
|
|
11
|
+
such as a view's state, a reducer's actions, or a function's own result, is
|
|
12
|
+
not a domain type, and neither is a generic type, an object literal with a tag
|
|
13
|
+
field, or a type that mirrors a third-party format whose tag key that format
|
|
14
|
+
fixes, such as a Slack Block Kit block. A union that a service returns, or an
|
|
15
|
+
event a module emits, is in scope even in a file that declares nothing else.
|
|
16
|
+
|
|
5
17
|
## Must
|
|
6
18
|
|
|
7
19
|
```ts
|
|
@@ -27,8 +39,9 @@ const renderResult = (result: Result) =>
|
|
|
27
39
|
## Never
|
|
28
40
|
|
|
29
41
|
```ts
|
|
30
|
-
type
|
|
42
|
+
type Payment = { kind: "card"; last4: string } | { kind: "invoice"; dueDate: string };
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
interface Payments {
|
|
45
|
+
readonly find: (id: PaymentId) => Effect.Effect<Payment, PaymentNotFound>;
|
|
46
|
+
}
|
|
34
47
|
```
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: A service that depends on other services must be a Context.Service built by a Layer that yields those services as it is constructed, never a
|
|
2
|
+
description: A service that depends on other services must be a Context.Service built by a Layer that yields those services as it is constructed, never a function or class that takes other services' implementations as arguments and returns a service's implementation for its callers to wire by hand.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
A service wired by hand hides its dependencies from the types, so each caller
|
|
6
|
+
builds them again, and a test cannot replace one with a layer.
|
|
7
|
+
|
|
8
|
+
A function that the service's own Layer calls with the services it yielded is
|
|
9
|
+
part of that Layer and is out of scope, and so is a decorator that takes a
|
|
10
|
+
service and returns the same service. A boundary adapter that wraps a
|
|
11
|
+
library's client, such as an SDK or a Promise API, takes no service and is out
|
|
12
|
+
of scope. A function that builds a service from other services is in scope
|
|
13
|
+
when code other than that service's Layer calls it.
|
|
14
|
+
|
|
5
15
|
## Must
|
|
6
16
|
|
|
7
17
|
```ts
|
|
@@ -31,8 +41,11 @@ export const UsersLive = Layer.effect(
|
|
|
31
41
|
## Never
|
|
32
42
|
|
|
33
43
|
```ts
|
|
34
|
-
export const makeUsers = (
|
|
35
|
-
|
|
44
|
+
export const makeUsers = (
|
|
45
|
+
database: Database["Service"],
|
|
46
|
+
logger: Logger["Service"],
|
|
47
|
+
): Users["Service"] => ({
|
|
48
|
+
find: (id) =>
|
|
36
49
|
Effect.gen(function* () {
|
|
37
50
|
yield* logger.log(`finding ${id}`);
|
|
38
51
|
return yield* database.findUser(id);
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: The interface a Context.Service exposes must hold only readonly members, never writable fields or mutable state, such as a Map, Set, or array, that its consumers could change, even through a readonly property.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
State the service's implementation keeps behind its operations is not part of
|
|
6
|
+
its interface, and neither are a fixture's controls kept beside the service it
|
|
7
|
+
provides. A plain interface that no Context.Service declares, such as a
|
|
8
|
+
helper's registry or a connection's data, is out of scope. A Context.Service's
|
|
9
|
+
interface is in scope however its file declares it.
|
|
10
|
+
|
|
5
11
|
## Must
|
|
6
12
|
|
|
7
13
|
```ts
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: A service's operations must have no requirements, typed Effect<A, E, never>, with their dependencies acquired when the service's Layer is built, never left in an operation's requirements type, as in Effect<A, E, Database>.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
## Must
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
class Users extends Context.Service<
|
|
9
|
-
Users,
|
|
10
|
-
{
|
|
11
|
-
readonly find: (id: UserId) => Effect.Effect<User, UserNotFound, never>;
|
|
12
|
-
}
|
|
13
|
-
>()("@app/Users") {}
|
|
14
|
-
|
|
15
|
-
export const UsersLive = Layer.effect(
|
|
16
|
-
Users,
|
|
17
|
-
Effect.gen(function* () {
|
|
18
|
-
const database = yield* Database;
|
|
19
|
-
const logger = yield* Logger;
|
|
20
|
-
return Users.of({
|
|
21
|
-
find: (id) =>
|
|
22
|
-
Effect.gen(function* () {
|
|
23
|
-
yield* logger.log(`finding ${id}`);
|
|
24
|
-
return yield* database.findUser(id);
|
|
25
|
-
}),
|
|
26
|
-
});
|
|
27
|
-
}),
|
|
28
|
-
);
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Never
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
class Users extends Context.Service<
|
|
35
|
-
Users,
|
|
36
|
-
{
|
|
37
|
-
readonly find: (id: UserId) => Effect.Effect<User, UserNotFound, Database | Logger>;
|
|
38
|
-
}
|
|
39
|
-
>()("@app/Users") {}
|
|
40
|
-
|
|
41
|
-
export const UsersLive = Layer.succeed(Users, {
|
|
42
|
-
find: (id) =>
|
|
43
|
-
Effect.gen(function* () {
|
|
44
|
-
const database = yield* Database;
|
|
45
|
-
const logger = yield* Logger;
|
|
46
|
-
yield* logger.log(`finding ${id}`);
|
|
47
|
-
return yield* database.findUser(id);
|
|
48
|
-
}),
|
|
49
|
-
});
|
|
50
|
-
```
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: A test implementation of a service should be built with Layer.sync or Layer.succeed over in-memory state, and should not reach a real database, network, or file. Integration tests that exercise the real service are not in scope.
|
|
3
|
-
tests: only
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## Should
|
|
7
|
-
|
|
8
|
-
```ts
|
|
9
|
-
static readonly testLayer = Layer.sync(Cache, () => {
|
|
10
|
-
const store = new Map<string, string>()
|
|
11
|
-
|
|
12
|
-
const get = (key: string) => Effect.succeed(store.get(key) ?? null)
|
|
13
|
-
const set = (key: string, value: string) => Effect.sync(() => void store.set(key, value))
|
|
14
|
-
|
|
15
|
-
return { get, set }
|
|
16
|
-
})
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Should not
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
const EmailTest = Layer.effect(
|
|
23
|
-
Email,
|
|
24
|
-
Effect.gen(function* () {
|
|
25
|
-
const smtp = yield* SmtpClient;
|
|
26
|
-
return Email.of({ send: (message) => smtp.deliver(message) });
|
|
27
|
-
}),
|
|
28
|
-
).pipe(Layer.provide(SmtpClient.layer({ host: "localhost", port: 1025 })));
|
|
29
|
-
```
|