@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.
Files changed (113) hide show
  1. package/README.md +406 -90
  2. package/package.json +7 -7
  3. package/presets/alchemy/apis/match-remote-errors-by-tag.md +6 -2
  4. package/presets/alchemy/apis/schemas-at-trust-boundaries.md +6 -2
  5. package/presets/alchemy/data/commit-migrations-then-deploy.md +6 -2
  6. package/presets/alchemy/data/hyperdrive-direct-origin.md +6 -2
  7. package/presets/alchemy/data/migrations-use-direct-url.md +6 -2
  8. package/presets/alchemy/data/never-disable-tls-verification.md +6 -2
  9. package/presets/alchemy/data/private-endpoint-at-runtime.md +6 -2
  10. package/presets/alchemy/data/single-migration-owner.md +6 -2
  11. package/presets/alchemy/durable/idempotent-event-handlers.md +6 -2
  12. package/presets/alchemy/durable/io-inside-steps.md +6 -2
  13. package/presets/alchemy/durable/no-self-triggering-bucket-writes.md +6 -2
  14. package/presets/alchemy/durable/stable-step-names.md +6 -2
  15. package/presets/alchemy/durable/webhook-state-is-truth.md +6 -2
  16. package/presets/alchemy/providers/diff-guards-unresolved-props.md +6 -2
  17. package/presets/alchemy/providers/idempotent-delete.md +8 -3
  18. package/presets/alchemy/providers/lazy-layer-construction.md +6 -2
  19. package/presets/alchemy/providers/reconcile-from-observed-state.md +6 -2
  20. package/presets/alchemy/resources/anchor-paths-with-import-meta.md +6 -2
  21. package/presets/alchemy/resources/idempotent-actions-and-seeds.md +6 -2
  22. package/presets/alchemy/resources/previews-reference-shared-resources.md +6 -2
  23. package/presets/alchemy/resources/protect-irreplaceable-data.md +6 -2
  24. package/presets/alchemy/resources/remote-state-for-ci.md +6 -2
  25. package/presets/alchemy/resources/stable-logical-ids.md +6 -2
  26. package/presets/alchemy/resources/stage-unique-physical-names.md +6 -2
  27. package/presets/alchemy/runtime/bind-tag-not-implementation.md +6 -2
  28. package/presets/alchemy/runtime/deterministic-config-defaults.md +6 -2
  29. package/presets/alchemy/runtime/disposables-in-request-scope.md +6 -2
  30. package/presets/alchemy/runtime/lambda-no-post-response-work.md +6 -2
  31. package/presets/alchemy/runtime/no-per-request-work-at-construction.md +6 -2
  32. package/presets/alchemy/runtime/resolve-config-and-bindings-at-construction.md +6 -2
  33. package/presets/alchemy/secrets/let-alchemy-mint-infra-secrets.md +6 -2
  34. package/presets/alchemy/secrets/none-in-browser-env.md +6 -2
  35. package/presets/alchemy/secrets/redacted-until-use.md +6 -2
  36. package/presets/alchemy/secrets/unwrap-before-compare-or-send.md +6 -2
  37. package/presets/alchemy/security/authorize-public-endpoints.md +6 -2
  38. package/presets/alchemy/security/github-secret-not-variable.md +6 -2
  39. package/presets/alchemy/security/identity-from-verified-session.md +6 -2
  40. package/presets/alchemy/security/keep-auth-protections-on.md +6 -2
  41. package/presets/alchemy/security/least-privilege-bindings.md +6 -2
  42. package/presets/alchemy/security/least-privilege-ci-credentials.md +6 -2
  43. package/presets/alchemy/security/lookup-failure-is-not-anonymous.md +6 -2
  44. package/presets/effect/basics/gen-for-sequencing.md +7 -3
  45. package/presets/effect/basics/instrument-with-pipe.md +6 -2
  46. package/presets/effect/config/business-logic-depends-on-config-service.md +6 -2
  47. package/presets/effect/config/secrets-are-redacted.md +6 -2
  48. package/presets/effect/config/tests-provide-values-directly.md +7 -2
  49. package/presets/effect/config/validate-with-schema.md +6 -2
  50. package/presets/effect/data/brand-meaningful-primitives.md +7 -3
  51. package/presets/effect/data/variants-are-tagged-unions.md +7 -3
  52. package/presets/effect/errors/catch-defects-at-boundaries-only.md +6 -2
  53. package/presets/effect/errors/defects-for-bugs.md +6 -2
  54. package/presets/effect/errors/wrap-external-errors.md +6 -2
  55. package/presets/effect/services/dependencies-through-layers.md +7 -3
  56. package/presets/effect/services/fresh-layer-per-test.md +7 -2
  57. package/presets/effect/services/memoize-parameterized-layers.md +6 -2
  58. package/presets/effect/services/no-mutable-state.md +6 -2
  59. package/presets/effect/services/operations-have-no-requirements.md +6 -2
  60. package/presets/effect/services/test-layers-are-in-memory.md +7 -2
  61. package/presets/effect/testing/test-clock-for-time.md +7 -2
  62. package/presets/react/effects/clean-up-what-they-start.md +23 -0
  63. package/presets/react/effects/event-logic-in-handlers.md +29 -0
  64. package/presets/react/effects/external-stores-use-sync-external-store.md +38 -0
  65. package/presets/react/effects/fetches-ignore-stale-responses.md +26 -0
  66. package/presets/react/effects/notify-parents-in-handlers.md +29 -0
  67. package/presets/react/server/no-private-data-to-client.md +23 -0
  68. package/presets/react/server/server-functions-authorize-and-validate.md +29 -0
  69. package/presets/react/state/no-contradictory-state.md +17 -0
  70. package/presets/react/state/no-props-copied-into-state.md +23 -0
  71. package/presets/react/state/store-ids-not-copies.md +20 -0
  72. package/presets/security/constant-time-secret-compare.md +22 -0
  73. package/presets/security/no-secrets-in-output.md +19 -0
  74. package/presets/security/no-untrusted-input-in-commands-or-paths.md +18 -0
  75. package/presets/security/parameterized-queries.md +16 -0
  76. package/presets/typescript/async/concurrent-independent-work.md +23 -0
  77. package/presets/typescript/async/network-calls-have-timeouts.md +19 -0
  78. package/presets/typescript/design/no-argument-mutation.md +20 -0
  79. package/presets/typescript/errors/no-swallowed-errors.md +29 -0
  80. package/presets/typescript/errors/release-on-every-path.md +29 -0
  81. package/presets/typescript/testing/assert-outcomes.md +22 -0
  82. package/presets/typescript/testing/independent-tests.md +41 -0
  83. package/presets/typescript/types/assertions-only-when-proven.md +20 -0
  84. package/presets/typescript/types/derive-dont-restate.md +24 -0
  85. package/presets/typescript/types/unrepresentable-invalid-states.md +23 -0
  86. package/presets/typescript/types/validate-external-data.md +21 -0
  87. package/skills/adhere/SKILL.md +41 -12
  88. package/skills/adhere-fix/SKILL.md +180 -0
  89. package/src/cli.ts +59 -14
  90. package/src/comments.ts +36 -0
  91. package/src/config.ts +126 -14
  92. package/src/contradictions.ts +10 -2
  93. package/src/excerpt.ts +78 -58
  94. package/src/highlight.ts +2 -0
  95. package/src/index.ts +1 -1
  96. package/src/init.ts +119 -12
  97. package/src/main.ts +0 -1
  98. package/src/markdown.ts +83 -14
  99. package/src/models/Audit.ts +2 -0
  100. package/src/presets.ts +12 -3
  101. package/src/rules.ts +36 -3
  102. package/src/services/AdhereConfig.ts +36 -13
  103. package/src/services/AuditCache.ts +31 -5
  104. package/src/services/Jev.http.ts +76 -11
  105. package/src/services/Jev.ts +191 -73
  106. package/src/services/SourceWalker.ts +51 -13
  107. package/src/suppress.ts +119 -0
  108. package/src/version.ts +25 -1
  109. package/src/workflows/audit.ts +192 -40
  110. package/src/workflows/format.ts +71 -24
  111. package/presets/alchemy/data/parameterized-sql.md +0 -13
  112. package/presets/alchemy/secrets/never-logged-returned-or-output.md +0 -15
  113. package/presets/effect/basics/external-calls-are-resilient.md +0 -17
