@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 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/) | 18 | 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`. |
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.** Four rules say `tests: only`, so they judge tests and nothing else:
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.** Two rules say "should": test layers are in memory, outside
858
- integration tests, and tests provide config through a layer.
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, and
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 | What happens now |
1304
- | ----------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1305
- | 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`. |
1306
- | 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. |
1307
- | 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. |
1308
- | before 0.13 | `.adhere/tsconfig.json` included only `config.ts`. | Make its `include` `["**/*.ts"]` to give rules written in TypeScript their types. |
1309
- | 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. |
1310
- | 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. |
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.14.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.14.0",
72
- "@drkmttr/adhere-darwin-x64": "0.14.0",
73
- "@drkmttr/adhere-linux-arm64": "0.14.0",
74
- "@drkmttr/adhere-linux-x64": "0.14.0",
75
- "@drkmttr/adhere-win32-x64": "0.14.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 config with Layer.succeed on the config service, and should not set environment variables or a ConfigProvider to do it.
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 count, must be declared as a branded schema, never as a bare string or number. The rule judges where a type is declared, such as a schema field, an interface, or a type alias; a literal value, local variable, or parameter whose type is declared elsewhere is out of scope.
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
- retries: Schema.Number,
40
+ email: Schema.String,
29
41
  });
30
42
  ```
@@ -1,7 +1,19 @@
1
1
  ---
2
- description: Structured variants, alternatives that carry fields, must be Schema.TaggedClass members of a Schema.Union, never hand-written object types joined by a tag field. Simple alternatives without fields may be Schema.Literals. The rule judges declared union types and schemas; an object literal with a tag field is out of scope, and so is a type that mirrors a third-party format whose tag key that format fixes, such as the type of a Slack Block Kit block.
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 Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number };
42
+ type Payment = { kind: "card"; last4: string } | { kind: "invoice"; dueDate: string };
31
43
 
32
- const area = (shape: Shape) =>
33
- shape.kind === "circle" ? Math.PI * shape.radius ** 2 : shape.side ** 2;
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 factory function or class that takes services, clients, or their implementations as arguments and returns the service's operations with them built in. A helper that takes a client and one call's arguments and returns a single Effect is not a factory and is out of scope; the code that assembles the service from such helpers is in scope.
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 = (database: Database["Service"], logger: Logger["Service"]) => ({
35
- find: (id: UserId) =>
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: A service must expose only readonly members, never mutable state.
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
- ```