Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions documentation/usage.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ rules:
| `severity` | no | `blocking` (por defecto), `warning` o `advisory` (ver abajo) |
| `target` | no | `files` (por defecto) o `commit-msg` (ver abajo) |

> **Autocompletado y validación en el editor.** `.bec/rules.yaml` tiene un
> [JSON Schema](../schema/rules.schema.json) publicado. `becwright init` escribe
> una línea `# yaml-language-server: $schema=…` al inicio del archivo, así los
> editores con language server de YAML (la extensión YAML de VS Code, los IDEs
> de JetBrains) autocompletan los campos y marcan un typo (`pathss:`,
> `severity: blockng`) mientras escribís. Agregá esa línea a mano en un archivo
> de reglas preexistente para obtener lo mismo.

**`schema_version`** es una clave opcional de nivel superior (no un campo de
regla). Sella la versión de formato del archivo; cuando falta se trata como `1`,
así que los archivos existentes siguen funcionando. `becwright init` la escribe,
Expand Down
7 changes: 7 additions & 0 deletions documentation/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ rules:
| `severity` | no | `blocking` (default), `warning`, or `advisory` (see below) |
| `target` | no | `files` (default) or `commit-msg` (see below) |

> **Editor autocompletion and validation.** `.bec/rules.yaml` has a published
> [JSON Schema](../schema/rules.schema.json). `becwright init` writes a
> `# yaml-language-server: $schema=…` line at the top of the file, so editors
> with a YAML language server (VS Code's YAML extension, JetBrains IDEs)
> autocomplete the fields and flag a typo (`pathss:`, `severity: blockng`) as
> you type. Add that line by hand to a pre-existing rules file to get the same.

**`schema_version`** is an optional top-level key (not a rule field). It stamps
the format version of the file; when absent it is treated as `1`, so existing
files keep working. `becwright init` writes it, and becwright refuses a file
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ classifiers = [
becwright = "becwright.cli:main"

[project.optional-dependencies]
dev = ["pytest>=8", "pytest-cov>=5", "mcp>=1.2"]
# jsonschema is test-only: it validates schema/rules.schema.json against examples.
dev = ["pytest>=8", "pytest-cov>=5", "mcp>=1.2", "jsonschema>=4"]
# Used to freeze the standalone binary distributed via npm (see packaging/).
build = ["pyinstaller>=6"]
# MCP server for AI agents: `becwright mcp` (see src/becwright/mcp_server.py).
Expand Down
72 changes: 72 additions & 0 deletions schema/rules.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/DataDave-Dev/becwright/main/schema/rules.schema.json",
"title": "becwright rules",
"description": "Schema for .bec/rules.yaml — the rules becwright enforces on every commit. Field reference: https://github.com/DataDave-Dev/becwright/blob/main/documentation/usage.md",
"type": "object",
"additionalProperties": false,
"properties": {
"schema_version": {
"type": "integer",
"minimum": 1,
"description": "Format version of this file; absent means 1. becwright refuses a file stamped newer than it understands."
},
"rules": {
"type": "array",
"description": "The rules (BECs) enforced on every commit.",
"items": { "$ref": "#/definitions/rule" }
}
},
"definitions": {
"rule": {
"type": "object",
"additionalProperties": false,
"required": ["id", "check"],
"properties": {
"id": {
"type": "string",
"minLength": 1,
"description": "Unique rule id, e.g. no-token-in-logs."
},
"intent": {
"type": "string",
"description": "What the rule asks for — the 'bound' half of the BEC."
},
"why_it_matters": {
"type": "string",
"description": "Why the rule exists; shown the moment the rule fails."
},
"rejected_alternatives": {
"type": "array",
"items": { "type": "string" },
"description": "Context: approaches that were considered and dropped."
},
"paths": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"description": "Glob patterns of the files this rule applies to (e.g. src/**/*.py). Not needed for target: commit-msg rules."
},
"exclude": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"description": "Globs carved out of `paths` — vendored code, generated files, the check's own implementation."
},
"check": {
"type": "string",
"minLength": 1,
"description": "Shell command that runs the check: reads the file list on stdin, exits 0 (pass) or non-zero (fail). E.g. becwright run forbid --pattern '...'"
},
"severity": {
"enum": ["blocking", "warning", "advisory"],
"default": "blocking",
"description": "blocking = stops the commit | warning = deterministic finding, does not block | advisory = best-effort / non-deterministic, reports but never blocks."
},
"target": {
"enum": ["files", "commit-msg"],
"default": "files",
"description": "files = the changed files (default) | commit-msg = the commit message."
}
}
}
}
}
1 change: 1 addition & 0 deletions src/becwright/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,7 @@ def _rules_from_claude_md(text: str, langs: list[str]) -> list[tuple[dict, str]]

def _render_rules_yaml(rules: list[dict]) -> str:
header = (
"# yaml-language-server: $schema=https://raw.githubusercontent.com/DataDave-Dev/becwright/main/schema/rules.schema.json\n"
"# becwright rules - generated by `becwright init`. Tune them to your repo.\n"
"# More rules: `becwright search` to list the catalog, `becwright add <name>` to install.\n"
"# Docs: https://github.com/DataDave-Dev/becwright/tree/main/documentation\n"
Expand Down
80 changes: 80 additions & 0 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import json
from pathlib import Path

import pytest
import yaml

from becwright import cli

_SCHEMA_PATH = Path(__file__).resolve().parents[1] / "schema" / "rules.schema.json"
_REPO_RULES = Path(__file__).resolve().parents[1] / ".bec" / "rules.yaml"

jsonschema = pytest.importorskip("jsonschema")


def _schema():
return json.loads(_SCHEMA_PATH.read_text(encoding="utf-8"))


def _validate(data):
jsonschema.validate(data, _schema())


def test_schema_is_valid_jsonschema():
jsonschema.Draft7Validator.check_schema(_schema())


def test_valid_rules_file_passes():
_validate({
"schema_version": 1,
"rules": [{
"id": "no-token-in-logs",
"intent": "Tokens must never reach a log.",
"why_it_matters": "A token in the logs lets anyone steal a session.",
"rejected_alternatives": ["Redact at log time -> too easy to bypass"],
"paths": ["src/**/*.py"],
"exclude": ["src/logging_setup.py"],
"check": "becwright run no_token_in_logs",
"severity": "blocking",
}],
})


def test_commit_msg_rule_needs_no_paths():
_validate({
"rules": [{
"id": "conventional-commits",
"target": "commit-msg",
"check": "becwright run require --pattern '^feat'",
}],
})


def test_typoed_field_fails():
with pytest.raises(jsonschema.ValidationError):
_validate({
"rules": [{
"id": "x", "check": "true",
"pathss": ["**/*.py"], # the typo the schema exists to catch
}],
})


def test_invalid_severity_fails():
with pytest.raises(jsonschema.ValidationError):
_validate({"rules": [{"id": "x", "check": "true", "severity": "blockng"}]})


def test_missing_check_fails():
with pytest.raises(jsonschema.ValidationError):
_validate({"rules": [{"id": "x", "paths": ["**/*.py"]}]})


def test_generated_init_output_validates():
rendered = cli._render_rules_yaml(cli._starter_rules(["python", "ts"]))
assert rendered.startswith("# yaml-language-server: $schema=")
_validate(yaml.safe_load(rendered))


def test_this_repos_rules_validate():
_validate(yaml.safe_load(_REPO_RULES.read_text(encoding="utf-8")))
Loading