From e13bf4fdf66071459b6dbc00b5616f48680f01bf Mon Sep 17 00:00:00 2001 From: teo1747 Date: Fri, 13 Nov 2026 00:58:27 +0100 Subject: [PATCH 1/2] hw/gen: --format json, the generator's findings for tools validate, generate and check take --format json: one JSON document on standard output and nothing else, its shape in hw/schemas/diagnostics.schema.json. It holds the status and exit code, every finding with its file, line, node path and message apart (and the line the text output prints), the rule it concerns (HW-001, HW-002, HW-003), each outdated generated file's diff, the files written and the boards checked. A failure of the generator itself, as the Python 3.11 one was, still yields a document (internal-error, exit status 3), its trace on standard error. EmbLink Studio's first stage reads it. The findings are taken apart from the HW-002 text in one place (emb_hwgen/diagnostics.py); every error the negative tests raise must come back whole from its parts, which found node paths holding a compatible (bindings.st,stm32f4-iwdg.class). The Diagnostics tests check each outcome's document against the schema. Text output is unchanged. Signed-off-by: teo1747 --- changes/feat-hwgen-json-diagnostics.md | 3 + docs/engineering/hwgen.md | 36 ++++++++ hw/gen/emb-hwgen.py | 64 ++++++++++++--- hw/gen/emb_hwgen/diagnostics.py | 58 +++++++++++++ hw/gen/test_hwgen.py | 109 ++++++++++++++++++++++++- hw/schemas/diagnostics.schema.json | 62 ++++++++++++++ 6 files changed, 318 insertions(+), 14 deletions(-) create mode 100644 changes/feat-hwgen-json-diagnostics.md create mode 100644 hw/gen/emb_hwgen/diagnostics.py create mode 100644 hw/schemas/diagnostics.schema.json diff --git a/changes/feat-hwgen-json-diagnostics.md b/changes/feat-hwgen-json-diagnostics.md new file mode 100644 index 00000000..e2a59111 --- /dev/null +++ b/changes/feat-hwgen-json-diagnostics.md @@ -0,0 +1,3 @@ +### Added (the generator's output for tools, 2026-11-13) + +- `emb-hwgen validate`, `generate` and `check` take `--format json`, which prints one JSON document for an IDE or a script (`hw/schemas/diagnostics.schema.json`). It gives the status and exit code, every error and warning with its file, line, node path and message as separate fields, the diff of each outdated generated file, and the files written ([hwgen](docs/engineering/hwgen.md)). A failure of the generator itself still yields a document, with exit status 3. EmbLink Studio's first stage reads it. diff --git a/docs/engineering/hwgen.md b/docs/engineering/hwgen.md index 5f4d05a6..641e062d 100644 --- a/docs/engineering/hwgen.md +++ b/docs/engineering/hwgen.md @@ -15,9 +15,45 @@ The generator needs Python 3 and PyYAML, the configuration tool's one dependency - Exit status 2 means a description error. Every error is printed as `file:line: node.path: message`, and all errors found are reported, sorted. - `check` exits 1 when a committed output differs from what the descriptions give, and prints the diff. +- Exit status 3 means the generator itself failed: a bug to report, not a description error. Never edit `generated/` by hand (HW-003). Change the YAML and regenerate. +### Output for tools: `--format json` + +`validate`, `generate` and `check` take `--format json`. The command then prints a single JSON document on standard output and nothing else, for an IDE such as EmbLink Studio or a script. `hw/schemas/diagnostics.schema.json` defines the document. The exit status is the same as for text output. + +```sh +python3 -I hw/gen/emb-hwgen.py validate --board boards/st/nucleo_f446re --format json +``` + +With `baud: fast` in the NUCLEO-F446RE's description, it prints: + +```json +{ + "tool": "emb-hwgen", "version": "0.1.0", "format": 1, "command": "validate", + "status": "errors", "exit_code": 2, "files": [], + "diagnostics": [ + {"severity": "error", "rule": "HW-002", "file": "boards/st/nucleo_f446re/board.yaml", "line": 26, + "path": "console.baud", "message": "expected integer, found string 'fast'", + "text": "boards/st/nucleo_f446re/board.yaml:26: console.baud: expected integer, found string 'fast'"} + ] +} +``` + +- **`status`** is one of four values: + - `ok` (exit 0): warnings are still possible; + - `errors` (exit 2): the descriptions are wrong; + - `stale` (exit 1): `check` found outdated outputs, or hardware facts outside a description; + - `internal-error` (exit 3): the document carries `error`, the exception's type and message, and the traceback goes to standard error. +- **Each finding** gives its file, line, node path and message as separate fields. The file is relative to the repository root, or absolute for a board kept outside it. It also keeps the line the text output prints, in `text`. +- **`rule`** says which requirement the finding concerns: + - HW-002: a description error or warning; + - HW-001: a hardware fact outside a description; + - HW-003: a generated file out of date. These findings carry `diff`, a unified diff from the committed file to the generator's output. +- **`files`** lists what `generate` wrote. **`boards`** counts what `check` checked. +- **`format`** is the document's own version. A new field keeps it; a changed or removed field bumps it. + ## Files | Path | Kind | Content | diff --git a/hw/gen/emb-hwgen.py b/hw/gen/emb-hwgen.py index 80c1de46..19141b8b 100644 --- a/hw/gen/emb-hwgen.py +++ b/hw/gen/emb-hwgen.py @@ -8,19 +8,24 @@ emb-hwgen.py check # every generated/ is up to date; no hardware facts in C (HW-001) emb-hwgen.py check --board-root ~/my-project # the boards a project keeps outside the tree emb-hwgen.py validate --board boards/st/nucleo_f446re + emb-hwgen.py validate --board boards/st/nucleo_f446re --format json # for tools and IDEs -Exit status 0 on success, 1 when check finds a difference, 2 on a description error; each -error is printed as 'file:line: node.path: message' (HW-002). +Exit status 0 on success, 1 when check finds a difference, 2 on a description error, 3 when the +generator itself fails; each error is printed as 'file:line: node.path: message' (HW-002). With +--format json the output is one JSON document instead (hw/schemas/diagnostics.schema.json): the +status, the exit code, each finding with its file, line, node path and message apart, and the +files written; nothing else goes to standard output. """ import argparse import difflib import os import sys +import traceback from pathlib import Path ROOT = Path(__file__).resolve().parent.parent.parent sys.path.insert(0, str(Path(__file__).resolve().parent)) -from emb_hwgen import HwError, facts, generate # noqa: E402 +from emb_hwgen import HwError, diagnostics, facts, generate # noqa: E402 def _boards(root): @@ -34,6 +39,9 @@ def cmd_generate(args): out.mkdir(parents=True, exist_ok=True) for name, text in files.items(): (out / name).write_text(text, encoding="utf-8") + if args.format == "json": + written = [os.path.relpath(out / name, ROOT) for name in files] + return _emit(args, "ok", 0, _warnings(warnings), written) for w in warnings: print(f"warning: {w}", file=sys.stderr) print(f"emb-hwgen: wrote {len(files)} files to {os.path.relpath(out, ROOT)}") @@ -43,9 +51,13 @@ def cmd_generate(args): def cmd_check(args): bad = 0 roots = [Path(r) for r in args.board_root] or [ROOT] + found = [] for r in roots: if not (r / "boards").is_dir(): - print(f"emb-hwgen: {r} has no boards/ directory (a board root holds boards///)", file=sys.stderr) + message = f"{r} has no boards/ directory (a board root holds boards///)" + if args.format == "json": + return _emit(args, "errors", 2, [diagnostics.finding(message, "error", None)]) + print(f"emb-hwgen: {message}", file=sys.stderr) return 2 boards = [b for r in roots for b in _boards(r)] for board in boards: @@ -61,49 +73,77 @@ def cmd_check(args): bad += 1 rel = os.path.relpath(path, ROOT) if want is None: - print(f"{rel}: not an output of emb-hwgen; remove it") + found.append(diagnostics.stale(rel, "not an output of emb-hwgen; remove it")) elif have is None: - print(f"{rel}: missing; run emb-hwgen.py generate --board {os.path.relpath(board, ROOT)}") + found.append(diagnostics.stale(rel, f"missing; run emb-hwgen.py generate --board {os.path.relpath(board, ROOT)}")) else: - sys.stdout.writelines(difflib.unified_diff(have.splitlines(True), want.splitlines(True), - f"{rel} (committed)", f"{rel} (generated)")) + diff = "".join(difflib.unified_diff(have.splitlines(True), want.splitlines(True), + f"{rel} (committed)", f"{rel} (generated)")) + found.append(diagnostics.stale(rel, "differs from what emb-hwgen generates now", diff)) # HW-001 for the tree, or for each board root: its boards hold no hand-written files, and its # own SoC families' C no address-like constant (A84) problems = [p for r in roots for p in facts.check(r)] + code = 1 if bad or problems else 0 + if args.format == "json": + found += [diagnostics.finding(p, "error", "HW-001", with_path=False) for p in problems] + return _emit(args, "stale" if code else "ok", code, found, boards=len(boards)) + for f in found: # as before: the diff itself, or the file and what to do + sys.stdout.write(f["diff"] if f["diff"] else f"{f['text']}\n") for p in problems: print(p) print(f"emb-hwgen check: {len(boards)} board(s), {bad} stale file(s), {len(problems)} hardware fact(s) outside a description") - return 1 if bad or problems else 0 + return code def cmd_validate(args): _, warnings = generate.generate(Path(args.board), ROOT) + if args.format == "json": + return _emit(args, "ok", 0, _warnings(warnings)) for w in warnings: print(f"warning: {w}", file=sys.stderr) print(f"emb-hwgen: {args.board} is valid") return 0 +def _warnings(warnings): + return [diagnostics.finding(w, "warning", "HW-002") for w in warnings] + + +def _emit(args, status, code, findings=(), files=(), boards=None, error=None): + sys.stdout.write(diagnostics.dumps(diagnostics.document(args.cmd, status, code, findings, files, boards, error))) + return code + + def main(argv=None): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + fmt = argparse.ArgumentParser(add_help=False) + fmt.add_argument("--format", choices=("text", "json"), default="text", + help="text (default), or one JSON document for tools (hw/schemas/diagnostics.schema.json)") sub = ap.add_subparsers(dest="cmd", required=True) - g = sub.add_parser("generate", help="write a board's generated outputs") + g = sub.add_parser("generate", parents=[fmt], help="write a board's generated outputs") g.add_argument("--board", required=True, help="the board directory, holding board.yaml") g.add_argument("--out", help="output directory (default: /generated)") - c = sub.add_parser("check", help="regenerate every board and compare with the committed outputs (HW-003), " + c = sub.add_parser("check", parents=[fmt], help="regenerate every board and compare with the committed outputs (HW-003), " "and look for hardware facts outside the descriptions (HW-001)") c.add_argument("--board-root", action="append", default=[], help="check the boards under DIR/boards/ instead of the tree's (repeatable)") - v = sub.add_parser("validate", help="validate a board and its SoC without writing anything") + v = sub.add_parser("validate", parents=[fmt], help="validate a board and its SoC without writing anything") v.add_argument("--board", required=True) args = ap.parse_args(argv) try: return {"generate": cmd_generate, "check": cmd_check, "validate": cmd_validate}[args.cmd](args) except HwError as e: + if args.format == "json": + return _emit(args, "errors", 2, [diagnostics.finding(line, "error", "HW-002") for line in e.errors]) for line in e.errors: print(line, file=sys.stderr) print(f"emb-hwgen: {len(e.errors)} error(s)", file=sys.stderr) return 2 + except Exception as e: # the generator's own fault: a tool still gets a document, the trace goes to stderr + if args.format != "json": + raise + traceback.print_exc() + return _emit(args, "internal-error", 3, error={"type": type(e).__name__, "message": str(e)}) if __name__ == "__main__": diff --git a/hw/gen/emb_hwgen/diagnostics.py b/hw/gen/emb_hwgen/diagnostics.py new file mode 100644 index 00000000..ff480e61 --- /dev/null +++ b/hw/gen/emb_hwgen/diagnostics.py @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Junior Deogracias +"""The generator's findings as data, for tools that read them (`--format json`, HW-002). + +Every error and warning the generator raises is a line 'file:line: node.path: message' (HW-002); +the HW-001 check's problems are 'file[:line]: message'. Here each becomes a record with those +parts apart, and a command's outcome one JSON document (hw/schemas/diagnostics.schema.json): what +ran, its status and exit code, its findings, and the files it wrote. A line that does not have the +shape keeps its whole text as the message, so nothing is lost. +""" +import json +import re + +from . import VERSION + +FORMAT = 1 # the document's own version: a field added keeps it, one changed or removed bumps it + +# a Windows drive is part of the file name; a node path has no spaces, and its segments may be +# compatibles (bindings.st,stm32f4-iwdg.class) +_LINE = re.compile(r"^(?P(?:[A-Za-z]:)?[^:\n]+?)(?::(?P\d+))?: " + r"(?:(?P\(root\)|\(file\)|[A-Za-z_][\w,+-]*(?:\.[\w,+-]+|\[\d+\])*): )?" + r"(?P.+)$", re.S) +_FACT = re.compile(r"^(?P(?:[A-Za-z]:)?[^:\n]+?)(?::(?P\d+))?: (?P.+)$", re.S) + + +def finding(text, severity, rule, with_path=True): + """One finding from its line of text; with_path false for the HW-001 check's lines, which name + no node.""" + m = (_LINE if with_path else _FACT).match(text) + if not m: + return _record(severity, rule, None, None, None, text, text) + return _record(severity, rule, m["file"], m["line"], m.groupdict().get("path"), m["message"], text) + + +def _record(severity, rule, file, line, path, message, text): + return {"severity": severity, "rule": rule, "file": file, "line": int(line) if line else None, + "path": path, "message": message, "text": text} + + +def stale(file, message, diff=None): + """A generated file that differs from what the generator writes now (HW-003).""" + rec = _record("error", "HW-003", file, None, None, message, f"{file}: {message}") + rec["diff"] = diff + return rec + + +def document(command, status, exit_code, findings=(), files=(), boards=None, error=None): + doc = {"tool": "emb-hwgen", "version": VERSION, "format": FORMAT, "command": command, + "status": status, "exit_code": exit_code, "diagnostics": list(findings), "files": list(files)} + if boards is not None: + doc["boards"] = boards + if error is not None: + doc["error"] = error + return doc + + +def dumps(doc): + return json.dumps(doc, indent=2, sort_keys=True) + "\n" diff --git a/hw/gen/test_hwgen.py b/hw/gen/test_hwgen.py index e1c18712..76eb3f9f 100644 --- a/hw/gen/test_hwgen.py +++ b/hw/gen/test_hwgen.py @@ -15,13 +15,21 @@ HERE = Path(__file__).resolve().parent ROOT = HERE.parent.parent sys.path.insert(0, str(HERE)) -from emb_hwgen import HwError, facts, generate, load, model, schema # noqa: E402 +from emb_hwgen import HwError, diagnostics, facts, generate, load, model, schema # noqa: E402 BOARD = "boards/st/nucleo_f446re" UNO = "boards/arduino/arduino_uno" PICO = "boards/rpi/pico2_w" +def assert_every_error_parses(test, errors): + """--format json takes every error apart into file, line, node path and message (Diagnostics): + each must come back as its own text.""" + for e in errors: + f = diagnostics.finding(e, "error", "HW-002") + test.assertEqual(f"{f['file']}:{f['line']}: {f['path']}: {f['message']}", e) + + class Root: """A scratch repository root holding copies of the descriptions a test edits.""" @@ -71,6 +79,7 @@ def expect(self, file, line, path, message): want = f"{file}:{line}: {path}: " hits = [e for e in cm.exception.errors if e.startswith(want) and re.search(message, e)] self.assertTrue(hits, f"expected '{want}...{message}', got:\n" + "\n".join(cm.exception.errors)) + assert_every_error_parses(self, cm.exception.errors) def board_line(self, needle): return self.root.line_of(f"{BOARD}/board.yaml", needle) @@ -266,6 +275,7 @@ def expect(self, board, file, needle, path, message): want = f"{file}:{self.root.line_of(file, needle)}: {path}: " hits = [e for e in cm.exception.errors if e.startswith(want) and re.search(message, e)] self.assertTrue(hits, f"expected '{want}...{message}', got:\n" + "\n".join(cm.exception.errors)) + assert_every_error_parses(self, cm.exception.errors) def test_pin_table(self): files, _ = generate.generate(ROOT / PICO, ROOT) @@ -461,6 +471,7 @@ def expect(self, message, file=f"{BOARD}/board.yaml", needle=None, path=None): want = f"{file}:{self.root.line_of(file, needle)}: {path}: " hits = [e for e in cm.exception.errors if e.startswith(want) and re.search(message, e)] self.assertTrue(hits, f"expected '{want}...{message}', got:\n" + "\n".join(cm.exception.errors)) + assert_every_error_parses(self, cm.exception.errors) def files(self): return generate.generate(self.root.dir / BOARD, self.root.dir)[0] @@ -663,6 +674,99 @@ def test_check_board_root(self): self.assertIn("has no boards/ directory", r.stderr) +class Diagnostics(unittest.TestCase): + """--format json: one document, valid against hw/schemas/diagnostics.schema.json, for each + outcome, and nothing else on standard output.""" + + def setUp(self): + import importlib.util + spec = importlib.util.spec_from_file_location("emb_hwgen_cli", HERE / "emb-hwgen.py") + self.cli = importlib.util.module_from_spec(spec) + spec.loader.exec_module(self.cli) + self.schema = schema.load_schema("diagnostics") + + def run_json(self, *argv): + import contextlib + import io + out, err = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): + code = self.cli.main(list(argv) + ["--format", "json"]) + doc = json.loads(out.getvalue()) + self.assertEqual(schema.validate(doc, self.schema), [], doc) + self.assertEqual(doc["exit_code"], code) + return doc + + def board(self, edit=None): + d = Path(tempfile.mkdtemp(prefix="hwgen-diag-")) + self.addCleanup(shutil.rmtree, d) + board = d / "boards" / "acme" / "acme_m4" + board.mkdir(parents=True) + text = (ROOT / "boards/arm/mps2_an386/board.yaml").read_text(encoding="utf-8") + text = text.replace("name: mps2_an386", "name: acme_m4") + if edit: + self.assertIn(edit[0], text) + text = text.replace(*edit) + (board / "board.yaml").write_text(text, encoding="utf-8") + return d, board + + def test_validate_ok_with_its_warnings(self): + doc = self.run_json("validate", "--board", str(ROOT / BOARD)) + self.assertEqual((doc["status"], doc["command"], doc["tool"]), ("ok", "validate", "emb-hwgen")) + warn = [f for f in doc["diagnostics"] if f["path"] == "cpu.mpu"] + self.assertTrue(warn and warn[0]["severity"] == "warning" and warn[0]["line"] >= 1, doc) + + def test_description_error_located(self): + d, board = self.board(("baud: 115200", "baud: fast")) + doc = self.run_json("validate", "--board", str(board)) + self.assertEqual((doc["status"], doc["exit_code"]), ("errors", 2)) + f = next(f for f in doc["diagnostics"] if f["path"] == "console.baud") + self.assertEqual((f["severity"], f["rule"]), ("error", "HW-002")) + self.assertTrue(f["file"].endswith("board.yaml")) + lines = (board / "board.yaml").read_text(encoding="utf-8").splitlines() + self.assertIn("baud: fast", lines[f["line"] - 1]) + self.assertRegex(f["message"], "expected integer") + + def test_generate_lists_its_files(self): + d, board = self.board() + doc = self.run_json("generate", "--board", str(board)) + self.assertEqual(doc["status"], "ok") + self.assertIn("hw_config.h", {Path(f).name for f in doc["files"]}) + self.assertTrue(all((ROOT / f).is_file() for f in doc["files"])) + + def test_check_stale_with_its_diff(self): + d, board = self.board() + self.run_json("generate", "--board", str(board)) + self.assertEqual(self.run_json("check", "--board-root", str(d))["status"], "ok") + text = (board / "board.yaml").read_text(encoding="utf-8") + (board / "board.yaml").write_text(text.replace("baud: 115200", "baud: 57600"), encoding="utf-8") + doc = self.run_json("check", "--board-root", str(d)) + self.assertEqual((doc["status"], doc["exit_code"], doc["boards"]), ("stale", 1, 1)) + stale = [f for f in doc["diagnostics"] if f["rule"] == "HW-003"] + self.assertTrue(any(f["diff"] and "57600" in f["diff"] for f in stale), doc) + + def test_check_without_boards(self): + d, board = self.board() + doc = self.run_json("check", "--board-root", str(board)) + self.assertEqual((doc["status"], doc["exit_code"]), ("errors", 2)) + self.assertIn("has no boards/ directory", doc["diagnostics"][0]["message"]) + + def test_internal_error_is_still_a_document(self): + from unittest import mock + with mock.patch.object(self.cli.generate, "generate", side_effect=RuntimeError("boom")): + doc = self.run_json("validate", "--board", str(ROOT / BOARD)) + self.assertEqual((doc["status"], doc["exit_code"]), ("internal-error", 3)) + self.assertEqual(doc["error"], {"type": "RuntimeError", "message": "boom"}) + + def test_findings_of_each_shape(self): + f = diagnostics.finding(r"C:\p\b.yaml:4: devices[2].cs: no such pin", "error", "HW-002") + self.assertEqual((f["file"], f["line"], f["path"]), (r"C:\p\b.yaml", 4, "devices[2].cs")) + f = diagnostics.finding("soc/x/board.c:7: address-like constant 0x40004000u; describe it", "error", + "HW-001", with_path=False) + self.assertEqual((f["line"], f["path"], f["message"][:12]), (7, None, "address-like")) + f = diagnostics.finding("no shape at all", "error", None) + self.assertEqual((f["file"], f["message"]), (None, "no shape at all")) + + class Avr(unittest.TestCase): """The ATmega328P and the Arduino Uno: separate address spaces, vectors, fixed-function pins, page-sized erase units, and HW-006's second linker output.""" @@ -730,6 +834,7 @@ def expect(self, file, line, path, message): want = f"{file}:{line}: {path}: " hits = [e for e in cm.exception.errors if e.startswith(want) and re.search(message, e)] self.assertTrue(hits, f"expected '{want}...{message}', got:\n" + "\n".join(cm.exception.errors)) + assert_every_error_parses(self, cm.exception.errors) def test_fixed_pin_with_another_signal(self): b = f"{UNO}/board.yaml" @@ -945,7 +1050,7 @@ def test_every_part_validates(self): class Schemas(unittest.TestCase): def test_subset_only(self): - for kind in ("soc", "board", "hw", "bindings"): + for kind in ("soc", "board", "hw", "bindings", "diagnostics"): schema.load_schema(kind) # raises on a keyword outside the supported subset def test_bool_is_not_integer(self): diff --git a/hw/schemas/diagnostics.schema.json b/hw/schemas/diagnostics.schema.json new file mode 100644 index 00000000..e1e9a879 --- /dev/null +++ b/hw/schemas/diagnostics.schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://emblinkos.org/schemas/hw/0/diagnostics.schema.json", + "title": "emb-hwgen's outcome, format 1 (--format json, HW-002)", + "description": "What one emb-hwgen command did, for tools and IDEs: its status and exit code, every finding with its file, line, node path and message apart, and the files it wrote. Format 1: a field added keeps the format, a field changed or removed bumps it.", + "type": "object", + "required": ["tool", "version", "format", "command", "status", "exit_code", "diagnostics", "files"], + "additionalProperties": false, + "properties": { + "tool": { "const": "emb-hwgen" }, + "version": { "type": "string", "description": "emb-hwgen's own version" }, + "format": { "const": 1 }, + "command": { "enum": ["generate", "check", "validate"] }, + "status": { + "enum": ["ok", "errors", "stale", "internal-error"], + "description": "ok (exit 0, warnings possible); errors (exit 2: the descriptions are wrong, or check found no boards/); stale (exit 1: check found generated files that differ, or hardware facts outside a description); internal-error (exit 3: the generator itself failed, see error)" + }, + "exit_code": { "enum": [0, 1, 2, 3] }, + "diagnostics": { "type": "array", "items": { "$ref": "#/$defs/finding" } }, + "files": { + "type": "array", + "items": { "type": "string" }, + "description": "generate: the files written, relative to the repository root" + }, + "boards": { "type": "integer", "minimum": 0, "description": "check: the boards checked" }, + "error": { + "type": "object", + "required": ["type", "message"], + "additionalProperties": false, + "properties": { + "type": { "type": "string", "description": "the Python exception's class" }, + "message": { "type": "string" } + } + } + }, + "$defs": { + "finding": { + "type": "object", + "required": ["severity", "rule", "file", "line", "path", "message", "text"], + "additionalProperties": false, + "properties": { + "severity": { "enum": ["error", "warning"] }, + "rule": { + "type": ["string", "null"], + "description": "HW-002 (a description error or warning), HW-001 (a hardware fact outside a description), HW-003 (a generated file out of date), or null" + }, + "file": { "type": ["string", "null"], "description": "relative to the repository root, or absolute for a board outside it" }, + "line": { "type": ["integer", "null"], "minimum": 1 }, + "path": { + "type": ["string", "null"], + "description": "the node in the description, dotted (console.baud, devices[2].cs), (root) or (file)" + }, + "message": { "type": "string" }, + "text": { "type": "string", "description": "the finding as the text output prints it" }, + "diff": { + "type": ["string", "null"], + "description": "HW-003 only: a unified diff from the committed file to what emb-hwgen generates now" + } + } + } + } +} From 31aae7e2ab61aa106c672421528a0e651965c30f Mon Sep 17 00:00:00 2001 From: teo1747 Date: Fri, 13 Nov 2026 06:21:07 +0100 Subject: [PATCH 2/2] hw/gen: diagnostics.py takes the file header Main checks every file's header since #143; this one was written before it. Signed-off-by: teo1747 --- hw/gen/emb_hwgen/diagnostics.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hw/gen/emb_hwgen/diagnostics.py b/hw/gen/emb_hwgen/diagnostics.py index ff480e61..e943bdec 100644 --- a/hw/gen/emb_hwgen/diagnostics.py +++ b/hw/gen/emb_hwgen/diagnostics.py @@ -1,5 +1,9 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2026 Junior Deogracias +# +# This file is part of EmbLinkRTOS, a deterministic real-time kernel for microcontrollers. +# Licensed under the Apache License, Version 2.0; see the LICENSE file at the repository root. + """The generator's findings as data, for tools that read them (`--format json`, HW-002). Every error and warning the generator raises is a line 'file:line: node.path: message' (HW-002);