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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/core/ai-features.d.ts +65 -0
  4. package/dist/core/ai-features.js +113 -0
  5. package/dist/core/api-design.d.ts +41 -0
  6. package/dist/core/api-design.js +91 -0
  7. package/dist/core/architecture.d.ts +41 -0
  8. package/dist/core/architecture.js +93 -0
  9. package/dist/core/backend.d.ts +21 -0
  10. package/dist/core/backend.js +25 -0
  11. package/dist/core/code-quality.d.ts +91 -0
  12. package/dist/core/code-quality.js +179 -0
  13. package/dist/core/data.d.ts +45 -0
  14. package/dist/core/data.js +59 -0
  15. package/dist/core/delivery.d.ts +153 -0
  16. package/dist/core/delivery.js +149 -0
  17. package/dist/core/design-accessibility.d.ts +124 -0
  18. package/dist/core/design-accessibility.js +199 -0
  19. package/dist/core/frontend.d.ts +77 -0
  20. package/dist/core/frontend.js +109 -0
  21. package/dist/core/mobile.d.ts +46 -0
  22. package/dist/core/mobile.js +83 -0
  23. package/dist/core/money.d.ts +81 -0
  24. package/dist/core/money.js +135 -0
  25. package/dist/core/operations.d.ts +108 -0
  26. package/dist/core/operations.js +177 -0
  27. package/dist/core/performance.d.ts +53 -0
  28. package/dist/core/performance.js +85 -0
  29. package/dist/core/privacy-compliance.d.ts +68 -0
  30. package/dist/core/privacy-compliance.js +77 -0
  31. package/dist/core/reliability.d.ts +67 -0
  32. package/dist/core/reliability.js +105 -0
  33. package/dist/core/requirements.d.ts +41 -0
  34. package/dist/core/requirements.js +58 -0
  35. package/dist/core/safety-critical.d.ts +41 -0
  36. package/dist/core/safety-critical.js +71 -0
  37. package/dist/core/security.d.ts +281 -0
  38. package/dist/core/security.js +427 -0
  39. package/dist/core/system-design.d.ts +31 -0
  40. package/dist/core/system-design.js +69 -0
  41. package/dist/core/testing.d.ts +51 -0
  42. package/dist/core/testing.js +124 -0
  43. package/dist/domains.d.ts +6 -0
  44. package/dist/domains.js +53 -0
  45. package/dist/index.d.ts +30 -0
  46. package/dist/index.js +118 -0
  47. package/dist/profile.d.ts +262 -0
  48. package/dist/profile.js +269 -0
  49. package/dist/profiles/express.d.ts +2 -0
  50. package/dist/profiles/express.js +93 -0
  51. package/dist/profiles/fastapi.d.ts +2 -0
  52. package/dist/profiles/fastapi.js +104 -0
  53. package/dist/profiles/fastify.d.ts +2 -0
  54. package/dist/profiles/fastify.js +56 -0
  55. package/dist/profiles/github-actions.d.ts +2 -0
  56. package/dist/profiles/github-actions.js +151 -0
  57. package/dist/profiles/nestjs.d.ts +2 -0
  58. package/dist/profiles/nestjs.js +77 -0
  59. package/dist/profiles/next.d.ts +2 -0
  60. package/dist/profiles/next.js +73 -0
  61. package/dist/profiles/node.d.ts +2 -0
  62. package/dist/profiles/node.js +79 -0
  63. package/dist/profiles/python.d.ts +2 -0
  64. package/dist/profiles/python.js +232 -0
  65. package/dist/profiles/react-native.d.ts +2 -0
  66. package/dist/profiles/react-native.js +133 -0
  67. package/dist/profiles/react.d.ts +2 -0
  68. package/dist/profiles/react.js +180 -0
  69. package/dist/profiles/typescript.d.ts +2 -0
  70. package/dist/profiles/typescript.js +214 -0
  71. package/dist/rule.d.ts +72 -0
  72. package/dist/rule.js +39 -0
  73. package/package.json +39 -0
