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.
Files changed (33) hide show
  1. becwright-0.1.0/LICENSE +21 -0
  2. becwright-0.1.0/PKG-INFO +231 -0
  3. becwright-0.1.0/README.md +209 -0
  4. becwright-0.1.0/pyproject.toml +38 -0
  5. becwright-0.1.0/setup.cfg +4 -0
  6. becwright-0.1.0/src/becwright/__init__.py +1 -0
  7. becwright-0.1.0/src/becwright/bundle.py +157 -0
  8. becwright-0.1.0/src/becwright/checks/__init__.py +0 -0
  9. becwright-0.1.0/src/becwright/checks/dangerous_eval.py +34 -0
  10. becwright-0.1.0/src/becwright/checks/debug_remnants.py +36 -0
  11. becwright-0.1.0/src/becwright/checks/forbid.py +54 -0
  12. becwright-0.1.0/src/becwright/checks/hardcoded_secrets.py +46 -0
  13. becwright-0.1.0/src/becwright/checks/no_token_in_logs.py +37 -0
  14. becwright-0.1.0/src/becwright/checks/redundant_comments.py +83 -0
  15. becwright-0.1.0/src/becwright/checks/wildcard_imports.py +34 -0
  16. becwright-0.1.0/src/becwright/cli.py +179 -0
  17. becwright-0.1.0/src/becwright/engine.py +68 -0
  18. becwright-0.1.0/src/becwright/git.py +68 -0
  19. becwright-0.1.0/src/becwright/rules.py +40 -0
  20. becwright-0.1.0/src/becwright.egg-info/PKG-INFO +231 -0
  21. becwright-0.1.0/src/becwright.egg-info/SOURCES.txt +31 -0
  22. becwright-0.1.0/src/becwright.egg-info/dependency_links.txt +1 -0
  23. becwright-0.1.0/src/becwright.egg-info/entry_points.txt +2 -0
  24. becwright-0.1.0/src/becwright.egg-info/requires.txt +5 -0
  25. becwright-0.1.0/src/becwright.egg-info/top_level.txt +1 -0
  26. becwright-0.1.0/tests/test_bundle.py +312 -0
  27. becwright-0.1.0/tests/test_checks.py +19 -0
  28. becwright-0.1.0/tests/test_cli_and_git.py +142 -0
  29. becwright-0.1.0/tests/test_engine.py +21 -0
  30. becwright-0.1.0/tests/test_engine_integration.py +42 -0
  31. becwright-0.1.0/tests/test_forbid.py +56 -0
  32. becwright-0.1.0/tests/test_more_checks.py +104 -0
  33. becwright-0.1.0/tests/test_redundant_comments.py +32 -0
@@ -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.
@@ -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
+ [![CI](https://github.com/DataDave-Dev/becwright/actions/workflows/ci.yml/badge.svg)](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
+ [![CI](https://github.com/DataDave-Dev/becwright/actions/workflows/ci.yml/badge.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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