becwright 0.1.0__tar.gz
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.
- becwright-0.1.0/LICENSE +21 -0
- becwright-0.1.0/PKG-INFO +231 -0
- becwright-0.1.0/README.md +209 -0
- becwright-0.1.0/pyproject.toml +38 -0
- becwright-0.1.0/setup.cfg +4 -0
- becwright-0.1.0/src/becwright/__init__.py +1 -0
- becwright-0.1.0/src/becwright/bundle.py +157 -0
- becwright-0.1.0/src/becwright/checks/__init__.py +0 -0
- becwright-0.1.0/src/becwright/checks/dangerous_eval.py +34 -0
- becwright-0.1.0/src/becwright/checks/debug_remnants.py +36 -0
- becwright-0.1.0/src/becwright/checks/forbid.py +54 -0
- becwright-0.1.0/src/becwright/checks/hardcoded_secrets.py +46 -0
- becwright-0.1.0/src/becwright/checks/no_token_in_logs.py +37 -0
- becwright-0.1.0/src/becwright/checks/redundant_comments.py +83 -0
- becwright-0.1.0/src/becwright/checks/wildcard_imports.py +34 -0
- becwright-0.1.0/src/becwright/cli.py +179 -0
- becwright-0.1.0/src/becwright/engine.py +68 -0
- becwright-0.1.0/src/becwright/git.py +68 -0
- becwright-0.1.0/src/becwright/rules.py +40 -0
- becwright-0.1.0/src/becwright.egg-info/PKG-INFO +231 -0
- becwright-0.1.0/src/becwright.egg-info/SOURCES.txt +31 -0
- becwright-0.1.0/src/becwright.egg-info/dependency_links.txt +1 -0
- becwright-0.1.0/src/becwright.egg-info/entry_points.txt +2 -0
- becwright-0.1.0/src/becwright.egg-info/requires.txt +5 -0
- becwright-0.1.0/src/becwright.egg-info/top_level.txt +1 -0
- becwright-0.1.0/tests/test_bundle.py +312 -0
- becwright-0.1.0/tests/test_checks.py +19 -0
- becwright-0.1.0/tests/test_cli_and_git.py +142 -0
- becwright-0.1.0/tests/test_engine.py +21 -0
- becwright-0.1.0/tests/test_engine_integration.py +42 -0
- becwright-0.1.0/tests/test_forbid.py +56 -0
- becwright-0.1.0/tests/test_more_checks.py +104 -0
- becwright-0.1.0/tests/test_redundant_comments.py +32 -0
becwright-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alonso David De Leon Rodarte
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
becwright-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: becwright
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministically enforces constraints (BECs) on your code, blocking commits that violate them.
|
|
5
|
+
Author: Alonso David De Leon Rodarte
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: POSIX
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: pyyaml>=6
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
> **English** · [Español](README.es.md)
|
|
24
|
+
|
|
25
|
+
# becwright
|
|
26
|
+
|
|
27
|
+
[](https://github.com/DataDave-Dev/becwright/actions/workflows/ci.yml)
|
|
28
|
+
|
|
29
|
+
**Rules that run, not notes that get ignored.**
|
|
30
|
+
|
|
31
|
+
`becwright` enforces constraints on your code deterministically: instead of
|
|
32
|
+
*asking* an AI agent to respect a rule (the way `CLAUDE.md`, `.cursorrules`,
|
|
33
|
+
etc. do — which the agent can read and ignore), becwright **verifies the
|
|
34
|
+
result** and blocks the commit if the rule is broken.
|
|
35
|
+
|
|
36
|
+
## The problem
|
|
37
|
+
|
|
38
|
+
An AI agent writes code and leaves a note: *"this must never log session
|
|
39
|
+
tokens"*. That note is text. Three months later, another agent regenerates the
|
|
40
|
+
module, doesn't read it, and drops the token into the logs. Nobody notices
|
|
41
|
+
until it blows up in production.
|
|
42
|
+
|
|
43
|
+
Notes are **probabilistic** (they depend on the agent reading, understanding and
|
|
44
|
+
obeying). becwright is **deterministic**: the rule runs against the real code
|
|
45
|
+
and returns pass/fail, no matter which agent or model made the change.
|
|
46
|
+
|
|
47
|
+
| | Note in CLAUDE.md | becwright rule |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| What it does | *Asks* to be respected | *Verifies* it was respected |
|
|
50
|
+
| Depends on | The agent reading and obeying | Nothing — it runs against the code |
|
|
51
|
+
| Result | Likely | Guaranteed |
|
|
52
|
+
| Analogy | A "speed limit" sign | A physical bump in the road |
|
|
53
|
+
|
|
54
|
+
The two layers are complementary: CLAUDE.md prevents (so 95% comes out right the
|
|
55
|
+
first time), becwright is the safety net for the 5% that slips through.
|
|
56
|
+
|
|
57
|
+
## Core concept: BEC (Bound Executable Constraint)
|
|
58
|
+
|
|
59
|
+
A BEC is a constraint with three properties that no current artifact has
|
|
60
|
+
together:
|
|
61
|
+
|
|
62
|
+
- **Bound** — the rule is born tied to the *intent* and the decision that
|
|
63
|
+
created it (the *why*); it is not a loose rule without context.
|
|
64
|
+
- **Executable** — it carries a check that runs and returns pass/fail; it is not
|
|
65
|
+
prose someone promises to respect.
|
|
66
|
+
- **Portable** — it can be exported from one repo and imported into another,
|
|
67
|
+
like a package (this is what creates the network effect over time).
|
|
68
|
+
|
|
69
|
+
## How to use it
|
|
70
|
+
|
|
71
|
+
becwright is installed once as a tool; each repo only contributes its own
|
|
72
|
+
`.bec/rules.yaml`.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# 1. Install the engine (once, global)
|
|
76
|
+
pipx install git+https://github.com/DataDave-Dev/becwright.git # or local: pipx install .
|
|
77
|
+
|
|
78
|
+
# 2. In the repo where you want the rules, install the git hook
|
|
79
|
+
becwright install # writes .git/hooks/pre-commit
|
|
80
|
+
|
|
81
|
+
# 3. Write your rules in .bec/rules.yaml (see examples below)
|
|
82
|
+
# 4. Done: each commit runs the checks; if a blocking rule fails, it stops.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Available commands:
|
|
86
|
+
|
|
87
|
+
| Command | What it does |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `becwright check` | Runs the rules over the staged files |
|
|
90
|
+
| `becwright install` | Installs the native `pre-commit` hook |
|
|
91
|
+
| `becwright uninstall` | Removes the hook |
|
|
92
|
+
| `becwright export <id>` | Exports a BEC to a `.bec.yaml` file |
|
|
93
|
+
| `becwright import <file\|URL>` | Imports a BEC from another repo |
|
|
94
|
+
|
|
95
|
+
A rule in `.bec/rules.yaml`:
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
rules:
|
|
99
|
+
- id: no-token-in-logs
|
|
100
|
+
intent: >
|
|
101
|
+
Session tokens and credentials must never reach any log.
|
|
102
|
+
why_it_matters: >
|
|
103
|
+
If a token shows up in the logs, anyone with access to them can steal a
|
|
104
|
+
user's session.
|
|
105
|
+
paths: ["src/**/*.py"]
|
|
106
|
+
check: "python3 -m becwright.checks.no_token_in_logs"
|
|
107
|
+
severity: blocking # blocking = stops the commit | warning = only warns
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Included checks
|
|
111
|
+
|
|
112
|
+
becwright ships ready-to-use checks. Each one is a module invoked from the
|
|
113
|
+
`check` field. They are **text/regex based** (no AST analysis), so they are
|
|
114
|
+
conservative and may have edge cases; the value is in tying each rule to its
|
|
115
|
+
*why*.
|
|
116
|
+
|
|
117
|
+
| Check | What it detects | Language | Suggested severity |
|
|
118
|
+
|---|---|---|---|
|
|
119
|
+
| `forbid` | Any regex you pass (`--pattern`) | any | depends on the case |
|
|
120
|
+
| `no_token_in_logs` | Tokens/credentials in log calls | Python | `blocking` |
|
|
121
|
+
| `hardcoded_secrets` | AWS keys, private keys, `password = "..."` literals | any | `blocking` |
|
|
122
|
+
| `debug_remnants` | Forgotten `breakpoint()`, `pdb.set_trace()`, `import pdb` | Python | `blocking` |
|
|
123
|
+
| `dangerous_eval` | `eval()` / `exec()` calls | any | `blocking` |
|
|
124
|
+
| `wildcard_imports` | `from x import *` | Python | `warning` |
|
|
125
|
+
|
|
126
|
+
Example rules to copy into your `.bec/rules.yaml`:
|
|
127
|
+
|
|
128
|
+
```yaml
|
|
129
|
+
rules:
|
|
130
|
+
- id: no-hardcoded-secrets
|
|
131
|
+
intent: >
|
|
132
|
+
No secret (key, token, password) should be hardcoded in the code.
|
|
133
|
+
why_it_matters: >
|
|
134
|
+
A secret in the repo stays in git history forever and is visible to
|
|
135
|
+
anyone with access to the code.
|
|
136
|
+
paths: ["src/**/*.py"]
|
|
137
|
+
check: "python3 -m becwright.checks.hardcoded_secrets"
|
|
138
|
+
severity: blocking
|
|
139
|
+
|
|
140
|
+
- id: no-debug-remnants
|
|
141
|
+
intent: >
|
|
142
|
+
Debug code (breakpoints, pdb) must not be committed.
|
|
143
|
+
why_it_matters: >
|
|
144
|
+
A forgotten breakpoint hangs the process in production or CI.
|
|
145
|
+
paths: ["src/**/*.py"]
|
|
146
|
+
check: "python3 -m becwright.checks.debug_remnants"
|
|
147
|
+
severity: blocking
|
|
148
|
+
|
|
149
|
+
- id: no-dangerous-eval
|
|
150
|
+
intent: >
|
|
151
|
+
Do not use eval()/exec(), which execute arbitrary code.
|
|
152
|
+
why_it_matters: >
|
|
153
|
+
eval/exec on untrusted input is remote code execution.
|
|
154
|
+
paths: ["src/**/*.py"]
|
|
155
|
+
check: "python3 -m becwright.checks.dangerous_eval"
|
|
156
|
+
severity: blocking
|
|
157
|
+
|
|
158
|
+
- id: no-wildcard-imports
|
|
159
|
+
intent: >
|
|
160
|
+
Avoid 'from x import *', which pollutes the namespace.
|
|
161
|
+
why_it_matters: >
|
|
162
|
+
Wildcard imports hide where each name comes from and break static
|
|
163
|
+
analysis.
|
|
164
|
+
paths: ["src/**/*.py"]
|
|
165
|
+
check: "python3 -m becwright.checks.wildcard_imports"
|
|
166
|
+
severity: warning
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Any language
|
|
170
|
+
|
|
171
|
+
becwright is **language-agnostic**: the engine only filters files by their
|
|
172
|
+
`paths` (globs) and runs the `check` as a command; it never assumes Python. You
|
|
173
|
+
can watch JavaScript, Go, Rust, or anything else.
|
|
174
|
+
|
|
175
|
+
The fastest way to write a rule for another language —without writing code— is
|
|
176
|
+
the `forbid` check, which fails if a regex appears in the files:
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
rules:
|
|
180
|
+
- id: no-debugger-js
|
|
181
|
+
intent: >
|
|
182
|
+
Do not leave 'debugger;' in JavaScript/TypeScript code.
|
|
183
|
+
why_it_matters: >
|
|
184
|
+
A forgotten 'debugger' halts execution and should not reach production.
|
|
185
|
+
paths: ["**/*.js", "**/*.ts"]
|
|
186
|
+
check: "python3 -m becwright.checks.forbid --pattern '\\bdebugger\\b'"
|
|
187
|
+
severity: blocking
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`forbid` accepts `--pattern REGEX`, `--ignore-case` and `--message TEXT`. For
|
|
191
|
+
finer checks, write your own script in whatever language you want (an executable
|
|
192
|
+
that reads the file list from stdin and exits with code 0/1) and point `check`
|
|
193
|
+
at it.
|
|
194
|
+
|
|
195
|
+
## Sharing BECs between repos
|
|
196
|
+
|
|
197
|
+
A BEC is **portable**: you can take it out of one repo and install it in
|
|
198
|
+
another. A bundle is a single self-contained `.bec.yaml` file (the rule + the
|
|
199
|
+
check's code if it is custom).
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# In the source repo: export a rule to a file
|
|
203
|
+
becwright export no-token-in-logs -o no-token-in-logs.bec.yaml
|
|
204
|
+
|
|
205
|
+
# In another repo: import it (from a file or an http/https URL)
|
|
206
|
+
becwright import no-token-in-logs.bec.yaml
|
|
207
|
+
becwright import https://example.com/no-token-in-logs.bec.yaml
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
On import, becwright **shows the check's code and asks for confirmation** before
|
|
211
|
+
installing it: importing a BEC is importing code that will run on every commit.
|
|
212
|
+
Use `--yes` to skip the confirmation in automated environments.
|
|
213
|
+
|
|
214
|
+
There is a **catalog of ready-to-use BECs** in [`becs/`](becs/) that you can
|
|
215
|
+
import directly from their raw URL.
|
|
216
|
+
|
|
217
|
+
Built-in checks (`python3 -m becwright.checks.*`) travel with the package, so
|
|
218
|
+
the bundle only stores their name. A **custom** check (`.bec/checks/foo.py`)
|
|
219
|
+
travels with its code embedded and lands in `.bec/checks/` of the target repo.
|
|
220
|
+
|
|
221
|
+
## Current status
|
|
222
|
+
|
|
223
|
+
The **installable MVP** is built and verified end-to-end: packaged engine
|
|
224
|
+
(`src/becwright/`), CLI (`check` / `install` / `uninstall` / `export` /
|
|
225
|
+
`import`), native git hook that blocks a commit with a token in a log, included
|
|
226
|
+
checks (Python + the generic `forbid` for any language), BEC portability between
|
|
227
|
+
repos, a catalog with Python and JS/TS BECs, and a green test suite. The original
|
|
228
|
+
prototype is **archived** under `prototype/` as a reference.
|
|
229
|
+
|
|
230
|
+
Future work (AST analysis, deep per-language tooling, cryptographic signing of
|
|
231
|
+
verifications) is documented in the project plan.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
> **English** · [Español](README.es.md)
|
|
2
|
+
|
|
3
|
+
# becwright
|
|
4
|
+
|
|
5
|
+
[](https://github.com/DataDave-Dev/becwright/actions/workflows/ci.yml)
|
|
6
|
+
|
|
7
|
+
**Rules that run, not notes that get ignored.**
|
|
8
|
+
|
|
9
|
+
`becwright` enforces constraints on your code deterministically: instead of
|
|
10
|
+
*asking* an AI agent to respect a rule (the way `CLAUDE.md`, `.cursorrules`,
|
|
11
|
+
etc. do — which the agent can read and ignore), becwright **verifies the
|
|
12
|
+
result** and blocks the commit if the rule is broken.
|
|
13
|
+
|
|
14
|
+
## The problem
|
|
15
|
+
|
|
16
|
+
An AI agent writes code and leaves a note: *"this must never log session
|
|
17
|
+
tokens"*. That note is text. Three months later, another agent regenerates the
|
|
18
|
+
module, doesn't read it, and drops the token into the logs. Nobody notices
|
|
19
|
+
until it blows up in production.
|
|
20
|
+
|
|
21
|
+
Notes are **probabilistic** (they depend on the agent reading, understanding and
|
|
22
|
+
obeying). becwright is **deterministic**: the rule runs against the real code
|
|
23
|
+
and returns pass/fail, no matter which agent or model made the change.
|
|
24
|
+
|
|
25
|
+
| | Note in CLAUDE.md | becwright rule |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| What it does | *Asks* to be respected | *Verifies* it was respected |
|
|
28
|
+
| Depends on | The agent reading and obeying | Nothing — it runs against the code |
|
|
29
|
+
| Result | Likely | Guaranteed |
|
|
30
|
+
| Analogy | A "speed limit" sign | A physical bump in the road |
|
|
31
|
+
|
|
32
|
+
The two layers are complementary: CLAUDE.md prevents (so 95% comes out right the
|
|
33
|
+
first time), becwright is the safety net for the 5% that slips through.
|
|
34
|
+
|
|
35
|
+
## Core concept: BEC (Bound Executable Constraint)
|
|
36
|
+
|
|
37
|
+
A BEC is a constraint with three properties that no current artifact has
|
|
38
|
+
together:
|
|
39
|
+
|
|
40
|
+
- **Bound** — the rule is born tied to the *intent* and the decision that
|
|
41
|
+
created it (the *why*); it is not a loose rule without context.
|
|
42
|
+
- **Executable** — it carries a check that runs and returns pass/fail; it is not
|
|
43
|
+
prose someone promises to respect.
|
|
44
|
+
- **Portable** — it can be exported from one repo and imported into another,
|
|
45
|
+
like a package (this is what creates the network effect over time).
|
|
46
|
+
|
|
47
|
+
## How to use it
|
|
48
|
+
|
|
49
|
+
becwright is installed once as a tool; each repo only contributes its own
|
|
50
|
+
`.bec/rules.yaml`.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# 1. Install the engine (once, global)
|
|
54
|
+
pipx install git+https://github.com/DataDave-Dev/becwright.git # or local: pipx install .
|
|
55
|
+
|
|
56
|
+
# 2. In the repo where you want the rules, install the git hook
|
|
57
|
+
becwright install # writes .git/hooks/pre-commit
|
|
58
|
+
|
|
59
|
+
# 3. Write your rules in .bec/rules.yaml (see examples below)
|
|
60
|
+
# 4. Done: each commit runs the checks; if a blocking rule fails, it stops.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Available commands:
|
|
64
|
+
|
|
65
|
+
| Command | What it does |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `becwright check` | Runs the rules over the staged files |
|
|
68
|
+
| `becwright install` | Installs the native `pre-commit` hook |
|
|
69
|
+
| `becwright uninstall` | Removes the hook |
|
|
70
|
+
| `becwright export <id>` | Exports a BEC to a `.bec.yaml` file |
|
|
71
|
+
| `becwright import <file\|URL>` | Imports a BEC from another repo |
|
|
72
|
+
|
|
73
|
+
A rule in `.bec/rules.yaml`:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
rules:
|
|
77
|
+
- id: no-token-in-logs
|
|
78
|
+
intent: >
|
|
79
|
+
Session tokens and credentials must never reach any log.
|
|
80
|
+
why_it_matters: >
|
|
81
|
+
If a token shows up in the logs, anyone with access to them can steal a
|
|
82
|
+
user's session.
|
|
83
|
+
paths: ["src/**/*.py"]
|
|
84
|
+
check: "python3 -m becwright.checks.no_token_in_logs"
|
|
85
|
+
severity: blocking # blocking = stops the commit | warning = only warns
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Included checks
|
|
89
|
+
|
|
90
|
+
becwright ships ready-to-use checks. Each one is a module invoked from the
|
|
91
|
+
`check` field. They are **text/regex based** (no AST analysis), so they are
|
|
92
|
+
conservative and may have edge cases; the value is in tying each rule to its
|
|
93
|
+
*why*.
|
|
94
|
+
|
|
95
|
+
| Check | What it detects | Language | Suggested severity |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| `forbid` | Any regex you pass (`--pattern`) | any | depends on the case |
|
|
98
|
+
| `no_token_in_logs` | Tokens/credentials in log calls | Python | `blocking` |
|
|
99
|
+
| `hardcoded_secrets` | AWS keys, private keys, `password = "..."` literals | any | `blocking` |
|
|
100
|
+
| `debug_remnants` | Forgotten `breakpoint()`, `pdb.set_trace()`, `import pdb` | Python | `blocking` |
|
|
101
|
+
| `dangerous_eval` | `eval()` / `exec()` calls | any | `blocking` |
|
|
102
|
+
| `wildcard_imports` | `from x import *` | Python | `warning` |
|
|
103
|
+
|
|
104
|
+
Example rules to copy into your `.bec/rules.yaml`:
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
rules:
|
|
108
|
+
- id: no-hardcoded-secrets
|
|
109
|
+
intent: >
|
|
110
|
+
No secret (key, token, password) should be hardcoded in the code.
|
|
111
|
+
why_it_matters: >
|
|
112
|
+
A secret in the repo stays in git history forever and is visible to
|
|
113
|
+
anyone with access to the code.
|
|
114
|
+
paths: ["src/**/*.py"]
|
|
115
|
+
check: "python3 -m becwright.checks.hardcoded_secrets"
|
|
116
|
+
severity: blocking
|
|
117
|
+
|
|
118
|
+
- id: no-debug-remnants
|
|
119
|
+
intent: >
|
|
120
|
+
Debug code (breakpoints, pdb) must not be committed.
|
|
121
|
+
why_it_matters: >
|
|
122
|
+
A forgotten breakpoint hangs the process in production or CI.
|
|
123
|
+
paths: ["src/**/*.py"]
|
|
124
|
+
check: "python3 -m becwright.checks.debug_remnants"
|
|
125
|
+
severity: blocking
|
|
126
|
+
|
|
127
|
+
- id: no-dangerous-eval
|
|
128
|
+
intent: >
|
|
129
|
+
Do not use eval()/exec(), which execute arbitrary code.
|
|
130
|
+
why_it_matters: >
|
|
131
|
+
eval/exec on untrusted input is remote code execution.
|
|
132
|
+
paths: ["src/**/*.py"]
|
|
133
|
+
check: "python3 -m becwright.checks.dangerous_eval"
|
|
134
|
+
severity: blocking
|
|
135
|
+
|
|
136
|
+
- id: no-wildcard-imports
|
|
137
|
+
intent: >
|
|
138
|
+
Avoid 'from x import *', which pollutes the namespace.
|
|
139
|
+
why_it_matters: >
|
|
140
|
+
Wildcard imports hide where each name comes from and break static
|
|
141
|
+
analysis.
|
|
142
|
+
paths: ["src/**/*.py"]
|
|
143
|
+
check: "python3 -m becwright.checks.wildcard_imports"
|
|
144
|
+
severity: warning
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Any language
|
|
148
|
+
|
|
149
|
+
becwright is **language-agnostic**: the engine only filters files by their
|
|
150
|
+
`paths` (globs) and runs the `check` as a command; it never assumes Python. You
|
|
151
|
+
can watch JavaScript, Go, Rust, or anything else.
|
|
152
|
+
|
|
153
|
+
The fastest way to write a rule for another language —without writing code— is
|
|
154
|
+
the `forbid` check, which fails if a regex appears in the files:
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
rules:
|
|
158
|
+
- id: no-debugger-js
|
|
159
|
+
intent: >
|
|
160
|
+
Do not leave 'debugger;' in JavaScript/TypeScript code.
|
|
161
|
+
why_it_matters: >
|
|
162
|
+
A forgotten 'debugger' halts execution and should not reach production.
|
|
163
|
+
paths: ["**/*.js", "**/*.ts"]
|
|
164
|
+
check: "python3 -m becwright.checks.forbid --pattern '\\bdebugger\\b'"
|
|
165
|
+
severity: blocking
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`forbid` accepts `--pattern REGEX`, `--ignore-case` and `--message TEXT`. For
|
|
169
|
+
finer checks, write your own script in whatever language you want (an executable
|
|
170
|
+
that reads the file list from stdin and exits with code 0/1) and point `check`
|
|
171
|
+
at it.
|
|
172
|
+
|
|
173
|
+
## Sharing BECs between repos
|
|
174
|
+
|
|
175
|
+
A BEC is **portable**: you can take it out of one repo and install it in
|
|
176
|
+
another. A bundle is a single self-contained `.bec.yaml` file (the rule + the
|
|
177
|
+
check's code if it is custom).
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# In the source repo: export a rule to a file
|
|
181
|
+
becwright export no-token-in-logs -o no-token-in-logs.bec.yaml
|
|
182
|
+
|
|
183
|
+
# In another repo: import it (from a file or an http/https URL)
|
|
184
|
+
becwright import no-token-in-logs.bec.yaml
|
|
185
|
+
becwright import https://example.com/no-token-in-logs.bec.yaml
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
On import, becwright **shows the check's code and asks for confirmation** before
|
|
189
|
+
installing it: importing a BEC is importing code that will run on every commit.
|
|
190
|
+
Use `--yes` to skip the confirmation in automated environments.
|
|
191
|
+
|
|
192
|
+
There is a **catalog of ready-to-use BECs** in [`becs/`](becs/) that you can
|
|
193
|
+
import directly from their raw URL.
|
|
194
|
+
|
|
195
|
+
Built-in checks (`python3 -m becwright.checks.*`) travel with the package, so
|
|
196
|
+
the bundle only stores their name. A **custom** check (`.bec/checks/foo.py`)
|
|
197
|
+
travels with its code embedded and lands in `.bec/checks/` of the target repo.
|
|
198
|
+
|
|
199
|
+
## Current status
|
|
200
|
+
|
|
201
|
+
The **installable MVP** is built and verified end-to-end: packaged engine
|
|
202
|
+
(`src/becwright/`), CLI (`check` / `install` / `uninstall` / `export` /
|
|
203
|
+
`import`), native git hook that blocks a commit with a token in a log, included
|
|
204
|
+
checks (Python + the generic `forbid` for any language), BEC portability between
|
|
205
|
+
repos, a catalog with Python and JS/TS BECs, and a green test suite. The original
|
|
206
|
+
prototype is **archived** under `prototype/` as a reference.
|
|
207
|
+
|
|
208
|
+
Future work (AST analysis, deep per-language tooling, cryptographic signing of
|
|
209
|
+
verifications) is documented in the project plan.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Packaging for becwright. Target Python 3.12 (current environment has 3.14).
|
|
2
|
+
# Minimal dependencies on purpose: only pyyaml (project convention).
|
|
3
|
+
[build-system]
|
|
4
|
+
requires = ["setuptools>=61"]
|
|
5
|
+
build-backend = "setuptools.build_meta"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "becwright"
|
|
9
|
+
version = "0.1.0"
|
|
10
|
+
description = "Deterministically enforces constraints (BECs) on your code, blocking commits that violate them."
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.12"
|
|
13
|
+
authors = [{ name = "Alonso David De Leon Rodarte" }]
|
|
14
|
+
license = { text = "MIT" }
|
|
15
|
+
dependencies = ["pyyaml>=6"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Operating System :: POSIX",
|
|
23
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# The global command. `pipx install .` makes `becwright` available on PATH.
|
|
27
|
+
[project.scripts]
|
|
28
|
+
becwright = "becwright.cli:main"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = ["pytest>=8", "pytest-cov>=5"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
pythonpath = ["src"]
|
|
38
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
import textwrap
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from .rules import Rule
|
|
11
|
+
|
|
12
|
+
BUNDLE_VERSION = 1
|
|
13
|
+
|
|
14
|
+
_BUILTIN = re.compile(r"^python3?\s+-m\s+becwright\.checks\.(\w+)(?:\s+(.*))?$")
|
|
15
|
+
_PY_PATH = re.compile(r"[\w./-]+\.py")
|
|
16
|
+
_ITEM_INDENT = re.compile(r"^([ \t]+)-\s", re.MULTILINE)
|
|
17
|
+
_EMPTY_RULES = re.compile(r"^rules:[ \t]*(?:\[[ \t]*\]|\{[ \t]*\})[ \t]*$", re.MULTILINE)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BundleError(RuntimeError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _clean(value):
|
|
25
|
+
if isinstance(value, str):
|
|
26
|
+
return value.strip()
|
|
27
|
+
if isinstance(value, list):
|
|
28
|
+
return [v.strip() if isinstance(v, str) else v for v in value]
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _origin(root: Path) -> str:
|
|
33
|
+
res = subprocess.run(
|
|
34
|
+
["git", "config", "--get", "remote.origin.url"],
|
|
35
|
+
cwd=root, capture_output=True, text=True,
|
|
36
|
+
)
|
|
37
|
+
return res.stdout.strip() or root.name
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def classify_check(command: str, root: Path) -> dict:
|
|
41
|
+
command = command.strip()
|
|
42
|
+
builtin = _BUILTIN.match(command)
|
|
43
|
+
if builtin:
|
|
44
|
+
out = {"kind": "builtin", "module": builtin.group(1)}
|
|
45
|
+
if builtin.group(2):
|
|
46
|
+
out["args"] = builtin.group(2).strip()
|
|
47
|
+
return out
|
|
48
|
+
for token in _PY_PATH.findall(command):
|
|
49
|
+
candidate = root / token
|
|
50
|
+
if candidate.is_file():
|
|
51
|
+
return {
|
|
52
|
+
"kind": "script",
|
|
53
|
+
"filename": Path(token).name,
|
|
54
|
+
"source": candidate.read_text(encoding="utf-8"),
|
|
55
|
+
}
|
|
56
|
+
return {"kind": "command", "command": command}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def export_bec(rule: Rule, root: Path) -> str:
|
|
60
|
+
rule_fields: dict = {"id": rule.id}
|
|
61
|
+
if rule.intent:
|
|
62
|
+
rule_fields["intent"] = rule.intent
|
|
63
|
+
if rule.why_it_matters:
|
|
64
|
+
rule_fields["why_it_matters"] = rule.why_it_matters
|
|
65
|
+
if rule.rejected_alternatives:
|
|
66
|
+
rule_fields["rejected_alternatives"] = list(rule.rejected_alternatives)
|
|
67
|
+
rule_fields["paths"] = list(rule.paths)
|
|
68
|
+
rule_fields["severity"] = rule.severity
|
|
69
|
+
|
|
70
|
+
bundle = {
|
|
71
|
+
"becwright_bec": BUNDLE_VERSION,
|
|
72
|
+
"exported_from": _origin(root),
|
|
73
|
+
"rule": rule_fields,
|
|
74
|
+
"check": classify_check(rule.check, root),
|
|
75
|
+
}
|
|
76
|
+
return yaml.safe_dump(bundle, sort_keys=False, allow_unicode=True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parse_bundle(text: str) -> dict:
|
|
80
|
+
try:
|
|
81
|
+
data = yaml.safe_load(text)
|
|
82
|
+
except yaml.YAMLError as e:
|
|
83
|
+
raise BundleError(f"The bundle is not valid YAML: {e}")
|
|
84
|
+
if not isinstance(data, dict):
|
|
85
|
+
raise BundleError("The bundle is empty or malformed.")
|
|
86
|
+
if data.get("becwright_bec") != BUNDLE_VERSION:
|
|
87
|
+
raise BundleError(
|
|
88
|
+
f"Unsupported bundle version: {data.get('becwright_bec')!r} "
|
|
89
|
+
f"(expected {BUNDLE_VERSION})."
|
|
90
|
+
)
|
|
91
|
+
rule = data.get("rule")
|
|
92
|
+
check = data.get("check")
|
|
93
|
+
if not isinstance(rule, dict) or "id" not in rule:
|
|
94
|
+
raise BundleError("The bundle has no valid rule (missing 'rule.id').")
|
|
95
|
+
if not isinstance(check, dict) or "kind" not in check:
|
|
96
|
+
raise BundleError("The bundle has no valid check (missing 'check.kind').")
|
|
97
|
+
required = {"builtin": ("module",), "script": ("filename", "source"), "command": ("command",)}
|
|
98
|
+
kind = check["kind"]
|
|
99
|
+
if kind not in required:
|
|
100
|
+
raise BundleError(f"Unknown check kind: {kind!r}.")
|
|
101
|
+
missing = [f for f in required[kind] if not check.get(f)]
|
|
102
|
+
if missing:
|
|
103
|
+
raise BundleError(f"The '{kind}' check is missing fields: {', '.join(missing)}.")
|
|
104
|
+
return data
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def materialize(bundle: dict, root: Path) -> dict:
|
|
108
|
+
check = bundle["check"]
|
|
109
|
+
kind = check.get("kind")
|
|
110
|
+
if kind == "builtin":
|
|
111
|
+
command = f"python3 -m becwright.checks.{check['module']}"
|
|
112
|
+
if check.get("args"):
|
|
113
|
+
command += f" {check['args']}"
|
|
114
|
+
elif kind == "script":
|
|
115
|
+
filename = Path(check["filename"]).name
|
|
116
|
+
dest = root / ".bec" / "checks" / filename
|
|
117
|
+
source = check["source"]
|
|
118
|
+
if dest.exists() and dest.read_text(encoding="utf-8") != source:
|
|
119
|
+
raise BundleError(
|
|
120
|
+
f"A different check already exists at {dest}. Not overwriting it; resolve by hand."
|
|
121
|
+
)
|
|
122
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
dest.write_text(source, encoding="utf-8")
|
|
124
|
+
dest.chmod(0o755)
|
|
125
|
+
command = f"python3 .bec/checks/{filename}"
|
|
126
|
+
elif kind == "command":
|
|
127
|
+
command = check["command"]
|
|
128
|
+
else:
|
|
129
|
+
raise BundleError(f"Unknown check kind: {kind!r}")
|
|
130
|
+
|
|
131
|
+
rule = bundle["rule"]
|
|
132
|
+
out: dict = {"id": rule["id"]}
|
|
133
|
+
for key in ("intent", "why_it_matters", "rejected_alternatives"):
|
|
134
|
+
if rule.get(key):
|
|
135
|
+
out[key] = _clean(rule[key])
|
|
136
|
+
out["paths"] = rule.get("paths", [])
|
|
137
|
+
out["check"] = command
|
|
138
|
+
out["severity"] = rule.get("severity", "blocking")
|
|
139
|
+
return out
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def append_rule(rules_path: Path, rule_dict: dict) -> None:
|
|
143
|
+
dumped = yaml.safe_dump([rule_dict], sort_keys=False, allow_unicode=True)
|
|
144
|
+
if not rules_path.exists():
|
|
145
|
+
rules_path.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
rules_path.write_text("rules:\n" + textwrap.indent(dumped, " "), encoding="utf-8")
|
|
147
|
+
return
|
|
148
|
+
# Normalize an empty inline list (`rules: []`) so block items can follow it.
|
|
149
|
+
text = _EMPTY_RULES.sub("rules:", rules_path.read_text(encoding="utf-8"), count=1)
|
|
150
|
+
if text and not text.endswith("\n"):
|
|
151
|
+
text += "\n"
|
|
152
|
+
if not re.search(r"^rules:", text, re.MULTILINE):
|
|
153
|
+
text += "rules:\n"
|
|
154
|
+
# Match the indentation the file already uses for list items.
|
|
155
|
+
existing = _ITEM_INDENT.search(text)
|
|
156
|
+
prefix = existing.group(1) if existing else " "
|
|
157
|
+
rules_path.write_text(text + textwrap.indent(dumped, prefix), encoding="utf-8")
|
|
File without changes
|