@@ -0,0 +1,151 @@
1
+ // The checks a pipeline runs, on GitHub Actions (RFC 0006, section 4). peer-ai render writes them
2
+ // as one workflow. The automatic ones are proven in Peer AI's own CI: each tool is run on the
3
+ // failing and passing examples. The TLS check and the scan of the running app need a live
4
+ // environment, so a review confirms they ran. Examples are from a made-up bicycle repair booking
5
+ // service.
6
+ /** A token in GitHub's format, built when the example runs, so no token is written in this file. */
7
+ const fakeToken = () => `${["gh", "p_"].join("")}${"R7kQ2mZ9".repeat(4)}ab4d`;
8
+ const checkout = (pinned) => pinned ? "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1" : "actions/checkout@v7";
9
+ /** A workflow that builds the app, pinned or not, starting from no permissions, with the job's own. */
10
+ function workflow(pinned, jobPermissions = ["contents: read"]) {
11
+ return [
12
+ "name: Build",
13
+ "on: pull_request",
14
+ "permissions: {}",
15
+ "jobs:",
16
+ " build:",
17
+ " runs-on: ubuntu-latest",
18
+ ...(jobPermissions.length === 1 && jobPermissions[0] === "write-all"
19
+ ? [" permissions: write-all"]
20
+ : [" permissions:", ...jobPermissions.map((line) => ` ${line}`)]),
21
+ " steps:",
22
+ ` - uses: ${checkout(pinned)}`,
23
+ " with:",
24
+ " persist-credentials: false",
25
+ " - run: make test",
26
+ "",
27
+ ].join("\n");
28
+ }
29
+ export const githubActions = {
30
+ id: "github-actions",
31
+ name: "GitHub Actions",
32
+ prefix: "GHA",
33
+ about: "The checks a pipeline on GitHub Actions runs on every change: secrets, dependencies with published vulnerabilities, workflows' own safety, and code scanning; and, where the project has environments with addresses, a TLS check and a scan of the running app in staging. `peer-ai render` writes them as one workflow, each tool pinned to a checked release.",
34
+ stacks: [],
35
+ rules: [
36
+ {
37
+ id: "GHA-01",
38
+ title: "Every change is scanned for secrets",
39
+ rule: "Every pull request's commits are scanned for secrets, such as keys and tokens, and the whole history is scanned every day; the check fails when one is found. A secret found in old history that has already been replaced is recorded in `.gitleaksignore`, with why.",
40
+ why: "A secret pushed once stays in the history for anyone with a copy, even after the file is fixed.",
41
+ ask: "Does the pipeline scan every change for secrets, and fail when it finds one?",
42
+ stage: "mvp",
43
+ check: "auto",
44
+ severity: "high",
45
+ carries: "SEC-27",
46
+ enforcer: { tool: "github-actions", job: "secrets", finding: "github-pat" },
47
+ examples: {
48
+ file: "settings.py",
49
+ fails: () => `BOOKINGS_TOKEN = "${fakeToken()}"\n`,
50
+ passes: 'import os\n\nBOOKINGS_TOKEN = os.environ["BOOKINGS_TOKEN"]\n',
51
+ },
52
+ },
53
+ {
54
+ id: "GHA-02",
55
+ title: "Dependencies are checked for published vulnerabilities",
56
+ rule: "Every change, and every day, the dependencies in each lockfile and manifest are checked against the published advisories, and the check fails when one is affected.",
57
+ why: "New advisories are published for code that hasn't changed, so a check that runs only on changes misses them.",
58
+ ask: "Does the pipeline check dependencies on every change and every day?",
59
+ stage: "mvp",
60
+ check: "auto",
61
+ severity: "high",
62
+ carries: "DEL-03",
63
+ enforcer: { tool: "github-actions", job: "dependencies", finding: "jinja2" },
64
+ examples: { file: "requirements.txt", fails: "jinja2==2.10\n", passes: "six==1.17.0\n" },
65
+ },
66
+ {
67
+ id: "GHA-03",
68
+ title: "Every action is pinned to a commit",
69
+ rule: "Every action a workflow or an action in the repository uses is pinned to a full commit, never to a tag or a branch that can move. A comment with its version helps a reviewer, and isn't checked.",
70
+ why: "A tag can be moved to other code, so a workflow that uses one runs whatever its owner, or whoever took over the account, puts there next.",
71
+ ask: "Is every action in the workflows pinned to a commit?",
72
+ stage: "prototype",
73
+ check: "auto",
74
+ severity: "high",
75
+ carries: "DEL-01",
76
+ enforcer: { tool: "github-actions", job: "workflows", finding: "unpinned-uses" },
77
+ examples: {
78
+ file: ".github/workflows/build.yml",
79
+ fails: workflow(false),
80
+ passes: workflow(true),
81
+ },
82
+ },
83
+ {
84
+ id: "GHA-04",
85
+ title: "No job asks for a token that can write everything",
86
+ rule: "No job asks for `write-all`, the token that can change the code, the releases and the settings.",
87
+ why: "With write-all, one compromised step, such as a hijacked action, can change anything the repository holds.",
88
+ ask: "Does any job in the workflows ask for write-all?",
89
+ stage: "mvp",
90
+ check: "auto",
91
+ severity: "high",
92
+ carries: "SEC-28",
93
+ enforcer: { tool: "github-actions", job: "workflows", finding: "excessive-permissions" },
94
+ examples: {
95
+ file: ".github/workflows/build.yml",
96
+ fails: workflow(true, ["write-all"]),
97
+ passes: workflow(true),
98
+ },
99
+ },
100
+ {
101
+ id: "GHA-05",
102
+ title: "Code scanning is a required check",
103
+ rule: "Every change is scanned for insecure code by a code scanner, such as Semgrep with a pinned set of rules, or CodeQL, and the check must pass before the change merges.",
104
+ why: "A scanner finds the patterns a reviewer skims past, in every change, including the ones nobody reviews closely.",
105
+ ask: "Does code scanning run on every change, and must it pass before a merge?",
106
+ stage: "mvp",
107
+ check: "auto",
108
+ severity: "high",
109
+ carries: "DEL-04",
110
+ enforcer: { tool: "github-actions", job: "code", finding: "avoid-pickle" },
111
+ examples: {
112
+ file: "app.py",
113
+ fails: 'import pickle\nimport subprocess\n\nimport yaml\n\n\ndef load_booking(raw: bytes) -> object:\n return pickle.loads(raw)\n\n\ndef load_settings(text: str) -> object:\n return yaml.load(text)\n\n\ndef print_label(serial: str) -> None:\n subprocess.call("lpr " + serial, shell=True)\n',
114
+ passes: 'import json\nimport subprocess\n\n\ndef load_booking(raw: bytes) -> object:\n return json.loads(raw)\n\n\ndef print_label(serial: str) -> None:\n subprocess.run(["lpr", serial], check=True)\n',
115
+ },
116
+ },
117
+ {
118
+ id: "GHA-06",
119
+ title: "Each environment's TLS is checked",
120
+ rule: "On a schedule, each environment with an address is checked against modern TLS settings, such as Mozilla's intermediate profile, and a failure is fixed.",
121
+ why: "TLS settings drift as certificates renew and servers are rebuilt, and nothing else notices until a browser refuses the site.",
122
+ ask: "Does the pipeline check each environment's TLS on a schedule, and did the last run pass?",
123
+ stage: "mvp",
124
+ check: "ai-review",
125
+ severity: "medium",
126
+ carries: "SEC-23",
127
+ },
128
+ {
129
+ id: "GHA-07",
130
+ title: "The running app is scanned in staging",
131
+ rule: "On a schedule, and before a release, the running app is scanned, such as by OWASP ZAP's baseline scan, in each environment the config marks as not production, never in production. Each finding is fixed, or accepted by a person in `.github/zap-rules.tsv`, with the reason.",
132
+ why: "Some problems only show in a running app: headers, cookies and pages left open.",
133
+ ask: "Does the pipeline scan the running app in staging, and are its findings fixed or accepted?",
134
+ stage: "production",
135
+ check: "ai-review",
136
+ severity: "medium",
137
+ carries: "DEL-08",
138
+ },
139
+ {
140
+ id: "GHA-08",
141
+ title: "A workflow starts from no permissions, and each job asks for what it uses",
142
+ rule: "A workflow gives its token no permissions by default, with `permissions: {}`, and each job asks for only what it uses, such as `contents: read`.",
143
+ why: "The default token can often write, so a workflow that doesn't say otherwise gives every step more than it needs.",
144
+ ask: "Does every workflow start from no permissions, and does each job ask for only what it uses?",
145
+ stage: "mvp",
146
+ check: "ai-review",
147
+ severity: "medium",
148
+ carries: "SEC-28",
149
+ },
150
+ ],
151
+ };
@@ -0,0 +1,2 @@
1
+ import type { ProfileInput } from "../profile.ts";
2
+ export declare const nestjs: ProfileInput;
@@ -0,0 +1,77 @@
1
+ // NestJS. It builds on Node. Examples are from a made-up bicycle repair booking service.
2
+ export const nestjs = {
3
+ id: "nestjs",
4
+ name: "NestJS",
5
+ prefix: "NEST",
6
+ about: "APIs in NestJS: a validation pipe that refuses unknown fields, guards on every controller, thin controllers, an exception filter that keeps internals in, and settings checked at start-up. It builds on the Node profile.",
7
+ stacks: ["nest"],
8
+ extends: ["node"],
9
+ rules: [
10
+ {
11
+ id: "NEST-01",
12
+ title: "A global validation pipe refuses unknown fields",
13
+ rule: "A global `ValidationPipe` runs with `whitelist` and `forbidNonWhitelisted`, and every request body is a DTO class with validation decorators.",
14
+ why: "Without the pipe, a DTO's decorators do nothing, and a body with extra fields, such as a role, reaches the service as it came.",
15
+ ask: "Does the app run a global ValidationPipe that refuses unknown fields, and does every body have a validated DTO?",
16
+ stage: "prototype",
17
+ check: "ai-review",
18
+ severity: "high",
19
+ carries: "SEC-05",
20
+ },
21
+ {
22
+ id: "NEST-02",
23
+ title: "Every route is guarded unless it says it's public",
24
+ rule: "An authentication guard is global, and a route open to everyone says so with a decorator, such as `@Public()`, which a reviewer can see.",
25
+ why: "A guard added controller by controller is missed on the one controller that needed it.",
26
+ ask: "Is every route in this change behind the guard, or marked public on purpose?",
27
+ stage: "mvp",
28
+ check: "ai-review",
29
+ severity: "critical",
30
+ carries: "SEC-03",
31
+ },
32
+ {
33
+ id: "NEST-03",
34
+ title: "Controllers call services, which hold the rules",
35
+ rule: "A controller maps the request to a service call and the result to a response. The business rules live in the service, which knows nothing of HTTP.",
36
+ why: "Rules in a controller can't be reused by a queue consumer or a scheduled job, and are tested only through HTTP.",
37
+ ask: "Does any controller in this change hold a business rule?",
38
+ stage: "mvp",
39
+ check: "ai-review",
40
+ severity: "medium",
41
+ carries: "ARC-05",
42
+ },
43
+ {
44
+ id: "NEST-04",
45
+ title: "One exception filter gives every error the same shape",
46
+ rule: "A global exception filter answers every error in the project's error shape, whichever exception it was, with the detail kept for the log.",
47
+ why: "Nest's built-in answers differ between its own exceptions and everything else, so a client has to handle several shapes.",
48
+ ask: "Does every error in this change reach the client in the project's error shape?",
49
+ stage: "mvp",
50
+ check: "ai-review",
51
+ severity: "medium",
52
+ carries: "API-04",
53
+ },
54
+ {
55
+ id: "NEST-05",
56
+ title: "Settings are checked when the app starts",
57
+ rule: "`ConfigModule` loads settings with a validation schema, so the app refuses to start with a setting missing or wrong, and an environment name it doesn't know is treated as production.",
58
+ why: "A setting read unchecked fails on the first request that needs it, long after the deploy looked fine.",
59
+ ask: "Does the app check its settings with a schema at start-up?",
60
+ stage: "mvp",
61
+ check: "ai-review",
62
+ severity: "medium",
63
+ carries: "REL-04",
64
+ },
65
+ {
66
+ id: "NEST-06",
67
+ title: "Modules share only the providers they export",
68
+ rule: "A module uses another only through the providers that module exports, never by importing its internal files.",
69
+ why: "Reaching past a module's exports ties the two together, so neither can change alone.",
70
+ ask: "Does this change reach into another module past its exports?",
71
+ stage: "mvp",
72
+ check: "ai-review",
73
+ severity: "medium",
74
+ carries: "ARC-03",
75
+ },
76
+ ],
77
+ };
@@ -0,0 +1,2 @@
1
+ import type { ProfileInput } from "../profile.ts";
2
+ export declare const next: ProfileInput;
@@ -0,0 +1,73 @@
1
+ // Next.js, for apps whose screens and server code live together. It builds on React and Node.
2
+ // Examples are from a made-up bicycle repair booking service.
3
+ export const next = {
4
+ id: "next",
5
+ name: "Next.js",
6
+ prefix: "NEXT",
7
+ about: "Full-stack apps in Next.js, where screens and server code share one repository: images sized for the screen, secrets that stay on the server, and server actions and route handlers treated as the public endpoints they are. It builds on the React and Node profiles.",
8
+ stacks: ["next"],
9
+ extends: ["react", "node"],
10
+ rules: [
11
+ {
12
+ id: "NEXT-01",
13
+ title: "Images go through Next's image component",
14
+ rule: "Images are shown with `next/image`, which sizes and compresses each for the screen that asks, not with a plain `img`.",
15
+ why: "A plain img sends the full-size file to every phone, and the page waits for it.",
16
+ ask: "Does this change show an image with a plain img?",
17
+ stage: "mvp",
18
+ check: "auto",
19
+ severity: "low",
20
+ carries: "PERF-07",
21
+ enforcer: { tool: "eslint", rule: "@next/next/no-img-element" },
22
+ examples: {
23
+ file: "example.tsx",
24
+ fails: 'export function Frame() {\n return <img src="/frame.jpg" alt="A steel touring frame" />;\n}\n',
25
+ passes: 'import Image from "next/image";\nexport function Frame() {\n return <Image src="/frame.jpg" alt="A steel touring frame" width={640} height={480} />;\n}\n',
26
+ },
27
+ },
28
+ {
29
+ id: "NEXT-02",
30
+ title: "Secrets stay on the server",
31
+ rule: "Nothing secret is in a `NEXT_PUBLIC_` variable, which is built into the page. Modules that hold secrets import `server-only`, so importing them from a client component fails the build.",
32
+ why: "Anything a client component touches is sent to every browser, and one import is enough to send a secret with it.",
33
+ ask: "Could anything secret in this change reach the browser?",
34
+ stage: "prototype",
35
+ check: "ai-review",
36
+ severity: "critical",
37
+ carries: "SEC-11",
38
+ },
39
+ {
40
+ id: "NEXT-03",
41
+ title: "Server actions and route handlers check who's calling",
42
+ rule: "Every server action and route handler checks the caller may do this, to this record, on the server, as its first step.",
43
+ why: "A server action is a public endpoint anyone can call directly, whatever the screen that normally calls it shows.",
44
+ ask: "Does every server action and route handler in this change check the caller's permission first?",
45
+ stage: "mvp",
46
+ check: "ai-review",
47
+ severity: "critical",
48
+ carries: "SEC-03",
49
+ },
50
+ {
51
+ id: "NEXT-04",
52
+ title: "Server actions and route handlers check their input",
53
+ rule: "Every server action and route handler checks its input against a schema before using it, whatever types the calling component declares.",
54
+ why: "Types in the client are a promise the caller doesn't have to keep. The server gets whatever is sent.",
55
+ ask: "Does every server action and route handler in this change check its input on the server?",
56
+ stage: "prototype",
57
+ check: "ai-review",
58
+ severity: "high",
59
+ carries: "SEC-05",
60
+ },
61
+ {
62
+ id: "NEXT-05",
63
+ title: "Every response sets its security headers",
64
+ rule: "The security headers, such as a content security policy, are set for every route, in `next.config` or the proxy, which was called middleware before Next.js 16.",
65
+ why: "Without them, a browser lets another site frame the page, and runs any script injected into it.",
66
+ ask: "Do the app's responses set the security headers?",
67
+ stage: "mvp",
68
+ check: "ai-review",
69
+ severity: "medium",
70
+ carries: "SEC-17",
71
+ },
72
+ ],
73
+ };
@@ -0,0 +1,2 @@
1
+ import type { ProfileInput } from "../profile.ts";
2
+ export declare const node: ProfileInput;
@@ -0,0 +1,79 @@
1
+ // Node on the server, and the base for Express, NestJS, Fastify and Next.js. Examples are from a
2
+ // made-up bicycle repair booking service.
3
+ export const node = {
4
+ id: "node",
5
+ name: "Node",
6
+ prefix: "NODE",
7
+ about: "Node on the server: no code built from strings, calls to other services that give up, settings checked once at start-up, and failures that stop the process instead of hiding. Express, NestJS, Fastify and Next.js build on it.",
8
+ stacks: ["node", "hono", "koa"],
9
+ extends: ["typescript"],
10
+ rules: [
11
+ {
12
+ id: "NODE-01",
13
+ title: "No text is run with eval",
14
+ rule: "`eval` isn't used, so no text, least of all text from outside, is ever run as code.",
15
+ why: "A string that reaches eval runs with everything the server can reach: its secrets, its database and its files.",
16
+ ask: "Does this change use eval?",
17
+ stage: "prototype",
18
+ check: "auto",
19
+ severity: "critical",
20
+ carries: "SEC-30",
21
+ enforcer: { tool: "eslint", rule: "no-eval" },
22
+ examples: {
23
+ file: "example.ts",
24
+ fails: "export function price(formula: string): number {\n return Number(eval(formula));\n}\n",
25
+ passes: "export function price(labour: number, parts: number): number {\n return labour + parts;\n}\n",
26
+ },
27
+ },
28
+ {
29
+ id: "NODE-02",
30
+ title: "Every call to another service gives up in time",
31
+ rule: "A call to another service, such as with `fetch`, has a timeout, such as `AbortSignal.timeout`, and its failure is handled, not left to hang or crash the request.",
32
+ why: "Node's fetch waits up to five minutes by default, so one slow service holds every request that calls it for that long.",
33
+ ask: "Does every call to another service in this change have a timeout and handle failure?",
34
+ stage: "mvp",
35
+ check: "ai-review",
36
+ severity: "high",
37
+ carries: "REL-01",
38
+ },
39
+ {
40
+ id: "NODE-03",
41
+ title: "Settings are read and checked once, at start-up",
42
+ rule: "`process.env` is read in one module that checks every setting at start-up, such as with a schema, and the process stops when one is missing or wrong. An environment name it doesn't know is treated as production. The rest of the code imports the checked values.",
43
+ why: "A setting read where it's used fails the first request that needs it, in production, long after the deploy looked fine.",
44
+ ask: "Does this change read process.env outside the settings module?",
45
+ stage: "mvp",
46
+ check: "ai-review",
47
+ severity: "medium",
48
+ carries: "REL-04",
49
+ },
50
+ {
51
+ id: "NODE-04",
52
+ title: "A promise that fails where nothing catches it stops the process",
53
+ rule: "Nothing swallows unhandled rejections: no `process.on('unhandledRejection')` handler that carries on. The process stops, and the platform restarts it.",
54
+ why: "A process that carries on after an unexpected failure runs in a state nobody designed, and fails in stranger ways later.",
55
+ ask: "Does this change let the process carry on after an unhandled rejection?",
56
+ stage: "mvp",
57
+ check: "ai-review",
58
+ severity: "medium",
59
+ carries: "CODE-11",
60
+ },
61
+ {
62
+ id: "NODE-05",
63
+ title: "No function is built from a string",
64
+ rule: "`new Function` isn't used, so no text is ever turned into a function and run.",
65
+ why: "A function built from a string is eval by another name, with the same reach into the server.",
66
+ ask: "Does this change build a function from a string?",
67
+ stage: "prototype",
68
+ check: "auto",
69
+ severity: "critical",
70
+ carries: "SEC-30",
71
+ enforcer: { tool: "eslint", rule: "no-new-func" },
72
+ examples: {
73
+ file: "example.ts",
74
+ fails: "export function price(formula: string): number {\n const run = new Function(`return ${formula}`) as () => number;\n return run();\n}\n",
75
+ passes: "export function price(labour: number, parts: number): number {\n return labour + parts;\n}\n",
76
+ },
77
+ },
78
+ ],
79
+ };
@@ -0,0 +1,2 @@
1
+ import type { ProfileInput } from "../profile.ts";
2
+ export declare const python: ProfileInput;
@@ -0,0 +1,232 @@
1
+ // Python, in any framework, enforced by Ruff. Examples are from a made-up bicycle repair booking
2
+ // service.
3
+ /** A function Ruff counts as `statements` statements long: it doesn't count the last line. */
4
+ function longFunction(statements) {
5
+ const body = Array.from({ length: statements - 1 }, (_, i) => ` total += ${String(i)}`);
6
+ return ["def repair_total() -> int:", " total = 0", ...body, " return total", ""].join("\n");
7
+ }
8
+ export const python = {
9
+ id: "python",
10
+ name: "Python",
11
+ prefix: "PY",
12
+ about: "Python in any framework, checked by Ruff: errors that are caught on purpose, types that say something, queries and commands never built from text, and calls to other services that give up. The FastAPI profile builds on it.",
13
+ stacks: ["python"],
14
+ rules: [
15
+ {
16
+ id: "PY-01",
17
+ title: "An except names what it catches",
18
+ rule: "An `except` names the errors it expects. A bare `except:` isn't written.",
19
+ why: "A bare except also catches the signal to stop, and every bug in the code it wraps.",
20
+ ask: "Does this change write a bare except?",
21
+ stage: "mvp",
22
+ check: "auto",
23
+ severity: "medium",
24
+ carries: "CODE-12",
25
+ enforcer: { tool: "ruff", rule: "E722" },
26
+ examples: {
27
+ file: "example.py",
28
+ fails: 'def parse_bay(text: str) -> int:\n try:\n return int(text)\n except:\n raise ValueError("not a bay") from None\n',
29
+ passes: 'def parse_bay(text: str) -> int:\n try:\n return int(text)\n except ValueError:\n raise ValueError("not a bay") from None\n',
30
+ },
31
+ },
32
+ {
33
+ id: "PY-02",
34
+ title: "Catching every error says why",
35
+ rule: "`except Exception` is written only where handling every error alike is right, with a comment saying why, beside a `noqa` for the check.",
36
+ why: "Catching everything hides the bugs along with the failures it meant to handle.",
37
+ ask: "Does every except Exception in this change say why?",
38
+ stage: "mvp",
39
+ check: "auto",
40
+ severity: "medium",
41
+ carries: "CODE-12",
42
+ enforcer: { tool: "ruff", rule: "BLE001" },
43
+ examples: {
44
+ file: "example.py",
45
+ fails: "from collections.abc import Callable\n\n\ndef parts_price(fetch: Callable[[], float]) -> float | None:\n try:\n return fetch()\n except Exception:\n return None\n",
46
+ passes: "from collections.abc import Callable\n\n\ndef parts_price(fetch: Callable[[], float]) -> float | None:\n try:\n return fetch()\n except ConnectionError:\n return None\n",
47
+ },
48
+ },
49
+ {
50
+ id: "PY-03",
51
+ title: "No error is caught and ignored",
52
+ rule: "An `except` block never just passes. Where ignoring an error is right, it logs it and says why.",
53
+ why: "An error that's caught and dropped turns a failure into silence.",
54
+ ask: "Does this change catch an error and do nothing with it?",
55
+ stage: "prototype",
56
+ check: "auto",
57
+ severity: "high",
58
+ carries: "CODE-11",
59
+ enforcer: { tool: "ruff", rule: "S110", settings: { "flake8-bandit": { "check-typed-exception": true } } },
60
+ examples: {
61
+ file: "example.py",
62
+ fails: "from collections.abc import Callable\n\n\ndef cancel(booking_id: int, release: Callable[[int], None]) -> None:\n try:\n release(booking_id)\n except KeyError:\n pass\n",
63
+ passes: 'import logging\nfrom collections.abc import Callable\n\nlogger = logging.getLogger(__name__)\n\n\ndef cancel(booking_id: int, release: Callable[[int], None]) -> None:\n try:\n release(booking_id)\n except KeyError:\n # Already released by the mechanic; nothing left to do.\n logger.info("booking %s already released", booking_id)\n',
64
+ },
65
+ },
66
+ {
67
+ id: "PY-04",
68
+ title: "No argument or return is typed `Any`",
69
+ rule: "No function takes or returns `Any`. Data of unknown shape is `object`, and checked before use. `Any` elsewhere, such as inside `dict[str, Any]`, is the strict type checker's to catch (PY-11).",
70
+ why: "`Any` turns the type checker off for everything it touches.",
71
+ ask: "Does any function in this change take or return Any?",
72
+ stage: "prototype",
73
+ check: "auto",
74
+ severity: "medium",
75
+ carries: "CODE-14",
76
+ enforcer: { tool: "ruff", rule: "ANN401" },
77
+ examples: {
78
+ file: "example.py",
79
+ fails: "from typing import Any\n\n\ndef bike_name(data: Any) -> str:\n return str(data)\n",
80
+ passes: 'def bike_name(data: object) -> str:\n return data if isinstance(data, str) else ""\n',
81
+ },
82
+ },
83
+ {
84
+ id: "PY-05",
85
+ title: "Every function says its argument types",
86
+ rule: "Every function's arguments have type annotations, so the type checker can check the calls.",
87
+ why: "An unannotated argument is `Any` to the type checker, and every call to it goes unchecked.",
88
+ ask: "Does this change add a function argument without a type?",
89
+ stage: "mvp",
90
+ check: "auto",
91
+ severity: "low",
92
+ carries: "CODE-14",
93
+ enforcer: { tool: "ruff", rule: "ANN001" },
94
+ examples: {
95
+ file: "example.py",
96
+ fails: "def total(labour, parts) -> int:\n return labour + parts\n",
97
+ passes: "def total(labour: int, parts: int) -> int:\n return labour + parts\n",
98
+ },
99
+ },
100
+ {
101
+ id: "PY-06",
102
+ title: "Queries are never built from text",
103
+ rule: "SQL is never built by joining or formatting strings. Values go to the driver as parameters.",
104
+ why: "Text put into a query can change what the query does, and read or delete any record.",
105
+ ask: "Does this change build a query from strings?",
106
+ stage: "prototype",
107
+ check: "auto",
108
+ severity: "critical",
109
+ carries: "SEC-07",
110
+ enforcer: { tool: "ruff", rule: "S608" },
111
+ examples: {
112
+ file: "example.py",
113
+ fails: "from sqlite3 import Cursor\n\n\ndef bikes_named(cursor: Cursor, name: str) -> list[tuple[str]]:\n cursor.execute(f\"SELECT serial FROM bikes WHERE name = '{name}'\")\n return cursor.fetchall()\n",
114
+ passes: 'from sqlite3 import Cursor\n\n\ndef bikes_named(cursor: Cursor, name: str) -> list[tuple[str]]:\n cursor.execute("SELECT serial FROM bikes WHERE name = ?", (name,))\n return cursor.fetchall()\n',
115
+ },
116
+ },
117
+ {
118
+ id: "PY-07",
119
+ title: "No text is run with eval",
120
+ rule: "`eval` isn't used, so no text, least of all text from outside, is ever run as code.",
121
+ why: "A string that reaches eval runs with everything the server can reach.",
122
+ ask: "Does this change use eval?",
123
+ stage: "prototype",
124
+ check: "auto",
125
+ severity: "critical",
126
+ carries: "SEC-30",
127
+ enforcer: { tool: "ruff", rule: "S307" },
128
+ examples: {
129
+ file: "example.py",
130
+ fails: "def price(formula: str) -> float:\n return float(eval(formula))\n",
131
+ passes: "def price(labour: float, parts: float) -> float:\n return labour + parts\n",
132
+ },
133
+ },
134
+ {
135
+ id: "PY-08",
136
+ title: "Commands don't go through a shell",
137
+ rule: "`subprocess` runs a command as a list of arguments, never with `shell=True`, so no argument can become another command.",
138
+ why: "With a shell, one argument holding a semicolon runs whatever follows it.",
139
+ ask: "Does this change run a command through a shell?",
140
+ stage: "prototype",
141
+ check: "auto",
142
+ severity: "critical",
143
+ carries: "SEC-30",
144
+ enforcer: { tool: "ruff", rule: "S602" },
145
+ examples: {
146
+ file: "example.py",
147
+ fails: 'import subprocess\n\n\ndef print_label(serial: str) -> None:\n subprocess.run(f"lpr label-{serial}.pdf", shell=True, check=True)\n',
148
+ passes: 'import subprocess\n\n\ndef print_label(serial: str) -> None:\n subprocess.run(["lpr", f"label-{serial}.pdf"], check=True)\n',
149
+ },
150
+ },
151
+ {
152
+ id: "PY-09",
153
+ title: "Every request to another service has a timeout",
154
+ rule: "Every HTTP request, such as with `requests`, passes a `timeout`, and its failure is handled.",
155
+ why: "requests waits forever by default, so one slow service holds every request that calls it.",
156
+ ask: "Does every HTTP request in this change pass a timeout?",
157
+ stage: "mvp",
158
+ check: "auto",
159
+ severity: "high",
160
+ carries: "REL-01",
161
+ enforcer: { tool: "ruff", rule: "S113" },
162
+ examples: {
163
+ file: "example.py",
164
+ fails: 'import requests\n\n\ndef parts_price(sku: str) -> float:\n return float(requests.get(f"https://parts.example.com/{sku}").json()["price"])\n',
165
+ passes: 'import requests\n\n\ndef parts_price(sku: str) -> float:\n response = requests.get(f"https://parts.example.com/{sku}", timeout=5)\n return float(response.json()["price"])\n',
166
+ },
167
+ },
168
+ {
169
+ id: "PY-10",
170
+ title: "A function with more than {value} statements prompts a question",
171
+ rule: "A function over {value} statements fails the lint, so someone asks whether it does two things. Where it doesn't, a `noqa` beside it says why.",
172
+ why: "Long functions usually hide a second job, and the second job is what the next change breaks.",
173
+ ask: "Has any function grown past {value} statements, and does each that stays say why?",
174
+ stage: "mvp",
175
+ check: "auto",
176
+ severity: "low",
177
+ carries: "CODE-10",
178
+ default: { value: 50, unit: "statements" },
179
+ enforcer: { tool: "ruff", rule: "PLR0915", settings: { pylint: { "max-statements": "$value" } } },
180
+ examples: {
181
+ file: "example.py",
182
+ fails: (value) => longFunction(Number(value) + 1),
183
+ passes: (value) => longFunction(Number(value)),
184
+ },
185
+ },
186
+ {
187
+ id: "PY-11",
188
+ title: "The type checker runs in strict mode",
189
+ rule: "A type checker, such as mypy with `strict = true` or pyright in strict mode, runs on every change and fails the build.",
190
+ why: "Annotations nobody checks drift from the code, and read as a promise nothing keeps.",
191
+ ask: "Does a strict type checker run on every change?",
192
+ stage: "mvp",
193
+ check: "ai-review",
194
+ severity: "medium",
195
+ carries: "CODE-14",
196
+ },
197
+ {
198
+ id: "PY-12",
199
+ title: "No text is run with exec",
200
+ rule: "`exec` isn't used, so no text, least of all text from outside, is ever run as code.",
201
+ why: "exec runs any statement it's given, with everything the server can reach.",
202
+ ask: "Does this change use exec?",
203
+ stage: "prototype",
204
+ check: "auto",
205
+ severity: "critical",
206
+ carries: "SEC-30",
207
+ enforcer: { tool: "ruff", rule: "S102" },
208
+ examples: {
209
+ file: "example.py",
210
+ fails: 'def apply_discount(code: str) -> None:\n exec(f"discount = {code}")\n',
211
+ passes: "def apply_discount(percent: int) -> float:\n return percent / 100\n",
212
+ },
213
+ },
214
+ {
215
+ id: "PY-13",
216
+ title: "No command is run by os.system or another shell helper",
217
+ rule: "`os.system`, `os.popen` and the like, which always go through a shell, aren't used. A command runs with `subprocess` and a list of arguments.",
218
+ why: "Every one of these hands the whole string to a shell, so an argument holding a semicolon runs whatever follows it.",
219
+ ask: "Does this change run a command through os.system or another shell helper?",
220
+ stage: "prototype",
221
+ check: "auto",
222
+ severity: "critical",
223
+ carries: "SEC-30",
224
+ enforcer: { tool: "ruff", rule: "S605" },
225
+ examples: {
226
+ file: "example.py",
227
+ fails: 'import os\n\n\ndef print_label(serial: str) -> None:\n os.system(f"lpr label-{serial}.pdf")\n',
228
+ passes: 'import subprocess\n\n\ndef print_label(serial: str) -> None:\n subprocess.run(["lpr", f"label-{serial}.pdf"], check=True)\n',
229
+ },
230
+ },
231
+ ],
232
+ };
@@ -0,0 +1,2 @@
1
+ import type { ProfileInput } from "../profile.ts";
2
+ export declare const reactNative: ProfileInput;