@@ -1,8 +1,11 @@
1
1
  ---
2
2
  description: A test that depends on time must use TestClock, never a real sleep or it.live, unless the test needs real time.
3
+ tests: only
3
4
  ---
4
5
 
5
- ```ts must
6
+ ## Must
7
+
8
+ ```ts
6
9
  it.effect("time-based test", () =>
7
10
  Effect.gen(function* () {
8
11
  const fiber = yield* Effect.delay(Effect.succeed("done"), "10 seconds").pipe(Effect.forkChild);
@@ -13,7 +16,9 @@ it.effect("time-based test", () =>
13
16
  );
14
17
  ```
15
18
 
16
- ```ts never
19
+ ## Never
20
+
21
+ ```ts
17
22
  it.live("retries after a delay", () =>
18
23
  Effect.gen(function* () {
19
24
  yield* Effect.sleep("2 seconds");
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: An effect that subscribes to something, adds an event listener, starts a timer or an interval, or opens a connection must return a cleanup function that undoes it, never leave it running after the component unmounts or the effect runs again.
3
+ appliesTo: ["a React effect"]
4
+ ---
5
+
6
+ ## Must
7
+
8
+ ```tsx
9
+ useEffect(() => {
10
+ const onResize = () => setWidth(window.innerWidth);
11
+ window.addEventListener("resize", onResize);
12
+ return () => window.removeEventListener("resize", onResize);
13
+ }, []);
14
+ ```
15
+
16
+ ## Never
17
+
18
+ ```tsx
19
+ useEffect(() => {
20
+ const onResize = () => setWidth(window.innerWidth);
21
+ window.addEventListener("resize", onResize);
22
+ }, []);
23
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: Code that runs because the user did something, such as submitting a form, clicking a button, or choosing an option, must run in that event's handler, never in an effect that watches state the handler set. An effect is for keeping a component in sync with something outside React while it is shown.
3
+ appliesTo: ["a React component or hook"]
4
+ ---
5
+
6
+ ## Must
7
+
8
+ ```tsx
9
+ function CheckoutForm({ cart }: { cart: Cart }) {
10
+ const handleSubmit = async (event: FormEvent) => {
11
+ event.preventDefault();
12
+ await placeOrder(cart);
13
+ showToast("Order placed");
14
+ };
15
+ return <form onSubmit={handleSubmit}>{/* fields */}</form>;
16
+ }
17
+ ```
18
+
19
+ ## Never
20
+
21
+ ```tsx
22
+ function CheckoutForm({ cart }: { cart: Cart }) {
23
+ const [submitted, setSubmitted] = useState(false);
24
+ useEffect(() => {
25
+ if (submitted) placeOrder(cart).then(() => showToast("Order placed"));
26
+ }, [submitted, cart]);
27
+ return <form onSubmit={() => setSubmitted(true)}>{/* fields */}</form>;
28
+ }
29
+ ```
@@ -0,0 +1,38 @@
1
+ ---
2
+ description: A component or hook that reads a value from a store outside React, such as a browser API like `navigator.onLine` or a store module, should subscribe to it with `useSyncExternalStore`, and should not copy the value into state from a subscription an effect sets up.
3
+ appliesTo: ["a React component or hook"]
4
+ ---
5
+
6
+ ## Should
7
+
8
+ ```tsx
9
+ const subscribe = (callback: () => void) => {
10
+ window.addEventListener("online", callback);
11
+ window.addEventListener("offline", callback);
12
+ return () => {
13
+ window.removeEventListener("online", callback);
14
+ window.removeEventListener("offline", callback);
15
+ };
16
+ };
17
+
18
+ export const useOnlineStatus = () =>
19
+ useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
20
+ ```
21
+
22
+ ## Should not
23
+
24
+ ```tsx
25
+ export function useOnlineStatus() {
26
+ const [isOnline, setIsOnline] = useState(true);
27
+ useEffect(() => {
28
+ const update = () => setIsOnline(navigator.onLine);
29
+ window.addEventListener("online", update);
30
+ window.addEventListener("offline", update);
31
+ return () => {
32
+ window.removeEventListener("online", update);
33
+ window.removeEventListener("offline", update);
34
+ };
35
+ }, []);
36
+ return isOnline;
37
+ }
38
+ ```
@@ -0,0 +1,26 @@
1
+ ---
2
+ description: An effect that requests data over the network must ignore or abort a response that arrives after its inputs changed or the component unmounted, such as with an `ignore` flag or an AbortController that its cleanup sets, never set state from every response in whatever order they arrive.
3
+ appliesTo: ["a React effect"]
4
+ ---
5
+
6
+ ## Must
7
+
8
+ ```tsx
9
+ useEffect(() => {
10
+ let ignore = false;
11
+ fetchResults(query).then((results) => {
12
+ if (!ignore) setResults(results);
13
+ });
14
+ return () => {
15
+ ignore = true;
16
+ };
17
+ }, [query]);
18
+ ```
19
+
20
+ ## Never
21
+
22
+ ```tsx
23
+ useEffect(() => {
24
+ fetchResults(query).then((results) => setResults(results));
25
+ }, [query]);
26
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: A component must tell its parent about a change, through a callback prop such as `onChange`, in the event handler that makes the change, never from an effect that watches the component's own state.
3
+ appliesTo: ["a React component or hook"]
4
+ ---
5
+
6
+ ## Must
7
+
8
+ ```tsx
9
+ function Toggle({ onChange }: { onChange: (isOn: boolean) => void }) {
10
+ const [isOn, setIsOn] = useState(false);
11
+ const toggle = () => {
12
+ setIsOn(!isOn);
13
+ onChange(!isOn);
14
+ };
15
+ return <button onClick={toggle}>{isOn ? "On" : "Off"}</button>;
16
+ }
17
+ ```
18
+
19
+ ## Never
20
+
21
+ ```tsx
22
+ function Toggle({ onChange }: { onChange: (isOn: boolean) => void }) {
23
+ const [isOn, setIsOn] = useState(false);
24
+ useEffect(() => {
25
+ onChange(isOn);
26
+ }, [isOn, onChange]);
27
+ return <button onClick={() => setIsOn(!isOn)}>{isOn ? "On" : "Off"}</button>;
28
+ }
29
+ ```
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: A Server Component must pass a Client Component only the fields it shows, never a whole database record, a secret, a token, or other data the user must not see, since every prop passed to a Client Component is sent to the browser.
3
+ appliesTo: ["a React Server Component"]
4
+ level: warning
5
+ ---
6
+
7
+ ## Must
8
+
9
+ ```tsx
10
+ export default async function ProfilePage({ params }: { params: { id: string } }) {
11
+ const user = await db.user.findUniqueOrThrow({ where: { id: params.id } });
12
+ return <ProfileCard name={user.name} avatarUrl={user.avatarUrl} />;
13
+ }
14
+ ```
15
+
16
+ ## Never
17
+
18
+ ```tsx
19
+ export default async function ProfilePage({ params }: { params: { id: string } }) {
20
+ const user = await db.user.findUniqueOrThrow({ where: { id: params.id } });
21
+ return <ProfileCard user={user} />;
22
+ }
23
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: A Server Function, one marked `"use server"`, is an endpoint any client can call with any arguments, so it must check that the caller is signed in and allowed to do what it does, and validate its arguments at runtime; it must never trust that only the app's own UI calls it.
3
+ appliesTo: ["a Server Function, marked \"use server\""]
4
+ ---
5
+
6
+ ## Must
7
+
8
+ ```ts
9
+ "use server";
10
+
11
+ export async function deletePost(input: unknown) {
12
+ const session = await getSession();
13
+ if (!session) throw new Error("Not signed in");
14
+ const { postId } = DeletePostInput.parse(input);
15
+ const post = await db.post.findUnique({ where: { id: postId } });
16
+ if (post?.authorId !== session.userId) throw new Error("Not allowed");
17
+ await db.post.delete({ where: { id: postId } });
18
+ }
19
+ ```
20
+
21
+ ## Never
22
+
23
+ ```ts
24
+ "use server";
25
+
26
+ export async function deletePost(postId: string) {
27
+ await db.post.delete({ where: { id: postId } });
28
+ }
29
+ ```
@@ -0,0 +1,17 @@
1
+ ---
2
+ description: State that describes one thing's status, such as whether a request is being sent, was sent, or failed, should be a single state that holds one status at a time, and should not be separate booleans that can say contradictory things at once.
3
+ appliesTo: ["a React component or hook"]
4
+ ---
5
+
6
+ ## Should
7
+
8
+ ```tsx
9
+ const [status, setStatus] = useState<"typing" | "sending" | "sent">("typing");
10
+ ```
11
+
12
+ ## Should not
13
+
14
+ ```tsx
15
+ const [isSending, setIsSending] = useState(false);
16
+ const [isSent, setIsSent] = useState(false);
17
+ ```
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: A component must read a prop, or compute from it, during render, never copy it into state, where the copy stops following the prop when the parent passes a new value.
3
+ appliesTo: ["a React component or hook"]
4
+ excludeIf: ["a prop meant only as the starting value and named for that, such as initialColor or defaultValue"]
5
+ ---
6
+
7
+ ## Must
8
+
9
+ ```tsx
10
+ function Message({ messageColor }: { messageColor: string }) {
11
+ const color = messageColor;
12
+ return <p style={{ color }}>Hello</p>;
13
+ }
14
+ ```
15
+
16
+ ## Never
17
+
18
+ ```tsx
19
+ function Message({ messageColor }: { messageColor: string }) {
20
+ const [color, setColor] = useState(messageColor);
21
+ return <p style={{ color }}>Hello</p>;
22
+ }
23
+ ```
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: State that says which item of a list is selected, or otherwise points into data held elsewhere, should hold the item's id and look the item up during render, and should not hold a copy of the item, which goes stale when the list changes.
3
+ appliesTo: ["a React component or hook"]
4
+ level: warning
5
+ ---
6
+
7
+ ## Should
8
+
9
+ ```tsx
10
+ const [items, setItems] = useState(initialItems);
11
+ const [selectedId, setSelectedId] = useState(initialItems[0].id);
12
+ const selectedItem = items.find((item) => item.id === selectedId);
13
+ ```
14
+
15
+ ## Should not
16
+
17
+ ```tsx
18
+ const [items, setItems] = useState(initialItems);
19
+ const [selectedItem, setSelectedItem] = useState(initialItems[0]);
20
+ ```
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: Code that checks a secret it received, such as a token, an API key, or an HMAC signature, must compare it in constant time, as `crypto.timingSafeEqual` or a library's verify function does, never with `===`, `!==`, or a string method.
3
+ ---
4
+
5
+ ## Must
6
+
7
+ ```ts
8
+ const expected = createHmac("sha256", WEBHOOK_SECRET).update(body).digest();
9
+ const received = Buffer.from(request.headers.get("x-signature") ?? "", "hex");
10
+ if (received.length !== expected.length || !timingSafeEqual(received, expected)) {
11
+ return new Response("Invalid signature", { status: 401 });
12
+ }
13
+ ```
14
+
15
+ ## Never
16
+
17
+ ```ts
18
+ const expected = createHmac("sha256", WEBHOOK_SECRET).update(body).digest("hex");
19
+ if (request.headers.get("x-signature") !== expected) {
20
+ return new Response("Invalid signature", { status: 401 });
21
+ }
22
+ ```
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: A secret, such as a password, an API key, a token, a private key, a session id, or a connection string with credentials, must never be written to a log, put in an error message, or returned in an HTTP response; code must refer to a secret only by its name, its presence, or a non-secret id.
3
+ appliesTo: ["code that writes a log, builds an error message, or builds an HTTP response"]
4
+ excludeIf: ["a masked secret that shows at most a few of its characters"]
5
+ ---
6
+
7
+ ## Must
8
+
9
+ ```ts
10
+ logger.info("calling billing", { keyId: apiKey.id });
11
+ throw new Error(`Could not connect to the database at ${dbHost}`);
12
+ ```
13
+
14
+ ## Never
15
+
16
+ ```ts
17
+ logger.info(`calling billing with key ${apiKey.secret}`);
18
+ throw new Error(`Could not connect to ${process.env.DATABASE_URL}`);
19
+ ```
@@ -0,0 +1,18 @@
1
+ ---
2
+ description: Input from outside the program, such as a request's parameters or body, must reach a shell command only as a separate argument, and a file path only after the resolved path is checked to be inside the directory meant for it; it must never be interpolated into a command string, passed to `eval` or `new Function`, or joined into a path unchecked.
3
+ ---
4
+
5
+ ## Must
6
+
7
+ ```ts
8
+ const file = resolve(UPLOADS, request.params.name);
9
+ if (!file.startsWith(UPLOADS + sep)) return new Response("Not found", { status: 404 });
10
+ const { stdout } = await execFile("ffprobe", ["-show_format", file]);
11
+ ```
12
+
13
+ ## Never
14
+
15
+ ```ts
16
+ const file = join(UPLOADS, request.params.name);
17
+ const { stdout } = await exec(`ffprobe -show_format ${file}`);
18
+ ```
@@ -0,0 +1,16 @@
1
+ ---
2
+ description: A SQL query that includes a value that comes from outside the code, such as user input or a function's argument, must pass it as a bound parameter, or through a query builder or tagged template that binds it, never concatenate or interpolate the value into the query string.
3
+ excludeIf: ["an identifier, such as a table or column name, the code chose from a fixed list"]
4
+ ---
5
+
6
+ ## Must
7
+
8
+ ```ts
9
+ const { rows } = await db.query("SELECT * FROM users WHERE email = $1", [email]);
10
+ ```
11
+
12
+ ## Never
13
+
14
+ ```ts
15
+ const { rows } = await db.query(`SELECT * FROM users WHERE email = '${email}'`);
16
+ ```
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: Awaited calls that do not depend on each other's results should run concurrently, with `Promise.all` or `Promise.allSettled`, and should not be awaited one after another or one per loop iteration.
3
+ excludeIf: ["calls that must run in order, such as writes whose order matters", "calls spaced out on purpose, such as to respect a rate limit"]
4
+ level: warning
5
+ ---
6
+
7
+ ## Should
8
+
9
+ ```ts
10
+ const [user, orders, preferences] = await Promise.all([
11
+ getUser(userId),
12
+ getOrders(userId),
13
+ getPreferences(userId),
14
+ ]);
15
+ ```
16
+
17
+ ## Should not
18
+
19
+ ```ts
20
+ const user = await getUser(userId);
21
+ const orders = await getOrders(userId);
22
+ const preferences = await getPreferences(userId);
23
+ ```
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: An HTTP request, such as a `fetch` or a call through an HTTP client, should carry a timeout or an AbortSignal that ends it, and should not wait without limit.
3
+ excludeIf: ["a call through a client configured with a default timeout", "a call whose signal the caller passes in", "a request from browser code to the app's own API"]
4
+ level: warning
5
+ ---
6
+
7
+ ## Should
8
+
9
+ ```ts
10
+ const response = await fetch(`${BILLING_URL}/invoices/${id}`, {
11
+ signal: AbortSignal.timeout(5_000),
12
+ });
13
+ ```
14
+
15
+ ## Should not
16
+
17
+ ```ts
18
+ const response = await fetch(`${BILLING_URL}/invoices/${id}`);
19
+ ```
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: A function must return a changed copy of an object or array it received as an argument, never mutate the argument, such as by sorting it in place, pushing to it, deleting a key, or assigning to a property, where its caller does not expect the change.
3
+ excludeIf: ["a function whose name says it changes its argument, such as sortInPlace", "an accumulator the function builds, such as a reduce callback's"]
4
+ ---
5
+
6
+ ## Must
7
+
8
+ ```ts
9
+ const byNewest = (posts: ReadonlyArray<Post>) =>
10
+ posts.toSorted((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
11
+ ```
12
+
13
+ ## Never
14
+
15
+ ```ts
16
+ const byNewest = (posts: Post[]) => {
17
+ posts.sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
18
+ return posts;
19
+ };
20
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: A caught error must be handled, rethrown, or returned as a failure the caller can tell apart from success, never discarded or turned into a value that reads as a successful result, such as `null`, an empty array, or a default. Logging an error and carrying on as if nothing failed discards it. Recovering from one specific, expected error, such as a missing file that has a default, handles it.
3
+ ---
4
+
5
+ ## Must
6
+
7
+ ```ts
8
+ async function loadSettings(path: string): Promise<Settings> {
9
+ try {
10
+ return parseSettings(await readFile(path, "utf8"));
11
+ } catch (error) {
12
+ if (isNotFound(error)) return DEFAULT_SETTINGS;
13
+ throw new Error(`Could not load settings from ${path}`, { cause: error });
14
+ }
15
+ }
16
+ ```
17
+
18
+ ## Never
19
+
20
+ ```ts
21
+ async function loadInvoices(customerId: string): Promise<Invoice[]> {
22
+ try {
23
+ return await api.invoices.list({ customerId });
24
+ } catch (error) {
25
+ console.error(error);
26
+ return [];
27
+ }
28
+ }
29
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: A resource the code acquires and must give back, such as a file handle, a lock, a database connection or transaction, a temporary file, or a child process, must be released on every path, including when an error is thrown between acquiring and releasing it, with `finally` or `using`; it must never be released only after the work succeeds. A resource whose owner releases it, such as a framework or a pool's query method, is out of scope.
3
+ ---
4
+
5
+ ## Must
6
+
7
+ ```ts
8
+ const client = await pool.connect();
9
+ try {
10
+ await client.query("BEGIN");
11
+ await client.query("UPDATE accounts SET balance = balance - $1 WHERE id = $2", [amount, from]);
12
+ await client.query("COMMIT");
13
+ } catch (error) {
14
+ await client.query("ROLLBACK");
15
+ throw error;
16
+ } finally {
17
+ client.release();
18
+ }
19
+ ```
20
+
21
+ ## Never
22
+
23
+ ```ts
24
+ const client = await pool.connect();
25
+ await client.query("BEGIN");
26
+ await client.query("UPDATE accounts SET balance = balance - $1 WHERE id = $2", [amount, from]);
27
+ await client.query("COMMIT");
28
+ client.release();
29
+ ```
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: A test must assert what the code under test returned or did, such as its result, what it rendered, or the state it left, never only that it ran without throwing or returned something defined.
3
+ tests: only
4
+ ---
5
+
6
+ ## Must
7
+
8
+ ```ts
9
+ it("applies the discount to each line", () => {
10
+ const total = checkout([{ price: 100, quantity: 2 }], { discount: 0.1 });
11
+ expect(total).toBe(180);
12
+ });
13
+ ```
14
+
15
+ ## Never
16
+
17
+ ```ts
18
+ it("applies the discount to each line", () => {
19
+ const total = checkout([{ price: 100, quantity: 2 }], { discount: 0.1 });
20
+ expect(total).toBeDefined();
21
+ });
22
+ ```
@@ -0,0 +1,41 @@
1
+ ---
2
+ description: A test must set up the state it needs itself, or in a `beforeEach`, and must never rely on state another test left behind, such as a module-level variable, a record another test inserted, or a mock another test configured.
3
+ excludeIf: ["state a suite sets up once for all its tests, in beforeAll or its body, that no test changes"]
4
+ tests: only
5
+ ---
6
+
7
+ ## Must
8
+
9
+ ```ts
10
+ let cart: Cart;
11
+ beforeEach(() => {
12
+ cart = new Cart();
13
+ });
14
+
15
+ it("adds an item", () => {
16
+ cart.add(book);
17
+ expect(cart.items).toEqual([book]);
18
+ });
19
+
20
+ it("totals its items", () => {
21
+ cart.add(book);
22
+ cart.add(pen);
23
+ expect(cart.total()).toBe(book.price + pen.price);
24
+ });
25
+ ```
26
+
27
+ ## Never
28
+
29
+ ```ts
30
+ const cart = new Cart();
31
+
32
+ it("adds an item", () => {
33
+ cart.add(book);
34
+ expect(cart.items).toEqual([book]);
35
+ });
36
+
37
+ it("totals its items", () => {
38
+ cart.add(pen);
39
+ expect(cart.total()).toBe(book.price + pen.price);
40
+ });
41
+ ```
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: A type assertion with `as`, or a non-null assertion with `!`, should claim only what the code around it has already established, and should not be used to make a type error go away. `as const` claims nothing and is out of scope.
3
+ excludeIf: ["data read from outside the program, such as a parsed body or a fetch response"]
4
+ level: warning
5
+ ---
6
+
7
+ ## Should
8
+
9
+ ```ts
10
+ const input = document.querySelector("#email");
11
+ if (!(input instanceof HTMLInputElement)) throw new Error("#email is not an input");
12
+ input.value = email;
13
+ ```
14
+
15
+ ## Should not
16
+
17
+ ```ts
18
+ const input = document.querySelector("#email") as HTMLInputElement;
19
+ input.value = email;
20
+ ```
@@ -0,0 +1,24 @@
1
+ ---
2
+ description: A type that describes the same data as another type, a schema, a function, or a constant should be derived from it, with a utility type such as `Pick`, with `typeof` or `keyof`, or as a schema's inferred type, and should not restate its fields or values by hand.
3
+ level: warning
4
+ ---
5
+
6
+ ## Should
7
+
8
+ ```ts
9
+ const ROLES = ["admin", "member", "guest"] as const;
10
+ type Role = (typeof ROLES)[number];
11
+
12
+ const User = z.object({ id: z.string(), email: z.string(), role: z.enum(ROLES) });
13
+ type User = z.infer<typeof User>;
14
+ ```
15
+
16
+ ## Should not
17
+
18
+ ```ts
19
+ const ROLES = ["admin", "member", "guest"];
20
+ type Role = "admin" | "member" | "guest";
21
+
22
+ const User = z.object({ id: z.string(), email: z.string(), role: z.enum(ROLES) });
23
+ type User = { id: string; email: string; role: Role };
24
+ ```
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: A type whose fields are valid only in some combinations must be a union with one member per state, each carrying only the fields that state has, never one object type of optional fields and flags whose valid combinations only the code that reads them knows. A union of Schema.TaggedClass members is such a union.
3
+ appliesTo: ["a declared type, interface, or schema"]
4
+ ---
5
+
6
+ ## Must
7
+
8
+ ```ts
9
+ type Request<T> =
10
+ | { status: "loading" }
11
+ | { status: "ok"; data: T }
12
+ | { status: "error"; error: Error };
13
+ ```
14
+
15
+ ## Never
16
+
17
+ ```ts
18
+ interface Request<T> {
19
+ loading: boolean;
20
+ data?: T;
21
+ error?: Error;
22
+ }
23
+ ```
@@ -0,0 +1,21 @@
1
+ ---
2
+ description: Data from outside the program, such as a parsed request body, a fetch response, a file's contents, a message, or an environment variable, must be checked at runtime before code gives it a type, never cast with `as` or assigned from `any` to a declared type. A schema's parse or decode, such as Zod's or Effect Schema's, or a type guard that checks each field it claims, is a runtime check.
3
+ appliesTo: ["code that reads data from outside the program"]
4
+ excludeIf: ["a value read through a typed client of the program's own database"]
5
+ ---
6
+
7
+ ## Must
8
+
9
+ ```ts
10
+ const User = z.object({ id: z.string(), email: z.string().email() });
11
+
12
+ const response = await fetch(`/api/users/${id}`);
13
+ const user = User.parse(await response.json());
14
+ ```
15
+
16
+ ## Never
17
+
18
+ ```ts
19
+ const response = await fetch(`/api/users/${id}`);
20
+ const user = (await response.json()) as User;
21
+ ```