peer-ai-standards 1.0.0-next.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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/core/ai-features.d.ts +65 -0
- package/dist/core/ai-features.js +113 -0
- package/dist/core/api-design.d.ts +41 -0
- package/dist/core/api-design.js +91 -0
- package/dist/core/architecture.d.ts +41 -0
- package/dist/core/architecture.js +93 -0
- package/dist/core/backend.d.ts +21 -0
- package/dist/core/backend.js +25 -0
- package/dist/core/code-quality.d.ts +91 -0
- package/dist/core/code-quality.js +179 -0
- package/dist/core/data.d.ts +45 -0
- package/dist/core/data.js +59 -0
- package/dist/core/delivery.d.ts +153 -0
- package/dist/core/delivery.js +149 -0
- package/dist/core/design-accessibility.d.ts +124 -0
- package/dist/core/design-accessibility.js +199 -0
- package/dist/core/frontend.d.ts +77 -0
- package/dist/core/frontend.js +109 -0
- package/dist/core/mobile.d.ts +46 -0
- package/dist/core/mobile.js +83 -0
- package/dist/core/money.d.ts +81 -0
- package/dist/core/money.js +135 -0
- package/dist/core/operations.d.ts +108 -0
- package/dist/core/operations.js +177 -0
- package/dist/core/performance.d.ts +53 -0
- package/dist/core/performance.js +85 -0
- package/dist/core/privacy-compliance.d.ts +68 -0
- package/dist/core/privacy-compliance.js +77 -0
- package/dist/core/reliability.d.ts +67 -0
- package/dist/core/reliability.js +105 -0
- package/dist/core/requirements.d.ts +41 -0
- package/dist/core/requirements.js +58 -0
- package/dist/core/safety-critical.d.ts +41 -0
- package/dist/core/safety-critical.js +71 -0
- package/dist/core/security.d.ts +281 -0
- package/dist/core/security.js +427 -0
- package/dist/core/system-design.d.ts +31 -0
- package/dist/core/system-design.js +69 -0
- package/dist/core/testing.d.ts +51 -0
- package/dist/core/testing.js +124 -0
- package/dist/domains.d.ts +6 -0
- package/dist/domains.js +53 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +118 -0
- package/dist/profile.d.ts +262 -0
- package/dist/profile.js +269 -0
- package/dist/profiles/express.d.ts +2 -0
- package/dist/profiles/express.js +93 -0
- package/dist/profiles/fastapi.d.ts +2 -0
- package/dist/profiles/fastapi.js +104 -0
- package/dist/profiles/fastify.d.ts +2 -0
- package/dist/profiles/fastify.js +56 -0
- package/dist/profiles/github-actions.d.ts +2 -0
- package/dist/profiles/github-actions.js +151 -0
- package/dist/profiles/nestjs.d.ts +2 -0
- package/dist/profiles/nestjs.js +77 -0
- package/dist/profiles/next.d.ts +2 -0
- package/dist/profiles/next.js +73 -0
- package/dist/profiles/node.d.ts +2 -0
- package/dist/profiles/node.js +79 -0
- package/dist/profiles/python.d.ts +2 -0
- package/dist/profiles/python.js +232 -0
- package/dist/profiles/react-native.d.ts +2 -0
- package/dist/profiles/react-native.js +133 -0
- package/dist/profiles/react.d.ts +2 -0
- package/dist/profiles/react.js +180 -0
- package/dist/profiles/typescript.d.ts +2 -0
- package/dist/profiles/typescript.js +214 -0
- package/dist/rule.d.ts +72 -0
- package/dist/rule.js +39 -0
- package/package.json +39 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// React Native, with or without Expo. It builds on React, whose web-only rules don't match native
|
|
2
|
+
// components. Examples are from a made-up bicycle repair booking service.
|
|
3
|
+
export const reactNative = {
|
|
4
|
+
id: "react-native",
|
|
5
|
+
name: "React Native",
|
|
6
|
+
prefix: "RN",
|
|
7
|
+
about: "Phone apps in React Native, with or without Expo: text that follows the person's size, nothing written to the device log, secrets in the platform's secure store, and the checks a phone app needs before it's trusted with people's data. It builds on the React profile.",
|
|
8
|
+
stacks: ["react-native", "expo"],
|
|
9
|
+
extends: ["react"],
|
|
10
|
+
rules: [
|
|
11
|
+
{
|
|
12
|
+
id: "RN-01",
|
|
13
|
+
title: "Text follows the person's text size",
|
|
14
|
+
rule: "Text isn't stopped from scaling with `allowFontScaling={false}`. A layout that breaks at large sizes is fixed, such as by wrapping or scrolling, not by ignoring the setting.",
|
|
15
|
+
why: "People who set a larger text size need it to read at all; an app that ignores it is unusable to them.",
|
|
16
|
+
ask: "Does this change stop any text from following the person's text size?",
|
|
17
|
+
stage: "mvp",
|
|
18
|
+
check: "auto",
|
|
19
|
+
severity: "medium",
|
|
20
|
+
carries: "DES-14",
|
|
21
|
+
enforcer: {
|
|
22
|
+
tool: "eslint",
|
|
23
|
+
rule: "no-restricted-syntax",
|
|
24
|
+
options: [
|
|
25
|
+
{
|
|
26
|
+
selector: "JSXAttribute[name.name='allowFontScaling'][value.expression.value=false]",
|
|
27
|
+
message: "RN-01: text follows the person's text size. Fix the layout instead of turning scaling off.",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
examples: {
|
|
32
|
+
file: "example.tsx",
|
|
33
|
+
fails: 'import { Text } from "react-native";\nexport function Price() {\n return <Text allowFontScaling={false}>£45 service</Text>;\n}\n',
|
|
34
|
+
passes: 'import { Text } from "react-native";\nexport function Price() {\n return <Text>£45 service</Text>;\n}\n',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "RN-02",
|
|
39
|
+
title: "Nothing is written to the device log",
|
|
40
|
+
rule: "The app doesn't call `console`. What needs recording goes through the project's logger, which drops personal data and sends nothing from release builds.",
|
|
41
|
+
why: "On a phone, the console goes to the device log, which crash tools collect and anyone with the phone on a cable can read.",
|
|
42
|
+
ask: "Does this change call console?",
|
|
43
|
+
stage: "mvp",
|
|
44
|
+
check: "auto",
|
|
45
|
+
severity: "medium",
|
|
46
|
+
carries: "MOB-02",
|
|
47
|
+
enforcer: { tool: "eslint", rule: "no-console" },
|
|
48
|
+
examples: {
|
|
49
|
+
file: "example.ts",
|
|
50
|
+
fails: "export function signIn(email: string): void {\n console.log(`signing in ${email}`);\n}\n",
|
|
51
|
+
passes: 'export function signIn(email: string, log: (event: string) => void): void {\n log(email.length > 0 ? "sign-in started" : "sign-in without an email");\n}\n',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "RN-03",
|
|
56
|
+
title: "Tokens and personal data are kept in secure storage",
|
|
57
|
+
rule: "Session tokens, keys and personal data are kept in the platform's secure store, such as `expo-secure-store` or the Keychain and Keystore, never in AsyncStorage or a plain file.",
|
|
58
|
+
why: "AsyncStorage is a plain file on the device, readable from a backup or a rooted phone.",
|
|
59
|
+
ask: "Does this change keep a token or personal data outside secure storage?",
|
|
60
|
+
stage: "mvp",
|
|
61
|
+
check: "ai-review",
|
|
62
|
+
severity: "high",
|
|
63
|
+
carries: "MOB-01",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "RN-04",
|
|
67
|
+
title: "A web view opens only your own pages",
|
|
68
|
+
rule: "A `WebView` loads only the project's own pages: `originWhitelist` lists them, links elsewhere open in the system browser, and JavaScript is on only where the page needs it.",
|
|
69
|
+
why: "A web view that follows any link runs someone else's page inside the app, with whatever the app gave the web view.",
|
|
70
|
+
ask: "Can any web view in this change load a page the project doesn't own?",
|
|
71
|
+
stage: "mvp",
|
|
72
|
+
check: "ai-review",
|
|
73
|
+
severity: "high",
|
|
74
|
+
carries: "MOB-03",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "RN-05",
|
|
78
|
+
title: "A link into the app is checked before it's used",
|
|
79
|
+
rule: "Every value a deep link carries, such as an id or a URL, is checked before the app acts on it, like any other input from outside.",
|
|
80
|
+
why: "Anyone can send a link that opens the app. A link that's trusted can open a page it shouldn't, or act as the person.",
|
|
81
|
+
ask: "Does this change act on a deep link's values without checking them?",
|
|
82
|
+
stage: "mvp",
|
|
83
|
+
check: "ai-review",
|
|
84
|
+
severity: "high",
|
|
85
|
+
carries: "MOB-04",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "RN-06",
|
|
89
|
+
title: "Long lists use a list component",
|
|
90
|
+
rule: "A list that can grow long is a `FlatList`, `SectionList` or another virtualised list, not a `ScrollView` of mapped items.",
|
|
91
|
+
why: "A ScrollView draws every item at once, so the screen gets slower and uses more memory with every item.",
|
|
92
|
+
ask: "Does this change draw a list that can grow long inside a ScrollView?",
|
|
93
|
+
stage: "mvp",
|
|
94
|
+
check: "ai-review",
|
|
95
|
+
severity: "low",
|
|
96
|
+
carries: "PERF-05",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "RN-07",
|
|
100
|
+
title: "Touch targets are at least {value} points",
|
|
101
|
+
rule: "Everything a person taps is at least {value} by {value} points, using `hitSlop` where the visible part is smaller.",
|
|
102
|
+
why: "A small target is missed by people with less precise hands, and by everyone on a moving bus.",
|
|
103
|
+
ask: "Is anything tappable in this change smaller than {value} points?",
|
|
104
|
+
stage: "mvp",
|
|
105
|
+
check: "ai-review",
|
|
106
|
+
severity: "low",
|
|
107
|
+
carries: "DES-15",
|
|
108
|
+
default: { value: 44, unit: "points" },
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "RN-08",
|
|
112
|
+
title: "Permissions are asked for when they're needed",
|
|
113
|
+
rule: "The app asks for a permission, such as location or the camera, when the person starts the feature that needs it, and at the least level that works, such as while in use.",
|
|
114
|
+
why: "A permission asked for at launch is refused without being understood, and a wider one than needed is a risk the person didn't agree to.",
|
|
115
|
+
ask: "Does this change ask for a permission before it's needed, or at a wider level than it needs?",
|
|
116
|
+
stage: "mvp",
|
|
117
|
+
check: "ai-review",
|
|
118
|
+
severity: "medium",
|
|
119
|
+
carries: "MOB-05",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "RN-09",
|
|
123
|
+
title: "The app can require an update",
|
|
124
|
+
rule: "At start-up, the app asks the server for the lowest version it may run, and asks the person to update when it's below it, such as with Expo Updates or a store check.",
|
|
125
|
+
why: "An installed app can't be recalled. Without a way to require an update, a version with a security hole stays in use.",
|
|
126
|
+
ask: "Can the server require people to update this app?",
|
|
127
|
+
stage: "production",
|
|
128
|
+
check: "ai-review",
|
|
129
|
+
severity: "high",
|
|
130
|
+
carries: "MOB-06",
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// React on the web, and the base for React Native and Next.js. Examples are from a made-up
|
|
2
|
+
// bicycle repair booking service.
|
|
3
|
+
/** A component file `lines` lines long, of one-line components. */
|
|
4
|
+
function components(lines) {
|
|
5
|
+
return `${Array.from({ length: lines }, (_, i) => `export const Bay${String(i)} = () => <li>Bay ${String(i)}</li>;`).join("\n")}\n`;
|
|
6
|
+
}
|
|
7
|
+
export const react = {
|
|
8
|
+
id: "react",
|
|
9
|
+
name: "React",
|
|
10
|
+
prefix: "REACT",
|
|
11
|
+
about: "React components on the web: hooks that behave, state that isn't copied, components that stay small, no HTML from outside, and the accessibility checks a linter can make. React Native and Next.js build on it.",
|
|
12
|
+
stacks: ["react"],
|
|
13
|
+
extends: ["typescript"],
|
|
14
|
+
rules: [
|
|
15
|
+
{
|
|
16
|
+
id: "REACT-01",
|
|
17
|
+
title: "Hooks are called the same way on every render",
|
|
18
|
+
rule: "Hooks are called at the top of a component or another hook, never inside a condition, a loop or a callback.",
|
|
19
|
+
why: "React matches each hook to its state by the order of the calls. A hook called conditionally gets another hook's state, a bug that appears only on some renders.",
|
|
20
|
+
ask: "Is any hook in this change called conditionally, in a loop or in a callback?",
|
|
21
|
+
stage: "prototype",
|
|
22
|
+
check: "auto",
|
|
23
|
+
severity: "high",
|
|
24
|
+
carries: "CODE-16",
|
|
25
|
+
enforcer: { tool: "eslint", rule: "react-hooks/rules-of-hooks" },
|
|
26
|
+
examples: {
|
|
27
|
+
file: "example.tsx",
|
|
28
|
+
fails: 'import { useState } from "react";\nexport function Notes({ open }: { open: boolean }) {\n if (open) {\n const [note] = useState("");\n return <p>{note}</p>;\n }\n return null;\n}\n',
|
|
29
|
+
passes: 'import { useState } from "react";\nexport function Notes({ open }: { open: boolean }) {\n const [note] = useState("");\n return open ? <p>{note}</p> : null;\n}\n',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "REACT-02",
|
|
34
|
+
title: "State isn't copied from other data by an effect",
|
|
35
|
+
rule: "A value that follows from props or state is worked out during render. An effect never copies it into state of its own.",
|
|
36
|
+
why: "A copy is out of date for a render, and wrong for good when the effect misses a change.",
|
|
37
|
+
ask: "Does any effect in this change set state from other props or state?",
|
|
38
|
+
stage: "prototype",
|
|
39
|
+
check: "auto",
|
|
40
|
+
severity: "medium",
|
|
41
|
+
carries: "FE-02",
|
|
42
|
+
enforcer: { tool: "eslint", rule: "react-hooks/no-deriving-state-in-effects" },
|
|
43
|
+
examples: {
|
|
44
|
+
file: "example.tsx",
|
|
45
|
+
fails: 'import { useEffect, useState } from "react";\nexport function Total({ labour, parts }: { labour: number; parts: number }) {\n const [total, setTotal] = useState(0);\n useEffect(() => {\n setTotal(labour + parts);\n }, [labour, parts]);\n return <p>{total}</p>;\n}\n',
|
|
46
|
+
passes: "export function Total({ labour, parts }: { labour: number; parts: number }) {\n const total = labour + parts;\n return <p>{total}</p>;\n}\n",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "REACT-03",
|
|
51
|
+
title: "A component file longer than {value} lines prompts a question",
|
|
52
|
+
rule: "A component file over {value} lines, not counting blank lines and comments, fails the lint, so someone asks what could move out: a hook, a child component, or plain logic. Where nothing should, a disable comment at the top says why.",
|
|
53
|
+
why: "A component that keeps growing usually holds several jobs, and every change to one re-renders and re-tests the rest.",
|
|
54
|
+
ask: "Has any component file grown past {value} lines, and does each that stays say why?",
|
|
55
|
+
stage: "mvp",
|
|
56
|
+
check: "auto",
|
|
57
|
+
severity: "low",
|
|
58
|
+
carries: "CODE-10",
|
|
59
|
+
default: { value: 150, unit: "lines" },
|
|
60
|
+
enforcer: {
|
|
61
|
+
tool: "eslint",
|
|
62
|
+
rule: "max-lines",
|
|
63
|
+
options: [{ max: "$value", skipBlankLines: true, skipComments: true }],
|
|
64
|
+
files: ["**/*.tsx", "**/*.jsx"],
|
|
65
|
+
ignores: ["**/*.test.*", "**/*.spec.*", "**/*.stories.*"],
|
|
66
|
+
},
|
|
67
|
+
examples: {
|
|
68
|
+
file: "example.tsx",
|
|
69
|
+
fails: (value) => components(Number(value) + 1),
|
|
70
|
+
passes: (value) => components(Number(value)),
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "REACT-04",
|
|
75
|
+
title: "No HTML from outside is put into the page",
|
|
76
|
+
rule: "`dangerouslySetInnerHTML` isn't used. Content from outside, such as a supplier's description, is shown as text, or through a sanitiser whose use a reviewer can see.",
|
|
77
|
+
why: "HTML from outside can carry a script, which then runs as the person viewing the page.",
|
|
78
|
+
ask: "Does this change use dangerouslySetInnerHTML?",
|
|
79
|
+
stage: "prototype",
|
|
80
|
+
check: "auto",
|
|
81
|
+
severity: "high",
|
|
82
|
+
carries: "SEC-08",
|
|
83
|
+
enforcer: { tool: "eslint", rule: "react-dom/no-dangerously-set-innerhtml" },
|
|
84
|
+
examples: {
|
|
85
|
+
file: "example.tsx",
|
|
86
|
+
fails: "export function PartNotes({ html }: { html: string }) {\n return <div dangerouslySetInnerHTML={{ __html: html }} />;\n}\n",
|
|
87
|
+
passes: "export function PartNotes({ text }: { text: string }) {\n return <div>{text}</div>;\n}\n",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "REACT-05",
|
|
92
|
+
title: "Every control has a name a screen reader can say",
|
|
93
|
+
rule: "Every button, link and other control has a name a screen reader can announce: its text, or an `aria-label` when it shows only an icon.",
|
|
94
|
+
why: 'A button that shows only an icon is announced as "button", with nothing to say what it does.',
|
|
95
|
+
ask: "Does every control in this change have a name, including those that show only an icon?",
|
|
96
|
+
stage: "prototype",
|
|
97
|
+
check: "auto",
|
|
98
|
+
severity: "medium",
|
|
99
|
+
carries: "DES-08",
|
|
100
|
+
// Form fields are REACT-06's: a field inside its label is named, which this rule can't see.
|
|
101
|
+
enforcer: {
|
|
102
|
+
tool: "eslint",
|
|
103
|
+
rule: "jsx-a11y/control-has-associated-label",
|
|
104
|
+
options: [{ ignoreElements: ["input", "select", "textarea"] }],
|
|
105
|
+
},
|
|
106
|
+
examples: {
|
|
107
|
+
file: "example.tsx",
|
|
108
|
+
fails: 'export function Close({ onClose }: { onClose: () => void }) {\n return (\n <button type="button" onClick={onClose}>\n <svg aria-hidden="true" />\n </button>\n );\n}\n',
|
|
109
|
+
passes: 'export function Close({ onClose }: { onClose: () => void }) {\n return (\n <button type="button" aria-label="Close" onClick={onClose}>\n <svg aria-hidden="true" />\n </button>\n );\n}\n',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "REACT-06",
|
|
114
|
+
title: "Every label is tied to its field",
|
|
115
|
+
rule: "Every `label` is tied to its field, by wrapping it or by `htmlFor`, so the field is named when it's reached.",
|
|
116
|
+
why: "A label that sits beside a field without being tied to it isn't read out when the field is reached, so a screen reader announces an unnamed field.",
|
|
117
|
+
ask: "Is every label in this change tied to its field?",
|
|
118
|
+
stage: "prototype",
|
|
119
|
+
check: "auto",
|
|
120
|
+
severity: "medium",
|
|
121
|
+
carries: "DES-09",
|
|
122
|
+
enforcer: { tool: "eslint", rule: "jsx-a11y/label-has-associated-control", options: [{ assert: "either" }] },
|
|
123
|
+
examples: {
|
|
124
|
+
file: "example.tsx",
|
|
125
|
+
fails: 'export function Serial() {\n return (\n <div>\n <label>Frame number</label>\n <input id="serial" />\n </div>\n );\n}\n',
|
|
126
|
+
passes: 'export function Serial() {\n return (\n <label>\n Frame number\n <input id="serial" />\n </label>\n );\n}\n',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "REACT-07",
|
|
131
|
+
title: "Anything clickable works with a keyboard",
|
|
132
|
+
rule: "An element with `onClick` also answers the keyboard. Better, it's a `button` or a link, which do both.",
|
|
133
|
+
why: "A clickable `div` can't be reached or used by someone who doesn't use a mouse.",
|
|
134
|
+
ask: "Does anything clickable in this change ignore the keyboard?",
|
|
135
|
+
stage: "mvp",
|
|
136
|
+
check: "auto",
|
|
137
|
+
severity: "medium",
|
|
138
|
+
carries: "DES-10",
|
|
139
|
+
enforcer: { tool: "eslint", rule: "jsx-a11y/click-events-have-key-events" },
|
|
140
|
+
examples: {
|
|
141
|
+
file: "example.tsx",
|
|
142
|
+
fails: 'export function Cancel({ onCancel }: { onCancel: () => void }) {\n return <div role="button" tabIndex={0} onClick={onCancel}>Cancel booking</div>;\n}\n',
|
|
143
|
+
passes: 'export function Cancel({ onCancel }: { onCancel: () => void }) {\n return <button type="button" onClick={onCancel}>Cancel booking</button>;\n}\n',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "REACT-08",
|
|
148
|
+
title: "Server data comes through a query cache, not an effect",
|
|
149
|
+
rule: "In a component that runs in the browser, data from the server is read through the project's query cache, such as TanStack Query or SWR, not fetched in `useEffect` and kept in `useState`. Components that render on the server fetch there instead.",
|
|
150
|
+
why: "Fetching in an effect repeats the same request in every component that needs it, races when inputs change, and leaves each copy stale in its own way.",
|
|
151
|
+
ask: "Does this change fetch server data in an effect and keep it in component state?",
|
|
152
|
+
stage: "mvp",
|
|
153
|
+
check: "ai-review",
|
|
154
|
+
severity: "medium",
|
|
155
|
+
carries: "FE-01",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "REACT-09",
|
|
159
|
+
title: "Long lists draw only what's on screen",
|
|
160
|
+
rule: "A list that can grow past a screenful, such as every past booking, is virtualised, so only the rows in view are drawn.",
|
|
161
|
+
why: "Drawing every row makes the screen slower with every row the business adds.",
|
|
162
|
+
ask: "Does this change draw a list that can grow long without virtualising it?",
|
|
163
|
+
stage: "mvp",
|
|
164
|
+
check: "ai-review",
|
|
165
|
+
severity: "low",
|
|
166
|
+
carries: "PERF-05",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: "REACT-10",
|
|
170
|
+
title: "An effect stops what it starts",
|
|
171
|
+
rule: "An effect that starts something, such as a timer, a subscription or a request, returns a cleanup that stops it.",
|
|
172
|
+
why: "Without cleanup, work carries on after the component is gone, and sets state nobody will see or calls the server twice.",
|
|
173
|
+
ask: "Does every effect in this change stop what it starts?",
|
|
174
|
+
stage: "mvp",
|
|
175
|
+
check: "ai-review",
|
|
176
|
+
severity: "medium",
|
|
177
|
+
carries: "PERF-06",
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// TypeScript, in any runtime. Examples are from a made-up bicycle repair booking service.
|
|
2
|
+
const count = (value) => Number(value);
|
|
3
|
+
/** A function nested `depth` levels deep. */
|
|
4
|
+
function nested(depth) {
|
|
5
|
+
const open = Array.from({ length: depth }, (_, i) => `${" ".repeat(i + 1)}if (slots[${String(i)}] !== undefined) {`);
|
|
6
|
+
const close = Array.from({ length: depth }, (_, i) => `${" ".repeat(depth - i)}}`);
|
|
7
|
+
return [
|
|
8
|
+
"export function firstFree(slots: (number | undefined)[]): number {",
|
|
9
|
+
...open,
|
|
10
|
+
`${" ".repeat(depth + 1)}return 1;`,
|
|
11
|
+
...close,
|
|
12
|
+
" return 0;",
|
|
13
|
+
"}",
|
|
14
|
+
"",
|
|
15
|
+
].join("\n");
|
|
16
|
+
}
|
|
17
|
+
/** A function `lines` lines long, from its first line to its closing brace. */
|
|
18
|
+
function longFunction(lines) {
|
|
19
|
+
const body = Array.from({ length: lines - 4 }, (_, i) => ` total += ${String(i)};`);
|
|
20
|
+
return ["export function repairTotal(): number {", " let total = 0;", ...body, " return total;", "}", ""].join("\n");
|
|
21
|
+
}
|
|
22
|
+
/** A module `lines` lines long. */
|
|
23
|
+
function longModule(lines) {
|
|
24
|
+
return `${Array.from({ length: lines }, (_, i) => `export const bay${String(i)} = ${String(i)};`).join("\n")}\n`;
|
|
25
|
+
}
|
|
26
|
+
export const typescript = {
|
|
27
|
+
id: "typescript",
|
|
28
|
+
name: "TypeScript",
|
|
29
|
+
prefix: "TS",
|
|
30
|
+
about: "TypeScript in any runtime: the compiler at its strictest, no escape hatches from the types, and errors that can't disappear. The React, Node and backend profiles build on it.",
|
|
31
|
+
stacks: ["typescript"],
|
|
32
|
+
rules: [
|
|
33
|
+
{
|
|
34
|
+
id: "TS-01",
|
|
35
|
+
title: "The compiler is strict",
|
|
36
|
+
rule: 'Every tsconfig says `"strict": true`, so nothing is typed `any` without saying so, `null` is checked, and a caught error is `unknown` until it\'s narrowed. TypeScript 6 turns it on by default; saying so keeps it on with older compilers, and for anyone reading the file.',
|
|
37
|
+
why: "Each check strict mode turns off is a class of bug the compiler would have caught.",
|
|
38
|
+
ask: "Does every part's tsconfig say strict is on?",
|
|
39
|
+
stage: "prototype",
|
|
40
|
+
check: "auto",
|
|
41
|
+
severity: "medium",
|
|
42
|
+
carries: "CODE-14",
|
|
43
|
+
enforcer: { tool: "typescript", option: "strict", value: true },
|
|
44
|
+
examples: {
|
|
45
|
+
file: "example.ts",
|
|
46
|
+
fails: "export function deposit(price) {\n return price / 5;\n}\n",
|
|
47
|
+
passes: "export function deposit(price: number): number {\n return price / 5;\n}\n",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "TS-02",
|
|
52
|
+
title: "No `any` where a real type exists",
|
|
53
|
+
rule: "`any` isn't written. Data of unknown shape, such as a response from another service, is `unknown` and checked before use.",
|
|
54
|
+
why: "`any` turns the type checker off for everything it touches, and the error surfaces far from its cause.",
|
|
55
|
+
ask: "Does this change write `any`?",
|
|
56
|
+
stage: "prototype",
|
|
57
|
+
check: "auto",
|
|
58
|
+
severity: "medium",
|
|
59
|
+
carries: "CODE-14",
|
|
60
|
+
enforcer: { tool: "eslint", rule: "@typescript-eslint/no-explicit-any" },
|
|
61
|
+
examples: {
|
|
62
|
+
file: "example.ts",
|
|
63
|
+
fails: "export function bikeName(input: any): string {\n return input.name;\n}\n",
|
|
64
|
+
passes: 'export function bikeName(input: unknown): string {\n return typeof input === "object" && input !== null && "name" in input && typeof input.name === "string" ? input.name : "";\n}\n',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "TS-03",
|
|
69
|
+
title: "No `!` to tell the compiler a value is there",
|
|
70
|
+
rule: "A value that may be missing is checked, not asserted with `!`. Where it truly can't be missing, the types say so.",
|
|
71
|
+
why: "`!` is a promise the compiler can't check. When it's wrong, the crash comes at run time instead of a compile error.",
|
|
72
|
+
ask: "Does this change assert a value is present with `!`?",
|
|
73
|
+
stage: "mvp",
|
|
74
|
+
check: "auto",
|
|
75
|
+
severity: "low",
|
|
76
|
+
carries: "CODE-14",
|
|
77
|
+
enforcer: { tool: "eslint", rule: "@typescript-eslint/no-non-null-assertion" },
|
|
78
|
+
examples: {
|
|
79
|
+
file: "example.ts",
|
|
80
|
+
fails: "export function mechanic(names: Map<string, string>, id: string): string {\n return names.get(id)!;\n}\n",
|
|
81
|
+
passes: 'export function mechanic(names: Map<string, string>, id: string): string {\n return names.get(id) ?? "unassigned";\n}\n',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "TS-04",
|
|
86
|
+
title: "No empty catch",
|
|
87
|
+
rule: "A `catch` block is never empty. Where ignoring an error is right, the block logs it and says why.",
|
|
88
|
+
why: "An empty catch turns a failure into silence, and the bug report says only that something didn't happen.",
|
|
89
|
+
ask: "Does this change leave a catch block empty?",
|
|
90
|
+
stage: "prototype",
|
|
91
|
+
check: "auto",
|
|
92
|
+
severity: "high",
|
|
93
|
+
carries: "CODE-11",
|
|
94
|
+
enforcer: { tool: "eslint", rule: "no-empty", options: [{ allowEmptyCatch: false }] },
|
|
95
|
+
examples: {
|
|
96
|
+
file: "example.ts",
|
|
97
|
+
fails: "export function remind(send: () => void): void {\n try {\n send();\n } catch {\n }\n}\n",
|
|
98
|
+
passes: "export function remind(send: () => void, log: (message: string, error: unknown) => void): void {\n try {\n send();\n } catch (error) {\n // A reminder that fails mustn't cancel the booking; it's retried by the next run.\n log(\"reminder not sent\", error);\n }\n}\n",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "TS-05",
|
|
103
|
+
title: "Every promise is awaited or handled",
|
|
104
|
+
rule: "A promise is awaited, returned, or given a handler for its failure. One that's started and forgotten is marked with `void` and a comment saying why.",
|
|
105
|
+
why: "A forgotten promise's failure goes nowhere: the work silently doesn't happen, or crashes the process later.",
|
|
106
|
+
ask: "Does this change start a promise and not handle its failure?",
|
|
107
|
+
stage: "mvp",
|
|
108
|
+
check: "auto",
|
|
109
|
+
severity: "high",
|
|
110
|
+
carries: "CODE-11",
|
|
111
|
+
enforcer: { tool: "eslint", rule: "@typescript-eslint/no-floating-promises", typed: true },
|
|
112
|
+
examples: {
|
|
113
|
+
file: "example.ts",
|
|
114
|
+
fails: "export async function book(save: () => Promise<void>, notify: () => Promise<void>): Promise<void> {\n await save();\n notify();\n}\n",
|
|
115
|
+
passes: "export async function book(save: () => Promise<void>, notify: () => Promise<void>): Promise<void> {\n await save();\n await notify();\n}\n",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "TS-06",
|
|
120
|
+
title: "Nesting stays {value} levels deep or less",
|
|
121
|
+
rule: "Blocks nest at most {value} levels deep in a function. Deeper code handles the simple cases first and returns.",
|
|
122
|
+
why: "Each level is one more condition to hold in mind while reading the line inside it.",
|
|
123
|
+
ask: "Does any function in this change nest blocks more than {value} deep?",
|
|
124
|
+
stage: "mvp",
|
|
125
|
+
check: "auto",
|
|
126
|
+
severity: "low",
|
|
127
|
+
carries: "CODE-09",
|
|
128
|
+
default: { value: 3, unit: "levels" },
|
|
129
|
+
enforcer: { tool: "eslint", rule: "max-depth", options: [{ max: "$value" }] },
|
|
130
|
+
examples: {
|
|
131
|
+
file: "example.ts",
|
|
132
|
+
fails: (value) => nested(count(value) + 1),
|
|
133
|
+
passes: (value) => nested(count(value)),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "TS-07",
|
|
138
|
+
title: "A function longer than {value} lines prompts a question",
|
|
139
|
+
rule: "A function over {value} lines, not counting blank lines and comments, fails the lint, so someone asks whether it does two things. Where it doesn't, a disable comment beside it says why.",
|
|
140
|
+
why: "Long functions usually hide a second job, and the second job is what the next change breaks.",
|
|
141
|
+
ask: "Has any function grown past {value} lines, and does each that stays say why?",
|
|
142
|
+
stage: "mvp",
|
|
143
|
+
check: "auto",
|
|
144
|
+
severity: "low",
|
|
145
|
+
carries: "CODE-10",
|
|
146
|
+
default: { value: 60, unit: "lines" },
|
|
147
|
+
enforcer: {
|
|
148
|
+
tool: "eslint",
|
|
149
|
+
rule: "max-lines-per-function",
|
|
150
|
+
options: [{ max: "$value", skipBlankLines: true, skipComments: true }],
|
|
151
|
+
},
|
|
152
|
+
examples: {
|
|
153
|
+
file: "example.ts",
|
|
154
|
+
fails: (value) => longFunction(count(value) + 1),
|
|
155
|
+
passes: (value) => longFunction(count(value)),
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "TS-08",
|
|
160
|
+
title: "A file longer than {value} lines prompts a question",
|
|
161
|
+
rule: "A file over {value} lines, not counting blank lines and comments, fails the lint, so someone asks what could move out. Where nothing should, a disable comment at the top says why.",
|
|
162
|
+
why: "A file that keeps growing is usually several modules sharing a name.",
|
|
163
|
+
ask: "Has any file grown past {value} lines, and does each that stays say why?",
|
|
164
|
+
stage: "mvp",
|
|
165
|
+
check: "auto",
|
|
166
|
+
severity: "low",
|
|
167
|
+
carries: "CODE-10",
|
|
168
|
+
default: { value: 400, unit: "lines" },
|
|
169
|
+
enforcer: {
|
|
170
|
+
tool: "eslint",
|
|
171
|
+
rule: "max-lines",
|
|
172
|
+
options: [{ max: "$value", skipBlankLines: true, skipComments: true }],
|
|
173
|
+
},
|
|
174
|
+
examples: {
|
|
175
|
+
file: "example.ts",
|
|
176
|
+
fails: (value) => longModule(count(value) + 1),
|
|
177
|
+
passes: (value) => longModule(count(value)),
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "TS-09",
|
|
182
|
+
title: "A switch over a union handles every case",
|
|
183
|
+
rule: "A `switch` over a union of values handles each one, so adding a value to the union is a compile-time list of every place to change.",
|
|
184
|
+
why: "A new status that falls through to a default does whatever the default does, which is rarely right for it.",
|
|
185
|
+
ask: "Does every switch over a union in this change handle each of its values?",
|
|
186
|
+
stage: "mvp",
|
|
187
|
+
check: "auto",
|
|
188
|
+
severity: "medium",
|
|
189
|
+
carries: "CODE-15",
|
|
190
|
+
enforcer: {
|
|
191
|
+
tool: "eslint",
|
|
192
|
+
rule: "@typescript-eslint/switch-exhaustiveness-check",
|
|
193
|
+
options: [{ considerDefaultExhaustiveForUnions: false }],
|
|
194
|
+
typed: true,
|
|
195
|
+
},
|
|
196
|
+
examples: {
|
|
197
|
+
file: "example.ts",
|
|
198
|
+
fails: 'type Repair = "booked" | "in-progress" | "done";\nexport function label(repair: Repair): string {\n switch (repair) {\n case "booked":\n return "Booked";\n case "in-progress":\n return "Being repaired";\n default:\n return "";\n }\n}\n',
|
|
199
|
+
passes: 'type Repair = "booked" | "in-progress" | "done";\nexport function label(repair: Repair): string {\n switch (repair) {\n case "booked":\n return "Booked";\n case "in-progress":\n return "Being repaired";\n case "done":\n return "Ready to collect";\n }\n}\n',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "TS-10",
|
|
204
|
+
title: "A catch checks what it caught",
|
|
205
|
+
rule: "A `catch` that handles an error checks what it is, such as with `instanceof`, before using it. One that handles every kind alike says why that's safe.",
|
|
206
|
+
why: "In TypeScript every catch catches everything, so handling a network failure can also hide a bug in the code around it.",
|
|
207
|
+
ask: "Does every catch in this change check what it caught, or say why it handles all errors alike?",
|
|
208
|
+
stage: "mvp",
|
|
209
|
+
check: "ai-review",
|
|
210
|
+
severity: "medium",
|
|
211
|
+
carries: "CODE-12",
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
package/dist/rule.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type Trait } from "peer-ai-workflow";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export declare const STAGES: readonly ["prototype", "mvp", "production"];
|
|
4
|
+
export type Stage = (typeof STAGES)[number];
|
|
5
|
+
export declare const SourceSchema: z.ZodObject<{
|
|
6
|
+
name: z.ZodString;
|
|
7
|
+
ref: z.ZodString;
|
|
8
|
+
url: z.ZodOptional<z.ZodURL>;
|
|
9
|
+
}, z.core.$strict>;
|
|
10
|
+
export declare const RuleSchema: z.ZodObject<{
|
|
11
|
+
id: z.ZodString;
|
|
12
|
+
domain: z.ZodEnum<{
|
|
13
|
+
requirements: "requirements";
|
|
14
|
+
architecture: "architecture";
|
|
15
|
+
"system-design": "system-design";
|
|
16
|
+
"api-design": "api-design";
|
|
17
|
+
frontend: "frontend";
|
|
18
|
+
mobile: "mobile";
|
|
19
|
+
"design-accessibility": "design-accessibility";
|
|
20
|
+
backend: "backend";
|
|
21
|
+
data: "data";
|
|
22
|
+
performance: "performance";
|
|
23
|
+
reliability: "reliability";
|
|
24
|
+
security: "security";
|
|
25
|
+
"privacy-compliance": "privacy-compliance";
|
|
26
|
+
testing: "testing";
|
|
27
|
+
delivery: "delivery";
|
|
28
|
+
operations: "operations";
|
|
29
|
+
"ai-features": "ai-features";
|
|
30
|
+
"code-quality": "code-quality";
|
|
31
|
+
money: "money";
|
|
32
|
+
"safety-critical": "safety-critical";
|
|
33
|
+
}>;
|
|
34
|
+
title: z.ZodString;
|
|
35
|
+
rule: z.ZodString;
|
|
36
|
+
why: z.ZodString;
|
|
37
|
+
ask: z.ZodString;
|
|
38
|
+
stage: z.ZodEnum<{
|
|
39
|
+
prototype: "prototype";
|
|
40
|
+
mvp: "mvp";
|
|
41
|
+
production: "production";
|
|
42
|
+
}>;
|
|
43
|
+
check: z.ZodEnum<{
|
|
44
|
+
auto: "auto";
|
|
45
|
+
"ai-review": "ai-review";
|
|
46
|
+
person: "person";
|
|
47
|
+
}>;
|
|
48
|
+
severity: z.ZodEnum<{
|
|
49
|
+
critical: "critical";
|
|
50
|
+
high: "high";
|
|
51
|
+
medium: "medium";
|
|
52
|
+
low: "low";
|
|
53
|
+
}>;
|
|
54
|
+
when: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
55
|
+
"ai-features": "ai-features";
|
|
56
|
+
money: "money";
|
|
57
|
+
"safety-critical": "safety-critical";
|
|
58
|
+
"several-audiences": "several-audiences";
|
|
59
|
+
offline: "offline";
|
|
60
|
+
"real-time": "real-time";
|
|
61
|
+
uploads: "uploads";
|
|
62
|
+
}>>>;
|
|
63
|
+
sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
64
|
+
name: z.ZodString;
|
|
65
|
+
ref: z.ZodString;
|
|
66
|
+
url: z.ZodOptional<z.ZodURL>;
|
|
67
|
+
}, z.core.$strict>>>;
|
|
68
|
+
}, z.core.$strict>;
|
|
69
|
+
export type RuleInput = z.input<typeof RuleSchema>;
|
|
70
|
+
export type Rule = z.output<typeof RuleSchema>;
|
|
71
|
+
/** The traits a product needs for this rule to apply. */
|
|
72
|
+
export declare function traitsNeeded(rule: Pick<Rule, "domain" | "when">): Trait[];
|
package/dist/rule.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { CHECKS, DOMAIN_IDS, DOMAINS, SEVERITIES, TRAITS } from "peer-ai-workflow";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
// What every rule has (RFC 0003): an id, the rule in plain words, why it matters, a question a
|
|
4
|
+
// reviewer can answer, the stage it applies from, how it's checked, how serious breaking it
|
|
5
|
+
// usually is, and where it comes from when an outside standard is the source.
|
|
6
|
+
export const STAGES = ["prototype", "mvp", "production"];
|
|
7
|
+
const Text = z.string().min(1);
|
|
8
|
+
export const SourceSchema = z.strictObject({
|
|
9
|
+
name: Text.describe("The outside standard and its version, such as OWASP ASVS 5.0."),
|
|
10
|
+
ref: Text.describe("The requirement within it, such as 8.2.2."),
|
|
11
|
+
url: z.url().optional(),
|
|
12
|
+
});
|
|
13
|
+
export const RuleSchema = z
|
|
14
|
+
.strictObject({
|
|
15
|
+
id: z.string().regex(/^[A-Z][A-Z0-9]*-\d{2,}$/, "use the domain's prefix and a number, such as SEC-07"),
|
|
16
|
+
domain: z.enum(DOMAIN_IDS),
|
|
17
|
+
title: Text.max(100).describe("The rule in one short line."),
|
|
18
|
+
rule: Text.describe("The rule itself, in plain words."),
|
|
19
|
+
why: Text.describe("What goes wrong without it."),
|
|
20
|
+
ask: Text.regex(/\?$/, "the ask is a question").describe("The question a reviewer answers."),
|
|
21
|
+
stage: z.enum(STAGES).describe("The rule applies from this stage on."),
|
|
22
|
+
check: z.enum(CHECKS),
|
|
23
|
+
severity: z.enum(SEVERITIES).describe("How serious breaking it usually is, on RFC 0002's scale."),
|
|
24
|
+
when: z.array(z.enum(TRAITS)).min(1).optional().describe("The traits a product must have for the rule to apply."),
|
|
25
|
+
sources: z.array(SourceSchema).min(1).optional(),
|
|
26
|
+
})
|
|
27
|
+
.superRefine((rule, ctx) => {
|
|
28
|
+
const prefix = DOMAINS[rule.domain];
|
|
29
|
+
if (!rule.id.startsWith(`${prefix}-`)) {
|
|
30
|
+
ctx.addIssue({ code: "custom", path: ["id"], message: `a ${rule.domain} rule's id starts with ${prefix}-` });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
/** The sets that apply only to a product with the trait of the same name. */
|
|
34
|
+
const SET_TRAITS = { money: "money", "safety-critical": "safety-critical" };
|
|
35
|
+
/** The traits a product needs for this rule to apply. */
|
|
36
|
+
export function traitsNeeded(rule) {
|
|
37
|
+
const set = SET_TRAITS[rule.domain];
|
|
38
|
+
return [...new Set([...(set === undefined ? [] : [set]), ...(rule.when ?? [])])];
|
|
39
|
+
}
|