@drkmttr/adhere 0.10.0 → 0.12.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 (53) hide show
  1. package/README.md +226 -41
  2. package/package.json +7 -7
  3. package/presets/effect/basics/gen-for-sequencing.md +1 -1
  4. package/presets/effect/data/brand-meaningful-primitives.md +1 -1
  5. package/presets/effect/data/variants-are-tagged-unions.md +1 -1
  6. package/presets/effect/services/dependencies-through-layers.md +1 -1
  7. package/presets/react/effects/clean-up-what-they-start.md +23 -0
  8. package/presets/react/effects/event-logic-in-handlers.md +29 -0
  9. package/presets/react/effects/external-stores-use-sync-external-store.md +38 -0
  10. package/presets/react/effects/fetches-ignore-stale-responses.md +26 -0
  11. package/presets/react/effects/notify-parents-in-handlers.md +29 -0
  12. package/presets/react/server/no-private-data-to-client.md +23 -0
  13. package/presets/react/server/server-functions-authorize-and-validate.md +29 -0
  14. package/presets/react/state/no-contradictory-state.md +17 -0
  15. package/presets/react/state/no-props-copied-into-state.md +23 -0
  16. package/presets/react/state/store-ids-not-copies.md +20 -0
  17. package/presets/security/constant-time-secret-compare.md +22 -0
  18. package/presets/security/no-secrets-in-output.md +19 -0
  19. package/presets/security/no-untrusted-input-in-commands-or-paths.md +18 -0
  20. package/presets/security/parameterized-queries.md +16 -0
  21. package/presets/typescript/async/concurrent-independent-work.md +23 -0
  22. package/presets/typescript/async/network-calls-have-timeouts.md +19 -0
  23. package/presets/typescript/design/no-argument-mutation.md +20 -0
  24. package/presets/typescript/errors/no-swallowed-errors.md +29 -0
  25. package/presets/typescript/errors/release-on-every-path.md +29 -0
  26. package/presets/typescript/testing/assert-outcomes.md +22 -0
  27. package/presets/typescript/testing/independent-tests.md +41 -0
  28. package/presets/typescript/types/assertions-only-when-proven.md +20 -0
  29. package/presets/typescript/types/derive-dont-restate.md +24 -0
  30. package/presets/typescript/types/unrepresentable-invalid-states.md +23 -0
  31. package/presets/typescript/types/validate-external-data.md +21 -0
  32. package/skills/adhere/SKILL.md +4 -3
  33. package/skills/adhere-fix/SKILL.md +10 -4
  34. package/src/cli.ts +85 -4
  35. package/src/config.ts +29 -1
  36. package/src/excerpt.ts +72 -62
  37. package/src/highlight.ts +2 -0
  38. package/src/init.ts +168 -13
  39. package/src/install.ts +147 -0
  40. package/src/markdown.ts +6 -0
  41. package/src/presets.ts +6 -0
  42. package/src/rules.ts +1 -1
  43. package/src/services/AuditCache.ts +31 -5
  44. package/src/services/Jev.http.ts +76 -11
  45. package/src/services/Jev.ts +191 -73
  46. package/src/services/SourceWalker.ts +23 -7
  47. package/src/suppress.ts +13 -7
  48. package/src/version.ts +25 -1
  49. package/src/workflows/audit.ts +96 -26
  50. package/src/workflows/format.ts +51 -11
  51. package/presets/alchemy/data/parameterized-sql.md +0 -17
  52. package/presets/alchemy/secrets/never-logged-returned-or-output.md +0 -19
  53. package/presets/effect/basics/external-calls-are-resilient.md +0 -21
@@ -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
+ ```
@@ -94,9 +94,10 @@ Rules for the rule:
94
94
  - Jev reads code without its comments, except comments that say `@adhere`. A
95
95
  rule about comments, such as doc comments on exports, needs
96
96
  `includeComments: true` in the config.
97
- - Rules skip tests: `.test.ts` and `.spec.ts` files, and files under `test/`,
98
- `tests/`, `__tests__/`, or `fixtures/`. A rule about tests says `tests: only`
99
- in its front matter, and one that holds in tests as well `tests: include`.
97
+ - Rules skip tests: `.test` and `.spec` files, such as `a.test.ts` and
98
+ `Button.spec.tsx`, and files under `test/`, `tests/`, `__tests__/`, or
99
+ `fixtures/`. A rule about tests says `tests: only` in its front matter, and
100
+ one that holds in tests as well `tests: include`.
100
101
  - A nit, or a rule that tends to flag code wrongly, says `level: warning` in
101
102
  its front matter: its findings show in amber and do not fail CI.
102
103
  - `threshold` is optional. Start without it; set it in step 5 if needed.
@@ -7,8 +7,8 @@ description: Verify and fix the findings `adhere lint` reports. Decide whether e
7
7
 
8
8
  adhere is a linter for rules a normal linter cannot check. Jev, TypeSafe AI's
9
9
  model, reads each file against each rule and answers with the probability that
10
- the file breaks it; adhere reports each rule above a threshold, at the line Jev
11
- points to. A finding is a judgment, not a proof. On one rule adhere's authors
10
+ the file breaks it; adhere reports each rule above a threshold, in the section
11
+ of the file Jev points to. A finding is a judgment, not a proof. On one rule adhere's authors
12
12
  checked against the live API, only 1 of its 20 findings was a real violation.
13
13
  Verify every finding before you change code for it: fix the real ones, and
14
14
  report the false ones with the evidence.
@@ -34,7 +34,13 @@ Each finding shows:
34
34
  - the rule id, a preset's with its preset first, as in
35
35
  `alchemy/providers/idempotent-delete`, Jev's probability, and the rule's
36
36
  description;
37
- - the line Jev points at, underlined, with the code around it;
37
+ - the section of the file Jev points at, a run of whole statements of about 30
38
+ lines, with the lines it names underlined, or marked with a bar when they
39
+ are several: usually where the violation starts, which may run on past them;
40
+ - sometimes `warning: this file may not show enough to check this rule`: the
41
+ rule may turn on another file, a library, or a service. Most of these are
42
+ false; read what the code relies on before fixing it. Its `help:` suggests
43
+ an `@adhere` note, which follows the same rule as a suppression (see below);
38
44
  - `hint:`, the code the rule wants, or `never:` for a rule that only shows code
39
45
  to avoid.
40
46
 
@@ -57,7 +63,7 @@ the code under `## Must` and `## Never`, and anything it puts out of scope.
57
63
 
58
64
  Read the code as a reviewer would, not only the excerpt:
59
65
 
60
- - **The whole unit.** Read the function, handler, or class the line is in,
66
+ - **The whole unit.** Read the function, handler, or class the section holds,
61
67
  and what calls it.
62
68
  - **What it calls.** Follow helpers into other files. A helper can already do
63
69
  what the rule asks, such as a delete that looks unguarded calling a client