diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bcca7c7..b70b107 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -15,7 +15,7 @@ body: attributes: label: Relay version description: Run `relay --version`. - placeholder: relay 0.2.0 + placeholder: relay 0.2.1 validations: required: true - type: dropdown diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2a61b2..c2cbd38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: run: >- python -c "from pathlib import Path; required=['architecture.md','configuration.md','rules.md','timing-model.md', - 'limitations.md','security.md','tutorials.md']; + 'limitations.md','schemas.md','security.md','tutorials.md']; assert all((Path('docs') / name).is_file() for name in required)" - uses: actions/upload-artifact@v7 with: diff --git a/.gitignore b/.gitignore index 0747dc6..0e26ce6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ __pycache__/ build/ dist/ relay.toml +/relay-report.md +/timing-summary.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cdc9ae..76e7bd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.2.1 — 2026-07-29 + +- Added versioned JSON Schemas for Relay report schema 2.0 and parsed `relay.toml` configuration. +- Added `relay schema config` and `relay schema report`, with optional atomic `--output`. +- Included both schemas in the installed wheel and validated them against real reports and + configuration in CI. +- Updated the primary installation path to a directly installable, checksum-published GitHub + release wheel. +- Expanded `pathspec` compatibility to supported 1.x releases while retaining a `<2` upper bound. +- Removed generated reports from version control and ignored the documented local output names. + ## 0.2.0 — 2026-07-29 - Reframed Relay as a conservative local source-review tool rather than compiler-grade static diff --git a/README.md b/README.md index 14190d4..df0b88e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ **Relay shows what blocking code prevents from running.** Relay is a local-first source-review tool for timing and blocking risks in polyglot control -programs. Version 0.2.0 is an alpha: useful as a review assistant, but intentionally not presented +programs. Version 0.2.1 is an alpha: useful as a review assistant, but intentionally not presented as compiler-grade semantic analysis or safety certification. Analysis never executes, imports, compiles, or uploads the source being inspected. @@ -25,7 +25,14 @@ can be sensitive, and a review tool should not need to upload that source just t ## Install -Relay supports Python 3.9–3.14. +Relay 0.2.1 supports Python 3.9–3.14. Install the verified wheel from the GitHub release: + +```console +python -m pip install https://github.com/devkyato/Relay/releases/download/v0.2.1/relay_lint-0.2.1-py3-none-any.whl +relay --version +``` + +For development from a clone: ```powershell python -m venv .venv @@ -44,6 +51,7 @@ relay check examples relay check examples/arduino/blocking_robot.ino --format markdown --output relay-report.md relay explain examples/arduino/blocking_robot.ino --level beginner relay summary examples/arduino/blocking_robot.ino --config relay.toml.example --duration 5s +relay schema report --output relay-report.schema.json relay rules relay doctor . ``` @@ -63,7 +71,7 @@ Confidence: medium Exit status is non-zero when a finding reaches `--fail-on` (default: `warning`). Formats are `text`, `json`, `sarif`, and `markdown`. Use `--suggest` or `--suggestions-out relay-suggestions.md` for reviewable patterns; Relay never edits source in -0.2.0 and never claims a suggestion is automatically safe for machinery. +0.2.1 and never claims a suggestion is automatically safe for machinery. ## How it works @@ -108,6 +116,20 @@ emergency_stop = "checkEmergencyStop" See [configuration](docs/configuration.md) and the [timing model](docs/timing-model.md). +## Machine-readable contracts + +Relay bundles versioned JSON Schemas inside the installed package: + +```console +relay schema report --output relay-report.schema.json +relay schema config --output relay-config.schema.json +relay check . --format json --output relay-report.json +``` + +The report schema validates JSON schema version `2.0`. The configuration schema describes the +data produced by parsing `relay.toml`; Relay still performs the final cross-field validation. +See [schemas and integrations](docs/schemas.md). + ## Supported sources and rules | Language | Extensions | Analysis model | diff --git a/docs/architecture.md b/docs/architecture.md index 0a86988..d4c942a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -17,6 +17,8 @@ importantly, what it does not know. scheduler. 7. `reports` writes deterministic text, Markdown, JSON schema 2.0, and SARIF with stable fingerprints and explicit model metadata. +8. `schemas` packages the configuration and report contracts so an installed `relay` command can + furnish the exact files used by editors, CI jobs, and other integrations. `analyser.analyse_path` is the public orchestration boundary. Oh—one design choice I do not want to blur: no stage invokes a compiler, interpreter, hardware tool, network service, or target diff --git a/docs/configuration.md b/docs/configuration.md index 99b05a6..eb0761b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -20,3 +20,18 @@ The Python API uses the `RelayConfig` passed by its caller. Durations accept `us`, `ms`, `s`, and `m`. Unknown configuration keys and unknown rule IDs are rejected so a typo cannot silently change an analysis. Invalid values fail with exit code 2. + +## Configuration schema + +Relay 0.2.1 bundles a JSON Schema for the data model produced after parsing `relay.toml`: + +```console +relay schema config --output relay-config.schema.json +``` + +Editors and integrations can use that file for keys, primitive types, duration syntax, and rule +IDs. Relay remains the final validator because it also enforces relationships between values—for +example, `maximum_total_bytes` cannot be smaller than `maximum_file_bytes`. + +The schema is versioned with the release and is also available in +[`src/relay/schemas/relay-config.schema.json`](../src/relay/schemas/relay-config.schema.json). diff --git a/docs/releases/0.2.1.md b/docs/releases/0.2.1.md new file mode 100644 index 0000000..3e07d14 --- /dev/null +++ b/docs/releases/0.2.1.md @@ -0,0 +1,53 @@ +# Relay 0.2.1 + +After publishing 0.2.0, I went through the project again from the point of view of somebody who +had not built it locally. The analyser was there, the wheel worked, and the report format had a +version—but an integration still had to infer the contract from examples. I thought too on that +point that “stable” should mean Relay can hand you the exact schema, not only tell you its number. + +## What I furnished + +Relay now bundles two JSON Schemas: + +- `report` validates machine-readable `relay check --format json` output with schema version + `2.0`; +- `config` describes the data model produced by parsing `relay.toml`. + +You can print either schema or write it atomically: + +```console +relay schema report --output relay-report.schema.json +relay schema config --output relay-config.schema.json +``` + +Oh! On this part, I kept runtime validation authoritative. The configuration schema is useful for +editors and CI, while Relay still checks relationships such as the aggregate byte limit being at +least as large as the per-file limit. + +## Installation and compatibility + +The README now begins with a directly installable GitHub release wheel instead of treating an +editable development checkout as the normal installation path. The schemas are included inside +that wheel and tested through the installed command. + +This patch also accepts supported `pathspec` 1.x releases while retaining a `<2` upper bound. The +dependency update passed Relay’s complete Python 3.9–3.14 matrix before it was merged. + +## Clean repository state + +The example commands create `relay-report.md` and `timing-summary.md` locally. Those generated +files no longer live in version control, and the documented root-level names are ignored so trying +Relay does not leave a dirty checkout. + +## Compatibility + +- Relay runtime: Python 3.9–3.14 +- Analysed languages: Python, C, C++, Arduino, JavaScript, TypeScript, Java, C#, Go, Rust, + Kotlin, Swift, Ruby, and PHP +- JSON report schema: `2.0` (unchanged) +- SARIF: 2.1.0 (unchanged) +- Operation: local and offline after installation + +This remains an alpha review assistant, not a compiler, scheduler simulator, hardware model, or +safety certification system. The patch makes Relay easier to install and integrate without making +its semantic claims any larger. diff --git a/docs/roadmap.md b/docs/roadmap.md index 0e218ed..9792803 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,7 @@ # Roadmap -Relay 0.2.0 corrects the semantic overclaims in the first alpha. It adds qualified identities, +Relay 0.2.1 corrects the semantic overclaims in the first alpha and furnishes versioned schemas +for integrations. It adds qualified identities, async-aware Python handling, conservative cross-language profiles, explicit model metadata, reachability-scoped impacts, resource bounds, atomic reports, and reproducible-build verification. @@ -9,7 +10,6 @@ For 0.3.0, I am thinking about: - add source-level suppression directives with auditable reasons; - add opt-in compiler, Tree-sitter, or language-server front ends behind the existing capability boundary; -- publish a representative multi-language corpus with positive, negative, and ambiguous cases; - measure per-rule precision, recall, and false-positive budgets before marking any detector stable; - add mutation tests for rule boundaries and corpus regressions; diff --git a/docs/schemas.md b/docs/schemas.md new file mode 100644 index 0000000..8eb1c55 --- /dev/null +++ b/docs/schemas.md @@ -0,0 +1,50 @@ +# Schemas and integrations + +I thought about machine-readable output from the other side: a stable version number is useful, +but an integration still needs the actual contract. Relay therefore ships its schemas inside the +wheel instead of asking an editor or CI job to copy an example from the documentation. + +## Furnish a schema + +```console +relay schema report --output relay-report.schema.json +relay schema config --output relay-config.schema.json +``` + +Without `--output`, Relay writes the selected schema to standard output. Existing output files are +replaced atomically, and symlink targets are refused just like other Relay reports. + +## JSON reports + +```console +relay check firmware --format json --output relay-report.json +``` + +The bundled report schema describes schema version `2.0`, including summary counts, analysis +models, findings, confidence, source locations, evidence, impacts, call paths, and warnings. +Consumers should select a parser by `schema_version`, ignore no validation failure silently, and +pin the schema file from the same Relay release used to produce the report. + +Report objects reject unknown fields. Relay will increment `schema_version` for a breaking +machine-report change; consumers should not silently reinterpret a report from an unsupported +schema generation. + +The canonical source is +[`src/relay/schemas/relay-report-2.0.schema.json`](../src/relay/schemas/relay-report-2.0.schema.json). + +## Configuration + +The configuration schema describes the JSON-like data model obtained after parsing `relay.toml`. +It covers known sections and keys, types, rule IDs, duration strings, and positive resource +limits. Relay performs final semantic validation after schema validation because JSON Schema does +not express every useful relationship between configuration fields. + +The canonical source is +[`src/relay/schemas/relay-config.schema.json`](../src/relay/schemas/relay-config.schema.json). +The repository also carries intentionally +[valid](../examples/config/relay.valid.toml) and +[invalid](../examples/config/relay.invalid.toml) configuration fixtures so integrations can test +both outcomes. + +Oh—SARIF remains SARIF 2.1.0 rather than a Relay-specific schema. Relay includes stable +`relayFinding/v1` partial fingerprints and links SARIF output to the official SARIF schema. diff --git a/examples/config/relay.invalid.toml b/examples/config/relay.invalid.toml new file mode 100644 index 0000000..32722ac --- /dev/null +++ b/examples/config/relay.invalid.toml @@ -0,0 +1,4 @@ +# Intentionally invalid: use this fixture to verify that integrations reject unknown keys. +[analysis] +maximum_files = 0 +unknown_option = true diff --git a/examples/config/relay.valid.toml b/examples/config/relay.valid.toml new file mode 100644 index 0000000..c2f637a --- /dev/null +++ b/examples/config/relay.valid.toml @@ -0,0 +1,23 @@ +# A small valid Relay configuration for editor and CI integration tests. +[project] +target = "esp32" +entrypoints = ["setup", "loop"] + +[tasks.control_loop] +every = "20ms" +execution_context = "main-loop" + +[tasks.emergency_stop] +maximum_latency = "10ms" +execution_context = "main-loop" +safety_critical = true + +[functions] +control_loop = "updateMotors" +emergency_stop = "checkEmergencyStop" + +[analysis] +disable = ["RLY115"] +maximum_file_bytes = 2000000 +maximum_files = 2000 +maximum_total_bytes = 50000000 diff --git a/pyproject.toml b/pyproject.toml index 73f6e32..ccb84bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ [project.optional-dependencies] dev = [ "build>=1.0", + "jsonschema>=4.18,<5", "mypy>=1.8,<1.20", "pytest>=7.4", "pytest-cov>=4.1", diff --git a/relay-report.md b/relay-report.md deleted file mode 100644 index 14fe049..0000000 --- a/relay-report.md +++ /dev/null @@ -1,161 +0,0 @@ -# Relay analysis report - -Analysed 5 supported source file(s); found 10 issue(s). - -> Relay uses file-local AST analysis for Python and explicitly labelled structural models for other languages. Structural findings are review prompts, not compiler-verified facts. - -**Analysis models:** `cpp`: structural C-family model, `python`: Python AST (file-local) - -## HIGH RLY101 - -**Location:** `arduino/blocking_robot.ino:8` -**Confidence:** medium - -delay() blocks its current execution context for approximately 2.000 seconds. - -Evidence: - -```text -delay(2000) -``` - -Suggested approach: Use a millis()-based state transition. - -## HIGH RLY103 - -**Location:** `esp32/wifi_wait.cpp:5` -**Confidence:** medium - -This loop has no obvious cooperative yield or bounded exit. - -Evidence: - -```text -while (WiFi.status() != WL_CONNECTED) { -``` - -Suggested approach: Add a deadline, bounded exit, and cooperative yield. - -## HIGH RLY105 - -**Location:** `esp32/wifi_wait.cpp:5` -**Confidence:** medium - -Connection status is polled without an obvious deadline or attempt limit. - -Evidence: - -```text -while (WiFi.status() != WL_CONNECTED) { -``` - -Suggested approach: Bound attempts or elapsed time. - -## WARNING RLY113 - -**Location:** `esp32/wifi_wait.cpp:5` -**Confidence:** medium - -Repeated polling has no obvious delay, backoff, or attempt limit. - -Evidence: - -```text -while (WiFi.status() != WL_CONNECTED) { -``` - -Suggested approach: Add capped backoff and an attempt/deadline limit. - -## HIGH RLY103 - -**Location:** `micropython/controller.py:5` -**Confidence:** medium - -This loop has no obvious cooperative yield or bounded exit. - -Evidence: - -```text -while True: -``` - -Suggested approach: Add a deadline, bounded exit, and cooperative yield. - -## WARNING RLY113 - -**Location:** `micropython/controller.py:5` -**Confidence:** medium - -Repeated polling has no obvious delay, backoff, or attempt limit. - -Evidence: - -```text -while True: -``` - -Suggested approach: Add capped backoff and an attempt/deadline limit. - -## WARNING RLY115 - -**Location:** `micropython/controller.py:5` -**Confidence:** low - -Input is repeatedly read without obvious rate control. - -Evidence: - -```text -while True: -``` - -Suggested approach: Rate-limit sensor or input reads. - -## WARNING RLY102 - -**Location:** `micropython/controller.py:8` -**Confidence:** high - -time.sleep() blocks its current execution context for approximately 0.250 seconds. - -Evidence: - -```text -time.sleep(0.25) -``` - -Suggested approach: Use a cooperative timer, scheduler, or event-driven wait where appropriate. - -## HIGH RLY106 - -**Location:** `python/network_poll.py:6` -**Confidence:** medium - -requests.get() has no obvious explicit timeout. - -Evidence: - -```text -response = requests.get("https://device.invalid/status") -``` - -Suggested approach: Pass an explicit timeout and handle expiry. - -## WARNING RLY102 - -**Location:** `python/network_poll.py:7` -**Confidence:** high - -time.sleep() blocks its current execution context for approximately 1.000 seconds. - -Evidence: - -```text -time.sleep(1) -``` - -Suggested approach: Use a cooperative timer, scheduler, or event-driven wait where appropriate. - -## Analysis warnings - -- cpp: structural C-family model; type resolution, dynamic dispatch, macros, and build-configuration semantics are not modelled diff --git a/src/relay/__init__.py b/src/relay/__init__.py index 90791b0..4e670d1 100644 --- a/src/relay/__init__.py +++ b/src/relay/__init__.py @@ -12,4 +12,4 @@ "Severity", "analyse_path", ] -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/src/relay/cli.py b/src/relay/cli.py index 275dd5b..829c4b6 100644 --- a/src/relay/cli.py +++ b/src/relay/cli.py @@ -17,6 +17,7 @@ from .models import Severity from .reports import render, suggestions, write_report from .rules import RULES +from .schemas import schema_names, schema_text from .timing_summary import render_timing_summary _INIT_CONTENT = """# Relay timing contract. Relay estimates; it does not measure hardware timing. @@ -89,6 +90,9 @@ def build_parser() -> argparse.ArgumentParser: init = commands.add_parser("init", help="create a non-destructive relay.toml example") init.add_argument("--directory", type=Path, default=Path.cwd()) + schema = commands.add_parser("schema", help="print or write a bundled public schema") + schema.add_argument("name", choices=schema_names()) + schema.add_argument("--output", type=Path) commands.add_parser("rules", help="list the stable rule catalogue") doctor = commands.add_parser("doctor", help="check the local Relay environment") doctor.add_argument("path", nargs="?", type=Path, default=Path.cwd()) @@ -114,6 +118,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int: return _summary(arguments) if arguments.command == "init": return _init(arguments.directory) + if arguments.command == "schema": + return _schema(arguments.name, arguments.output) if arguments.command == "rules": return _rules() if arguments.command == "doctor": @@ -203,6 +209,16 @@ def _rules() -> int: return 0 +def _schema(name: str, output: Optional[Path]) -> int: + content = schema_text(name) + if output: + write_report(output, content) + print(f"Wrote {name} schema to {output}") + else: + sys.stdout.write(content) + return 0 + + def _doctor(path: Path, explicit_config: Optional[Path]) -> int: failures: List[str] = [] print(f"Relay version: {__version__} [ok]") diff --git a/src/relay/schemas/__init__.py b/src/relay/schemas/__init__.py new file mode 100644 index 0000000..4e18a7e --- /dev/null +++ b/src/relay/schemas/__init__.py @@ -0,0 +1,29 @@ +"""Bundled public schemas for Relay configuration and machine reports.""" + +from __future__ import annotations + +from importlib.resources import files + +_SCHEMA_FILES = { + "config": "relay-config.schema.json", + "report": "relay-report-2.0.schema.json", +} + + +def schema_text(name: str) -> str: + """Return a bundled schema as UTF-8 text.""" + + try: + filename = _SCHEMA_FILES[name] + except KeyError as exc: + raise ValueError(f"unknown schema: {name}") from exc + return files(__package__).joinpath(filename).read_text(encoding="utf-8") + + +def schema_names() -> tuple[str, ...]: + """Return the stable CLI names of bundled schemas.""" + + return tuple(sorted(_SCHEMA_FILES)) + + +__all__ = ["schema_names", "schema_text"] diff --git a/src/relay/schemas/relay-config.schema.json b/src/relay/schemas/relay-config.schema.json new file mode 100644 index 0000000..d7d3d75 --- /dev/null +++ b/src/relay/schemas/relay-config.schema.json @@ -0,0 +1,104 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/devkyato/Relay/v0.2.1/src/relay/schemas/relay-config.schema.json", + "title": "Relay configuration", + "description": "Schema for the data model produced by parsing relay.toml.", + "type": "object", + "additionalProperties": false, + "properties": { + "project": { + "type": "object", + "additionalProperties": false, + "properties": { + "target": { + "type": "string" + }, + "entrypoints": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "functions": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "tasks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/task" + } + }, + "analysis": { + "$ref": "#/$defs/analysis" + } + }, + "$defs": { + "duration": { + "type": "string", + "pattern": "^\\s*\\d+(?:\\.\\d+)?\\s*(?:[uU][sS]|[mM][sS]|[sS]|[mM])\\s*$" + }, + "rule_id": { + "type": "string", + "pattern": "^RLY1(?:0[1-9]|1[0-5])$" + }, + "task": { + "type": "object", + "additionalProperties": false, + "properties": { + "every": { + "$ref": "#/$defs/duration" + }, + "maximum_latency": { + "$ref": "#/$defs/duration" + }, + "execution_context": { + "type": "string", + "minLength": 1 + }, + "safety_critical": { + "type": "boolean" + } + } + }, + "analysis": { + "type": "object", + "additionalProperties": false, + "properties": { + "enable": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/rule_id" + } + }, + "disable": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/rule_id" + } + }, + "python_sleep_threshold": { + "$ref": "#/$defs/duration" + }, + "maximum_file_bytes": { + "type": "integer", + "minimum": 1 + }, + "maximum_files": { + "type": "integer", + "minimum": 1 + }, + "maximum_total_bytes": { + "type": "integer", + "minimum": 1 + } + } + } + } +} diff --git a/src/relay/schemas/relay-report-2.0.schema.json b/src/relay/schemas/relay-report-2.0.schema.json new file mode 100644 index 0000000..5aa731f --- /dev/null +++ b/src/relay/schemas/relay-report-2.0.schema.json @@ -0,0 +1,147 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/devkyato/Relay/v0.2.1/src/relay/schemas/relay-report-2.0.schema.json", + "title": "Relay report schema 2.0", + "description": "Machine-readable output from relay check --format json.", + "type": "object", + "additionalProperties": false, + "required": [ + "analysis_models", + "findings", + "relay_version", + "schema_version", + "summary", + "warnings" + ], + "properties": { + "schema_version": { + "const": "2.0" + }, + "relay_version": { + "type": "string", + "minLength": 1 + }, + "summary": { + "type": "object", + "additionalProperties": false, + "required": [ + "files", + "findings" + ], + "properties": { + "files": { + "type": "integer", + "minimum": 0 + }, + "findings": { + "type": "integer", + "minimum": 0 + } + } + }, + "analysis_models": { + "type": "object", + "additionalProperties": { + "type": "string", + "minLength": 1 + } + }, + "findings": { + "type": "array", + "items": { + "$ref": "#/$defs/finding" + } + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "$defs": { + "finding": { + "type": "object", + "additionalProperties": false, + "required": [ + "call_path", + "confidence", + "documentation", + "end_line", + "estimated_impact", + "evidence", + "explanation", + "file", + "message", + "remediation", + "rule_id", + "severity", + "start_line" + ], + "properties": { + "rule_id": { + "type": "string", + "pattern": "^RLY1(?:0[1-9]|1[0-5])$" + }, + "severity": { + "enum": [ + "info", + "warning", + "high", + "critical" + ] + }, + "confidence": { + "enum": [ + "low", + "medium", + "high" + ] + }, + "file": { + "type": "string", + "minLength": 1 + }, + "start_line": { + "type": "integer", + "minimum": 1 + }, + "end_line": { + "type": "integer", + "minimum": 1 + }, + "evidence": { + "type": "string" + }, + "message": { + "type": "string", + "minLength": 1 + }, + "explanation": { + "type": "string", + "minLength": 1 + }, + "remediation": { + "type": "string", + "minLength": 1 + }, + "documentation": { + "type": "string", + "minLength": 1 + }, + "estimated_impact": { + "type": "array", + "items": { + "type": "string" + } + }, + "call_path": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } +} diff --git a/tests/test_cli.py b/tests/test_cli.py index c78b781..cc33eba 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -42,3 +42,10 @@ def test_invalid_config(tmp_path: Path) -> None: def test_missing_path_is_invocation_error(tmp_path: Path) -> None: assert main(["check", str(tmp_path / "missing.ino")]) == 2 + + +def test_schema_command(tmp_path: Path, capsys: object) -> None: + output = tmp_path / "relay-report.schema.json" + assert main(["schema", "report", "--output", str(output)]) == 0 + assert '"const": "2.0"' in output.read_text(encoding="utf-8") + assert main(["schema", "config"]) == 0 diff --git a/tests/test_schemas.py b/tests/test_schemas.py new file mode 100644 index 0000000..66e1928 --- /dev/null +++ b/tests/test_schemas.py @@ -0,0 +1,57 @@ +import json +import sys +from pathlib import Path +from typing import Any, Dict + +import pytest +from jsonschema import Draft202012Validator, ValidationError + +if sys.version_info >= (3, 11): + import tomllib +else: # pragma: no cover - exercised by the Python 3.9/3.10 CI jobs + import tomli as tomllib + +from relay import analyse_path +from relay.config import ConfigError, RelayConfig +from relay.reports import render +from relay.schemas import schema_names, schema_text + +ROOT = Path(__file__).parents[1] +FIXTURE = Path(__file__).parent / "fixtures" / "blocking.ino" + + +def _schema(name: str) -> Dict[str, Any]: + value = json.loads(schema_text(name)) + assert isinstance(value, dict) + Draft202012Validator.check_schema(value) + return value + + +def test_report_schema_validates_real_output() -> None: + report = json.loads(render(analyse_path(FIXTURE), "json")) + Draft202012Validator(_schema("report")).validate(report) + + +def test_config_schema_matches_runtime_contract() -> None: + with (ROOT / "examples" / "config" / "relay.valid.toml").open("rb") as handle: + config = tomllib.load(handle) + Draft202012Validator(_schema("config")).validate(config) + RelayConfig.from_mapping(config) + uppercase_duration = {"tasks": {"poll": {"every": "10MS"}}} + Draft202012Validator(_schema("config")).validate(uppercase_duration) + RelayConfig.from_mapping(uppercase_duration) + + +def test_invalid_config_is_rejected_by_schema_and_runtime() -> None: + with (ROOT / "examples" / "config" / "relay.invalid.toml").open("rb") as handle: + invalid = tomllib.load(handle) + with pytest.raises(ValidationError): + Draft202012Validator(_schema("config")).validate(invalid) + with pytest.raises(ConfigError): + RelayConfig.from_mapping(invalid) + + +def test_schema_catalogue_is_stable() -> None: + assert schema_names() == ("config", "report") + with pytest.raises(ValueError, match="unknown schema"): + schema_text("missing") diff --git a/timing-summary.md b/timing-summary.md deleted file mode 100644 index c8061a7..0000000 --- a/timing-summary.md +++ /dev/null @@ -1,16 +0,0 @@ -# Relay declared timing summary - -This report lists declared release periods and detected blocking findings. It does not simulate scheduling, execution time, priority, pre-emption, cores, interrupts, or hardware. -Reference window: 0.000s to 5.000s - -- emergency_stop: declared maximum latency 0.010s -- read_distance: 100 nominal release point(s), every 0.050s -- send_telemetry: 10 nominal release point(s), every 0.500s -- update_motors: 250 nominal release point(s), every 0.020s - -## Detected blocking findings - -- arduino/blocking_robot.ino:8: RLY101 delay() blocks its current execution context for approximately 2.000 seconds. -- micropython/controller.py:8: RLY102 time.sleep() blocks its current execution context for approximately 0.250 seconds. -- python/network_poll.py:6: RLY106 requests.get() has no obvious explicit timeout. -- python/network_poll.py:7: RLY102 time.sleep() blocks its current execution context for approximately 1.000 seconds.