@drkmttr/adhere 0.9.5 → 0.11.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 +406 -90
- package/package.json +7 -7
- package/presets/alchemy/apis/match-remote-errors-by-tag.md +6 -2
- package/presets/alchemy/apis/schemas-at-trust-boundaries.md +6 -2
- package/presets/alchemy/data/commit-migrations-then-deploy.md +6 -2
- package/presets/alchemy/data/hyperdrive-direct-origin.md +6 -2
- package/presets/alchemy/data/migrations-use-direct-url.md +6 -2
- package/presets/alchemy/data/never-disable-tls-verification.md +6 -2
- package/presets/alchemy/data/private-endpoint-at-runtime.md +6 -2
- package/presets/alchemy/data/single-migration-owner.md +6 -2
- package/presets/alchemy/durable/idempotent-event-handlers.md +6 -2
- package/presets/alchemy/durable/io-inside-steps.md +6 -2
- package/presets/alchemy/durable/no-self-triggering-bucket-writes.md +6 -2
- package/presets/alchemy/durable/stable-step-names.md +6 -2
- package/presets/alchemy/durable/webhook-state-is-truth.md +6 -2
- package/presets/alchemy/providers/diff-guards-unresolved-props.md +6 -2
- package/presets/alchemy/providers/idempotent-delete.md +8 -3
- package/presets/alchemy/providers/lazy-layer-construction.md +6 -2
- package/presets/alchemy/providers/reconcile-from-observed-state.md +6 -2
- package/presets/alchemy/resources/anchor-paths-with-import-meta.md +6 -2
- package/presets/alchemy/resources/idempotent-actions-and-seeds.md +6 -2
- package/presets/alchemy/resources/previews-reference-shared-resources.md +6 -2
- package/presets/alchemy/resources/protect-irreplaceable-data.md +6 -2
- package/presets/alchemy/resources/remote-state-for-ci.md +6 -2
- package/presets/alchemy/resources/stable-logical-ids.md +6 -2
- package/presets/alchemy/resources/stage-unique-physical-names.md +6 -2
- package/presets/alchemy/runtime/bind-tag-not-implementation.md +6 -2
- package/presets/alchemy/runtime/deterministic-config-defaults.md +6 -2
- package/presets/alchemy/runtime/disposables-in-request-scope.md +6 -2
- package/presets/alchemy/runtime/lambda-no-post-response-work.md +6 -2
- package/presets/alchemy/runtime/no-per-request-work-at-construction.md +6 -2
- package/presets/alchemy/runtime/resolve-config-and-bindings-at-construction.md +6 -2
- package/presets/alchemy/secrets/let-alchemy-mint-infra-secrets.md +6 -2
- package/presets/alchemy/secrets/none-in-browser-env.md +6 -2
- package/presets/alchemy/secrets/redacted-until-use.md +6 -2
- package/presets/alchemy/secrets/unwrap-before-compare-or-send.md +6 -2
- package/presets/alchemy/security/authorize-public-endpoints.md +6 -2
- package/presets/alchemy/security/github-secret-not-variable.md +6 -2
- package/presets/alchemy/security/identity-from-verified-session.md +6 -2
- package/presets/alchemy/security/keep-auth-protections-on.md +6 -2
- package/presets/alchemy/security/least-privilege-bindings.md +6 -2
- package/presets/alchemy/security/least-privilege-ci-credentials.md +6 -2
- package/presets/alchemy/security/lookup-failure-is-not-anonymous.md +6 -2
- package/presets/effect/basics/gen-for-sequencing.md +7 -3
- package/presets/effect/basics/instrument-with-pipe.md +6 -2
- package/presets/effect/config/business-logic-depends-on-config-service.md +6 -2
- package/presets/effect/config/secrets-are-redacted.md +6 -2
- package/presets/effect/config/tests-provide-values-directly.md +7 -2
- package/presets/effect/config/validate-with-schema.md +6 -2
- package/presets/effect/data/brand-meaningful-primitives.md +7 -3
- package/presets/effect/data/variants-are-tagged-unions.md +7 -3
- package/presets/effect/errors/catch-defects-at-boundaries-only.md +6 -2
- package/presets/effect/errors/defects-for-bugs.md +6 -2
- package/presets/effect/errors/wrap-external-errors.md +6 -2
- package/presets/effect/services/dependencies-through-layers.md +7 -3
- package/presets/effect/services/fresh-layer-per-test.md +7 -2
- package/presets/effect/services/memoize-parameterized-layers.md +6 -2
- package/presets/effect/services/no-mutable-state.md +6 -2
- package/presets/effect/services/operations-have-no-requirements.md +6 -2
- package/presets/effect/services/test-layers-are-in-memory.md +7 -2
- package/presets/effect/testing/test-clock-for-time.md +7 -2
- package/presets/react/effects/clean-up-what-they-start.md +23 -0
- package/presets/react/effects/event-logic-in-handlers.md +29 -0
- package/presets/react/effects/external-stores-use-sync-external-store.md +38 -0
- package/presets/react/effects/fetches-ignore-stale-responses.md +26 -0
- package/presets/react/effects/notify-parents-in-handlers.md +29 -0
- package/presets/react/server/no-private-data-to-client.md +23 -0
- package/presets/react/server/server-functions-authorize-and-validate.md +29 -0
- package/presets/react/state/no-contradictory-state.md +17 -0
- package/presets/react/state/no-props-copied-into-state.md +23 -0
- package/presets/react/state/store-ids-not-copies.md +20 -0
- package/presets/security/constant-time-secret-compare.md +22 -0
- package/presets/security/no-secrets-in-output.md +19 -0
- package/presets/security/no-untrusted-input-in-commands-or-paths.md +18 -0
- package/presets/security/parameterized-queries.md +16 -0
- package/presets/typescript/async/concurrent-independent-work.md +23 -0
- package/presets/typescript/async/network-calls-have-timeouts.md +19 -0
- package/presets/typescript/design/no-argument-mutation.md +20 -0
- package/presets/typescript/errors/no-swallowed-errors.md +29 -0
- package/presets/typescript/errors/release-on-every-path.md +29 -0
- package/presets/typescript/testing/assert-outcomes.md +22 -0
- package/presets/typescript/testing/independent-tests.md +41 -0
- package/presets/typescript/types/assertions-only-when-proven.md +20 -0
- package/presets/typescript/types/derive-dont-restate.md +24 -0
- package/presets/typescript/types/unrepresentable-invalid-states.md +23 -0
- package/presets/typescript/types/validate-external-data.md +21 -0
- package/skills/adhere/SKILL.md +41 -12
- package/skills/adhere-fix/SKILL.md +180 -0
- package/src/cli.ts +59 -14
- package/src/comments.ts +36 -0
- package/src/config.ts +126 -14
- package/src/contradictions.ts +10 -2
- package/src/excerpt.ts +78 -58
- package/src/highlight.ts +2 -0
- package/src/index.ts +1 -1
- package/src/init.ts +119 -12
- package/src/main.ts +0 -1
- package/src/markdown.ts +83 -14
- package/src/models/Audit.ts +2 -0
- package/src/presets.ts +12 -3
- package/src/rules.ts +36 -3
- package/src/services/AdhereConfig.ts +36 -13
- package/src/services/AuditCache.ts +31 -5
- package/src/services/Jev.http.ts +76 -11
- package/src/services/Jev.ts +191 -73
- package/src/services/SourceWalker.ts +51 -13
- package/src/suppress.ts +119 -0
- package/src/version.ts +25 -1
- package/src/workflows/audit.ts +192 -40
- package/src/workflows/format.ts +71 -24
- package/presets/alchemy/data/parameterized-sql.md +0 -13
- package/presets/alchemy/secrets/never-logged-returned-or-output.md +0 -15
- package/presets/effect/basics/external-calls-are-resilient.md +0 -17
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
description: In a Worker, Function, or Service, Config values and binding factories must be yielded in the outer construction Effect and captured for the handlers, never yielded for the first time inside fetch or another request handler, which does not run at deploy time, so nothing gets bound.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
export default Cloudflare.Worker("Api", { main: import.meta.url },
|
|
7
9
|
Effect.gen(function* () {
|
|
8
10
|
const apiKey = yield* Config.Redacted("API_KEY");
|
|
@@ -17,7 +19,9 @@ export default Cloudflare.Worker("Api", { main: import.meta.url },
|
|
|
17
19
|
);
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
## Never
|
|
23
|
+
|
|
24
|
+
```ts
|
|
21
25
|
export default Cloudflare.Worker("Api", { main: import.meta.url },
|
|
22
26
|
Effect.gen(function* () {
|
|
23
27
|
return {
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
description: A token the infrastructure owns, such as a webhook secret or bearer token, should be minted once with alchemy's Random or left for alchemy to provision, and should not be a hardcoded literal or regenerated on every deploy with crypto.randomUUID().
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Should
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
export const WebhookSecret = Random("WebhookSecret");
|
|
7
9
|
const auth = yield* BetterAuth({ basePath: "/api/auth", emailAndPassword: { enabled: true } });
|
|
8
10
|
```
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
## Should not
|
|
13
|
+
|
|
14
|
+
```ts
|
|
11
15
|
const webhookSecret = crypto.randomUUID(); // a new secret on every deploy
|
|
12
16
|
const auth = yield* BetterAuth({ basePath: "/api/auth", secret: Redacted.make("dev-secret") });
|
|
13
17
|
```
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
description: A value in an env key that a framework inlines into the browser bundle, such as VITE_, NEXT_PUBLIC_, PUBLIC_, or NUXT_PUBLIC_, must be public configuration, never a secret, credential, or connection string, even wrapped in Redacted.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const site = yield* Cloudflare.Website.Vite("Site", {
|
|
7
9
|
env: { VITE_API_URL: api.url, STRIPE_SECRET_KEY: Config.Redacted("STRIPE_SECRET_KEY") },
|
|
8
10
|
});
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
## Never
|
|
14
|
+
|
|
15
|
+
```ts
|
|
12
16
|
const site = yield* Cloudflare.Website.Vite("Site", {
|
|
13
17
|
env: { VITE_STRIPE_SECRET_KEY: Config.Redacted("STRIPE_SECRET_KEY"), VITE_DATABASE_URL: branch.connectionUri },
|
|
14
18
|
});
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
description: A secret should be read with Config.Redacted, passed to resources and env as Redacted, and unwrapped with Redacted.value only at the call that needs the raw string; it should not be read with Config.String or process.env, or unwrapped early into plain variables.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Should
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const apiKey = yield* Config.Redacted("OPENAI_API_KEY");
|
|
7
9
|
return {
|
|
8
10
|
fetch: Effect.gen(function* () {
|
|
@@ -12,7 +14,9 @@ return {
|
|
|
12
14
|
};
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
## Should not
|
|
18
|
+
|
|
19
|
+
```ts
|
|
16
20
|
const apiKey = Redacted.value(yield* Config.Redacted("OPENAI_API_KEY"));
|
|
17
21
|
const webhook = process.env.SLACK_WEBHOOK_URL!;
|
|
18
22
|
return { fetch: handler(apiKey, webhook) };
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
description: A Redacted value must be unwrapped with Redacted.value before it is compared, sent, or serialized, never put into a template literal, string concatenation, String(), or JSON, where it becomes the literal text <redacted> and silently fails every check.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const expected = yield* Config.Redacted("AUTH_TOKEN");
|
|
7
9
|
if (request.headers.authorization !== `Bearer ${Redacted.value(expected)}`) {
|
|
8
10
|
return HttpServerResponse.empty({ status: 401 });
|
|
9
11
|
}
|
|
10
12
|
```
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
## Never
|
|
15
|
+
|
|
16
|
+
```ts
|
|
13
17
|
const expected = yield* Config.Redacted("AUTH_TOKEN");
|
|
14
18
|
if (request.headers.authorization !== `Bearer ${expected}`) { // compares against "Bearer <redacted>"
|
|
15
19
|
return HttpServerResponse.empty({ status: 401 });
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
description: A handler behind a public URL, such as a Function URL, workers.dev route, or a Durable Object chosen by a client-supplied name, must authenticate the caller and check its access before doing protected work, never treating CORS, an obscure URL, or a trusted origin as authorization.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
fetch: Effect.gen(function* () {
|
|
7
9
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
8
10
|
const user = yield* verifySession(request.headers.authorization);
|
|
@@ -11,7 +13,9 @@ fetch: Effect.gen(function* () {
|
|
|
11
13
|
}),
|
|
12
14
|
```
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
## Never
|
|
17
|
+
|
|
18
|
+
```ts
|
|
15
19
|
fetch: Effect.gen(function* () {
|
|
16
20
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
17
21
|
const name = /^\/counters\/([^/]+)$/.exec(new URL(request.url).pathname)?.[1] ?? "default";
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
description: A sensitive CI value, such as a token, key, password, or connection string, must be stored with GitHub.Secret, never with GitHub.Variable, which is plain text and visible in logs.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
yield* GitHub.Secret("CloudflareToken", { owner, repository, name: "CLOUDFLARE_API_TOKEN", value: token.value });
|
|
7
9
|
yield* GitHub.Variable("AwsRegion", { owner, repository, name: "AWS_REGION", value: "us-east-1" });
|
|
8
10
|
```
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
## Never
|
|
13
|
+
|
|
14
|
+
```ts
|
|
11
15
|
yield* GitHub.Variables({ owner, repository, variables: { CLOUDFLARE_API_TOKEN: token.value } });
|
|
12
16
|
```
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
description: The acting user must come from the session or token verified in that request, never from an ID in the request body, query, or headers, and never from a user cached in a process-wide layer.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const session = yield* auth.getSession();
|
|
7
9
|
if (session === null) return yield* Effect.fail(new Unauthorized());
|
|
8
10
|
const rows = yield* sql`SELECT * FROM uploads WHERE owner_id = ${session.user.id}`;
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
## Never
|
|
14
|
+
|
|
15
|
+
```ts
|
|
12
16
|
const { ownerId } = yield* HttpServerRequest.schemaBodyJson(Schema.Struct({ ownerId: Schema.String }));
|
|
13
17
|
const rows = yield* sql`SELECT * FROM uploads WHERE owner_id = ${ownerId}`;
|
|
14
18
|
```
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
description: Auth configuration must keep state, PKCE, origin and CSRF checks, ID-token verification, and email verification enabled, never disable them or merge accounts by email alone to work around an error or a cross-origin setup.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const auth = yield* BetterAuth({
|
|
7
9
|
trustedOrigins: [siteOrigin],
|
|
8
10
|
emailAndPassword: { enabled: true, requireEmailVerification: true },
|
|
@@ -10,7 +12,9 @@ const auth = yield* BetterAuth({
|
|
|
10
12
|
});
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
## Never
|
|
16
|
+
|
|
17
|
+
```ts
|
|
14
18
|
const auth = yield* BetterAuth({
|
|
15
19
|
trustedOrigins: ["*"],
|
|
16
20
|
advanced: { disableCSRFCheck: true },
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
description: A runtime should bind the narrowest capability its calls need, a Read or Write binding when it only reads or only writes, and should not bind ReadWrite for one-sided use.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Should
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const bucket = yield* Cloudflare.R2.ReadBucket(Uploads);
|
|
7
9
|
return { fetch: Effect.gen(function* () { return yield* bucket.get("report.json"); }) };
|
|
8
10
|
```
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
## Should not
|
|
13
|
+
|
|
14
|
+
```ts
|
|
11
15
|
const bucket = yield* Cloudflare.R2.ReadWriteBucket(Uploads); // only get is ever called
|
|
12
16
|
return { fetch: Effect.gen(function* () { return yield* bucket.get("report.json"); }) };
|
|
13
17
|
```
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
description: Credentials a stack mints for CI should be scoped to what the app deploys, using a GitHub OIDC role pinned to the repository on AWS and a trimmed permission list on Cloudflare, and should not grant AdministratorAccess or unused write scopes.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Should
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const role = yield* AWS.IAM.Role("GitHubDeploy", {
|
|
7
9
|
assumeRolePolicyDocument: { Version: "2012-10-17", Statement: [{
|
|
8
10
|
Effect: "Allow", Principal: { Federated: oidc.openIDConnectProviderArn },
|
|
@@ -13,7 +15,9 @@ const role = yield* AWS.IAM.Role("GitHubDeploy", {
|
|
|
13
15
|
});
|
|
14
16
|
```
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
## Should not
|
|
19
|
+
|
|
20
|
+
```ts
|
|
17
21
|
const role = yield* AWS.IAM.Role("GitHubDeploy", {
|
|
18
22
|
assumeRolePolicyDocument: { Version: "2012-10-17", Statement: [{
|
|
19
23
|
Effect: "Allow", Principal: { Federated: oidc.openIDConnectProviderArn },
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
description: Only a null session may be treated as anonymous; a failed session lookup must stay in the error channel or map to a server error, never be collapsed into signed out.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const session = yield* auth.getSession().pipe(
|
|
7
9
|
Effect.mapError(() => new AuthenticationUnavailable()),
|
|
8
10
|
);
|
|
9
11
|
if (session === null) return yield* Effect.fail(new Unauthorized());
|
|
10
12
|
```
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
## Never
|
|
15
|
+
|
|
16
|
+
```ts
|
|
13
17
|
const session = yield* auth.getSession().pipe(Effect.orElseSucceed(() => null));
|
|
14
18
|
if (session === null) return yield* Effect.fail(new Unauthorized());
|
|
15
19
|
```
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Sequential effectful steps must be written with Effect.gen and yield*, never as nested flatMap or callback chains.
|
|
2
|
+
description: Sequential effectful steps must be written with Effect.gen and yield*, never as nested flatMap or callback chains. A single flatMap or andThen in a flat pipe is out of scope; the rule is about a step whose callback holds further steps.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const program = Effect.gen(function* () {
|
|
7
9
|
const data = yield* fetchData;
|
|
8
10
|
yield* Effect.logInfo(`Processing data: ${data}`);
|
|
@@ -10,7 +12,9 @@ const program = Effect.gen(function* () {
|
|
|
10
12
|
});
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
## Never
|
|
16
|
+
|
|
17
|
+
```ts
|
|
14
18
|
const page = fetchUser(id).pipe(
|
|
15
19
|
Effect.flatMap((user) =>
|
|
16
20
|
fetchOrders(user.id).pipe(Effect.flatMap((orders) => render(user, orders))),
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
description: Timeouts, retries, logging, and spans must be attached with .pipe, never written into the body of the effect.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const program = fetchData.pipe(
|
|
7
9
|
Effect.timeout("5 seconds"),
|
|
8
10
|
Effect.retry(Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(3)))),
|
|
@@ -11,7 +13,9 @@ const program = fetchData.pipe(
|
|
|
11
13
|
);
|
|
12
14
|
```
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
## Never
|
|
17
|
+
|
|
18
|
+
```ts
|
|
15
19
|
const report = Effect.gen(function* () {
|
|
16
20
|
const rows = yield* Effect.timeout(loadRows, "5 seconds");
|
|
17
21
|
return yield* Effect.withSpan(summarize(rows), "summarize");
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
description: Business logic must depend on a config service. Config primitives must be read only inside that service's layer, never in business logic.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
class ApiConfig extends Context.Service<
|
|
7
9
|
ApiConfig,
|
|
8
10
|
{
|
|
@@ -21,7 +23,9 @@ class ApiConfig extends Context.Service<
|
|
|
21
23
|
}
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
## Never
|
|
27
|
+
|
|
28
|
+
```ts
|
|
25
29
|
const sendInvoice = Effect.fn("sendInvoice")(function* (invoice: Invoice) {
|
|
26
30
|
const apiUrl = yield* Config.string("BILLING_API_URL");
|
|
27
31
|
return yield* post(apiUrl, invoice);
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
description: A token, password, or key must be read with Config.redacted, never with Config.string.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const program = Effect.gen(function* () {
|
|
7
9
|
const apiKey = yield* Config.redacted("API_KEY");
|
|
8
10
|
|
|
@@ -14,6 +16,8 @@ const program = Effect.gen(function* () {
|
|
|
14
16
|
});
|
|
15
17
|
```
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
## Never
|
|
20
|
+
|
|
21
|
+
```ts
|
|
18
22
|
const token = yield* Config.string("GITHUB_TOKEN");
|
|
19
23
|
```
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
---
|
|
2
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.
|
|
3
|
+
tests: only
|
|
3
4
|
---
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
## Should
|
|
7
|
+
|
|
8
|
+
```ts
|
|
6
9
|
Effect.runPromise(
|
|
7
10
|
program.pipe(
|
|
8
11
|
Effect.provide(
|
|
@@ -15,7 +18,9 @@ Effect.runPromise(
|
|
|
15
18
|
);
|
|
16
19
|
```
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
## Should not
|
|
22
|
+
|
|
23
|
+
```ts
|
|
19
24
|
beforeAll(() => {
|
|
20
25
|
process.env.DATABASE_URL = "postgres://localhost/test";
|
|
21
26
|
});
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
description: A config value with constraints must be validated as it is read, with Config.schema or Config.mapOrFail, never checked by hand after reading.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const Port = Schema.NumberFromString.pipe(
|
|
7
9
|
Schema.check(Schema.isInt()),
|
|
8
10
|
Schema.check(Schema.isBetween({ minimum: 1, maximum: 65535 })),
|
|
@@ -28,7 +30,9 @@ const program = Effect.gen(function* () {
|
|
|
28
30
|
});
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
## Never
|
|
34
|
+
|
|
35
|
+
```ts
|
|
32
36
|
const timeoutMs = yield* Config.number("TIMEOUT_MS");
|
|
33
37
|
if (timeoutMs <= 0 || timeoutMs > 60_000) {
|
|
34
38
|
return yield* Effect.die("TIMEOUT_MS out of range");
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: A primitive with semantic meaning, such as an id, email, URL, port, or count, must be a branded schema, never a bare string or number.
|
|
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.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
export const UserId = Schema.String.pipe(Schema.brand("UserId"));
|
|
7
9
|
export type UserId = typeof UserId.Type;
|
|
8
10
|
|
|
@@ -16,7 +18,9 @@ export const Port = Schema.Int.pipe(
|
|
|
16
18
|
export type Port = typeof Port.Type;
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
## Never
|
|
22
|
+
|
|
23
|
+
```ts
|
|
20
24
|
type AccountId = string;
|
|
21
25
|
|
|
22
26
|
const Session = Schema.Struct({
|
|
@@ -1,8 +1,10 @@
|
|
|
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.
|
|
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.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
export class Success extends Schema.TaggedClass<Success>("Success")("Success", {
|
|
7
9
|
value: Schema.Number,
|
|
8
10
|
}) {}
|
|
@@ -22,7 +24,9 @@ const renderResult = (result: Result) =>
|
|
|
22
24
|
);
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
## Never
|
|
28
|
+
|
|
29
|
+
```ts
|
|
26
30
|
type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number };
|
|
27
31
|
|
|
28
32
|
const area = (shape: Shape) =>
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
description: Defects must be caught only at a system boundary for logging or shutdown, never in business logic.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
// At app entry: if config fails, nothing can proceed
|
|
7
9
|
const main = Effect.gen(function* () {
|
|
8
10
|
const config = yield* loadConfig.pipe(Effect.orDie);
|
|
@@ -10,6 +12,8 @@ const main = Effect.gen(function* () {
|
|
|
10
12
|
});
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
## Never
|
|
16
|
+
|
|
17
|
+
```ts
|
|
14
18
|
const price = computePrice(cart).pipe(Effect.catchDefect(() => Effect.succeed(0)));
|
|
15
19
|
```
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
description: A typed error must be for a failure the caller can handle. A bug or invariant violation must be a defect, never a typed error.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const recovered: Effect.Effect<string, ValidationError> = program.pipe(
|
|
7
9
|
Effect.catchTag("HttpError", (error) =>
|
|
8
10
|
Effect.gen(function* () {
|
|
@@ -18,7 +20,9 @@ const main = Effect.gen(function* () {
|
|
|
18
20
|
});
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
## Never
|
|
24
|
+
|
|
25
|
+
```ts
|
|
22
26
|
class CacheCorrupted extends Schema.TaggedError<CacheCorrupted>()("CacheCorrupted", {
|
|
23
27
|
key: Schema.String,
|
|
24
28
|
}) {}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
description: An error from an external library must be wrapped in a tagged error with a Schema.Defect field, never passed through raw.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
class ApiError extends Schema.TaggedError<ApiError>()("ApiError", {
|
|
7
9
|
endpoint: Schema.String,
|
|
8
10
|
statusCode: Schema.Number,
|
|
@@ -23,7 +25,9 @@ const fetchUser = (id: string) =>
|
|
|
23
25
|
);
|
|
24
26
|
```
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
## Never
|
|
29
|
+
|
|
30
|
+
```ts
|
|
27
31
|
const charge = (amount: number) =>
|
|
28
32
|
Effect.tryPromise(() => stripe.charges.create({ amount, currency: "usd" }));
|
|
29
33
|
```
|
|
@@ -1,8 +1,10 @@
|
|
|
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.
|
|
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.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
class Users extends Context.Service<
|
|
7
9
|
Users,
|
|
8
10
|
{
|
|
@@ -26,7 +28,9 @@ export const UsersLive = Layer.effect(
|
|
|
26
28
|
);
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
## Never
|
|
32
|
+
|
|
33
|
+
```ts
|
|
30
34
|
export const makeUsers = (database: Database["Service"], logger: Logger["Service"]) => ({
|
|
31
35
|
find: (id: UserId) =>
|
|
32
36
|
Effect.gen(function* () {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
---
|
|
2
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
|
+
tests: only
|
|
3
4
|
---
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
## Must
|
|
7
|
+
|
|
8
|
+
```ts
|
|
6
9
|
it.effect("starts at zero", () =>
|
|
7
10
|
Effect.gen(function* () {
|
|
8
11
|
const counter = yield* Counter;
|
|
@@ -19,7 +22,9 @@ it.effect("increments without leaking", () =>
|
|
|
19
22
|
);
|
|
20
23
|
```
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
## Never
|
|
26
|
+
|
|
27
|
+
```ts
|
|
23
28
|
it.layer(Counter.layer)("Counter", (it) => {
|
|
24
29
|
it.effect("starts at zero", () => Effect.gen(function* () { /* ... */ }));
|
|
25
30
|
it.effect("increments", () => Effect.gen(function* () { /* ... */ }));
|
|
@@ -2,7 +2,9 @@
|
|
|
2
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
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
const postgresLayer = Postgres.layer({ url: "postgres://localhost/mydb", poolSize: 10 });
|
|
7
9
|
|
|
8
10
|
const goodAppLayer = Layer.merge(
|
|
@@ -11,7 +13,9 @@ const goodAppLayer = Layer.merge(
|
|
|
11
13
|
);
|
|
12
14
|
```
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
## Never
|
|
17
|
+
|
|
18
|
+
```ts
|
|
15
19
|
const Live = Layer.merge(
|
|
16
20
|
Sessions.layer.pipe(Layer.provide(Redis.layer({ host: "localhost" }))),
|
|
17
21
|
RateLimits.layer.pipe(Layer.provide(Redis.layer({ host: "localhost" }))),
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
description: A service must expose only readonly members, never mutable state.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
class Logger extends Context.Service<
|
|
7
9
|
Logger,
|
|
8
10
|
{
|
|
@@ -11,7 +13,9 @@ class Logger extends Context.Service<
|
|
|
11
13
|
>()("@app/Logger") {}
|
|
12
14
|
```
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
## Never
|
|
17
|
+
|
|
18
|
+
```ts
|
|
15
19
|
class Counter extends Context.Service<
|
|
16
20
|
Counter,
|
|
17
21
|
{ count: number; readonly increment: () => Effect.Effect<void> }
|
|
@@ -2,7 +2,9 @@
|
|
|
2
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
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Must
|
|
6
|
+
|
|
7
|
+
```ts
|
|
6
8
|
class Users extends Context.Service<
|
|
7
9
|
Users,
|
|
8
10
|
{
|
|
@@ -26,7 +28,9 @@ export const UsersLive = Layer.effect(
|
|
|
26
28
|
);
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
## Never
|
|
32
|
+
|
|
33
|
+
```ts
|
|
30
34
|
class Users extends Context.Service<
|
|
31
35
|
Users,
|
|
32
36
|
{
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
---
|
|
2
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
|
|
3
4
|
---
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
## Should
|
|
7
|
+
|
|
8
|
+
```ts
|
|
6
9
|
static readonly testLayer = Layer.sync(Cache, () => {
|
|
7
10
|
const store = new Map<string, string>()
|
|
8
11
|
|
|
@@ -13,7 +16,9 @@ static readonly testLayer = Layer.sync(Cache, () => {
|
|
|
13
16
|
})
|
|
14
17
|
```
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
## Should not
|
|
20
|
+
|
|
21
|
+
```ts
|
|
17
22
|
const EmailTest = Layer.effect(
|
|
18
23
|
Email,
|
|
19
24
|
Effect.gen(function* () {
|