roschema 0.1.2__tar.gz → 0.2.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 (128) hide show
  1. {roschema-0.1.2 → roschema-0.2.0}/.github/workflows/ci.yml +3 -1
  2. {roschema-0.1.2 → roschema-0.2.0}/.github/workflows/release.yml +1 -0
  3. roschema-0.2.0/CHANGELOG.md +120 -0
  4. {roschema-0.1.2 → roschema-0.2.0}/PKG-INFO +29 -7
  5. {roschema-0.1.2 → roschema-0.2.0}/README.md +28 -6
  6. roschema-0.2.0/action/action.yml +65 -0
  7. {roschema-0.1.2 → roschema-0.2.0}/docs/rules.md +9 -9
  8. {roschema-0.1.2 → roschema-0.2.0}/pyproject.toml +4 -5
  9. roschema-0.2.0/src/roschema/__init__.py +8 -0
  10. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/cli.py +146 -44
  11. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/config.py +118 -55
  12. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/deep.py +21 -7
  13. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/differ.py +48 -30
  14. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/discovery/discover.py +6 -5
  15. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/discovery/model.py +3 -3
  16. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/discovery/resolve.py +58 -17
  17. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/freeze.py +1 -3
  18. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/ir.py +2 -6
  19. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/loader.py +1 -3
  20. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/mcap_fs.py +109 -35
  21. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/parser/__init__.py +1 -1
  22. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/parser/core.py +17 -19
  23. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/report/github.py +13 -3
  24. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/report/json_report.py +15 -9
  25. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/report/summary.py +2 -6
  26. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs002_field_added.py +1 -1
  27. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs006_array_kind_changed.py +5 -4
  28. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs007_bound_tightened.py +1 -1
  29. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs008_bound_loosened.py +1 -1
  30. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs060_message_version.py +1 -4
  31. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs110_transitive.py +2 -1
  32. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/gendocs.py +3 -4
  33. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/lint/rsl005_discouraged_types.py +6 -4
  34. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/lint/rsl006_unbounded.py +2 -6
  35. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/model.py +1 -0
  36. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/vfs.py +109 -8
  37. {roschema-0.1.2 → roschema-0.2.0}/tests/_helpers.py +9 -4
  38. roschema-0.2.0/tests/test_action.py +100 -0
  39. roschema-0.2.0/tests/test_check_cli.py +402 -0
  40. roschema-0.2.0/tests/test_cli_version.py +18 -0
  41. roschema-0.2.0/tests/test_config.py +281 -0
  42. roschema-0.2.0/tests/test_deep.py +153 -0
  43. {roschema-0.1.2 → roschema-0.2.0}/tests/test_differ.py +53 -20
  44. {roschema-0.1.2 → roschema-0.2.0}/tests/test_discovery.py +19 -1
  45. {roschema-0.1.2 → roschema-0.2.0}/tests/test_fmt.py +1 -5
  46. {roschema-0.1.2 → roschema-0.2.0}/tests/test_freeze.py +11 -3
  47. {roschema-0.1.2 → roschema-0.2.0}/tests/test_lint.py +54 -0
  48. roschema-0.2.0/tests/test_mcap.py +491 -0
  49. {roschema-0.1.2 → roschema-0.2.0}/tests/test_parser.py +19 -3
  50. {roschema-0.1.2 → roschema-0.2.0}/tests/test_report_formats.py +13 -0
  51. {roschema-0.1.2 → roschema-0.2.0}/tests/test_vfs.py +56 -1
  52. roschema-0.1.2/CHANGELOG.md +0 -55
  53. roschema-0.1.2/action/action.yml +0 -51
  54. roschema-0.1.2/src/roschema/__init__.py +0 -3
  55. roschema-0.1.2/tests/test_check_cli.py +0 -182
  56. roschema-0.1.2/tests/test_config.py +0 -149
  57. roschema-0.1.2/tests/test_deep.py +0 -75
  58. roschema-0.1.2/tests/test_mcap.py +0 -196
  59. {roschema-0.1.2 → roschema-0.2.0}/.gitignore +0 -0
  60. {roschema-0.1.2 → roschema-0.2.0}/LICENSE +0 -0
  61. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/discovery/__init__.py +0 -0
  62. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/formatter.py +0 -0
  63. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/parser/errors.py +0 -0
  64. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/py.typed +0 -0
  65. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/report/__init__.py +0 -0
  66. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/report/text.py +0 -0
  67. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/__init__.py +0 -0
  68. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/__init__.py +0 -0
  69. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs001_field_removed.py +0 -0
  70. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs003_field_type_changed.py +0 -0
  71. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs004_fields_reordered.py +0 -0
  72. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs005_field_renamed.py +0 -0
  73. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs020_constant_removed.py +0 -0
  74. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs021_constant_value_changed.py +0 -0
  75. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs022_constant_type_changed.py +0 -0
  76. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs030_default_changed.py +0 -0
  77. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/breaking/rs040_interface_removed.py +0 -0
  78. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/lint/__init__.py +0 -0
  79. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/lint/_common.py +0 -0
  80. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/lint/rsl001_field_snake_case.py +0 -0
  81. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/lint/rsl002_constant_upper_snake.py +0 -0
  82. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/lint/rsl003_name_pascal_case.py +0 -0
  83. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/lint/rsl004_missing_doc.py +0 -0
  84. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/lint/rsl010_srv_shadow.py +0 -0
  85. {roschema-0.1.2 → roschema-0.2.0}/src/roschema/rules/registry.py +0 -0
  86. {roschema-0.1.2 → roschema-0.2.0}/tests/conftest.py +0 -0
  87. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/AMENT_IGNORE +0 -0
  88. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/COLCON_IGNORE +0 -0
  89. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/golden_check.json +0 -0
  90. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/action/Move.action +0 -0
  91. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/msg/ArrayKind.msg +0 -0
  92. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/msg/BoundLoose.msg +0 -0
  93. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/msg/BoundTight.msg +0 -0
  94. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/msg/BoundedNest.msg +0 -0
  95. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/msg/Constants.msg +0 -0
  96. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/msg/Empty.msg +0 -0
  97. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/msg/FieldAdded.msg +0 -0
  98. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/msg/FieldRemoved.msg +0 -0
  99. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/msg/Renamed.msg +0 -0
  100. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/msg/Reordered.msg +0 -0
  101. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/msg/Suppressed.msg +0 -0
  102. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/msg/TypeChanged.msg +0 -0
  103. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/msg/Unchanged.msg +0 -0
  104. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/msg/WeirdSpacing.msg +0 -0
  105. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/package.xml +0 -0
  106. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_new/demo_msgs/srv/DoThing.srv +0 -0
  107. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/action/Move.action +0 -0
  108. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/ArrayKind.msg +0 -0
  109. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/BoundLoose.msg +0 -0
  110. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/BoundTight.msg +0 -0
  111. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/BoundedNest.msg +0 -0
  112. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/Constants.msg +0 -0
  113. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/Empty.msg +0 -0
  114. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/FieldAdded.msg +0 -0
  115. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/FieldRemoved.msg +0 -0
  116. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/Removed.msg +0 -0
  117. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/Renamed.msg +0 -0
  118. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/Reordered.msg +0 -0
  119. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/Suppressed.msg +0 -0
  120. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/TypeChanged.msg +0 -0
  121. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/Unchanged.msg +0 -0
  122. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/msg/WeirdSpacing.msg +0 -0
  123. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/package.xml +0 -0
  124. {roschema-0.1.2 → roschema-0.2.0}/tests/fixtures/ws_old/demo_msgs/srv/DoThing.srv +0 -0
  125. {roschema-0.1.2 → roschema-0.2.0}/tests/test_cli_ls.py +0 -0
  126. {roschema-0.1.2 → roschema-0.2.0}/tests/test_gendocs.py +0 -0
  127. {roschema-0.1.2 → roschema-0.2.0}/tests/test_golden.py +0 -0
  128. {roschema-0.1.2 → roschema-0.2.0}/tests/test_rules.py +0 -0
@@ -22,7 +22,9 @@ jobs:
22
22
  - name: Install
23
23
  run: uv sync --extra dev --extra bag
24
24
  - name: Ruff
25
- run: uv run ruff check .
25
+ run: |
26
+ uv run ruff check .
27
+ uv run ruff format --check .
26
28
  - name: Mypy
27
29
  run: uv run mypy --strict src
28
30
  - name: Rule docs in sync
@@ -20,6 +20,7 @@ jobs:
20
20
  run: |
21
21
  uv run pytest -q
22
22
  uv run ruff check .
23
+ uv run ruff format --check .
23
24
  uv run mypy --strict src
24
25
  uv run python -m roschema.rules.gendocs --check
25
26
  - name: Tag matches package version
@@ -0,0 +1,120 @@
1
+ # Changelog
2
+
3
+ The format follows [Keep a Changelog](https://keepachangelog.com/), and the
4
+ project adheres to [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [0.2.0] - 2026-11-03
7
+
8
+ ### Added
9
+
10
+ - `bag-check --level` and `--config`.
11
+ - Git baselines read initialised submodules at their recorded commit, and warn
12
+ when that commit is not available locally.
13
+
14
+ ### Changed
15
+
16
+ - `bag-check` reports at the `wire` level by default, since constant and other
17
+ source-level changes do not stop current code decoding a recording.
18
+ - Package-name targets resolve to the workspace source package before the
19
+ installed copy, with a warning when the installed copy is used.
20
+ - Adding or removing a sequence bound (`T[]` <-> `T[<=N]`) is reported as a
21
+ bound change (RS007/RS008) rather than an array-kind change (RS006).
22
+ - RS110 takes the severity of the change it traces back to and ignores
23
+ suppressed, allowlisted or disabled changes.
24
+ - Configuring a rule's severity no longer reports it outside the selected
25
+ level; only rules that are off by default (such as RS060) bypass the level.
26
+ - The GitHub Action passes `--level` only when set, so configured levels apply.
27
+ - RS002 is tagged `wire` only, and RSL005 explains why `char` and scalar `byte`
28
+ are ambiguous instead of calling them deprecated.
29
+
30
+ ### Fixed
31
+
32
+ - `bag-check` skips schemas recorded without a definition and compares service
33
+ event schemas (`SRV:` blocks) instead of failing.
34
+ - A same-package type written with its package (`pkg/Type` inside `pkg`) is no
35
+ longer reported as a type change.
36
+ - Duplicate field or constant names are parse errors, as in rosidl.
37
+ - Unknown configuration keys are configuration errors, with a suggestion.
38
+ - `lint --format json` no longer includes semver advisories.
39
+ - Text output no longer wraps long lines when not writing to a terminal.
40
+ - Files are read and written as UTF-8 regardless of locale.
41
+ - GitHub annotation text is escaped.
42
+ - `wchar` in MCAP IDL schemas is rejected instead of narrowed to `char`.
43
+ - The README's GitHub Action example references an existing ref (`@main`).
44
+
45
+ ## [0.1.3] - 2026-11-03
46
+
47
+ ### Added
48
+
49
+ - `--version` / `-v` to display the installed package version.
50
+
51
+ ### Fixed
52
+
53
+ - Pass GitHub Action inputs as literal arguments to prevent shell injection.
54
+ - Report tightened bounds even when another bound on the same field increases.
55
+ - Include deleted packages when checking glob targets against Git or directory
56
+ baselines, including globs with parent traversal; reject path globs for lockfile
57
+ and MCAP baselines.
58
+ - Reject unsupported, conflicting or unusable MCAP schemas instead of silently
59
+ checking incomplete definitions, while preserving supported IDL string constants.
60
+ - Accept equivalent NaN constants and defaults when comparing repeated MCAP schemas.
61
+ - Replace each installed package's full interface set with its source target in
62
+ `bag-check`, so deleted source interfaces are reported as removed.
63
+ - Honour workspace and package deep-check settings, with `--deep` / `--no-deep`
64
+ taking precedence.
65
+ - Inherit enclosing workspace configuration when targeting a package directly,
66
+ and apply package lint overrides from `roschema.toml` or `pyproject.toml`.
67
+ - Preserve root package settings in `bag-check` and after deleting its last interface.
68
+ - Reject unknown or misplaced rule identifiers and invalid configuration values.
69
+ - Detect unbounded string elements inside arrays and sequences with RSL006.
70
+
71
+ ## [0.1.2] - 2026-11-03
72
+
73
+ ### Fixed
74
+
75
+ - Respect `COLCON_IGNORE` and `AMENT_IGNORE` during package discovery to avoid
76
+ reading generated build and install trees, including dangling package symlinks.
77
+
78
+ ## [0.1.1] - 2026-07-25
79
+
80
+ Packaging and documentation release; no behavioural changes to the CLI.
81
+
82
+ ### Added
83
+
84
+ - `py.typed` marker (PEP 561) so downstream type-checkers use roschema's types.
85
+ - `Typing :: Typed` and Python 3.10-3.13 classifiers; `Repository` and `Issues`
86
+ project URLs.
87
+ - README: installation section, common-usage recipes, a demo screenshot, and a
88
+ link to the [roschema-demo](https://github.com/justagist/roschema-demo) repo.
89
+
90
+ ### Changed
91
+
92
+ - Dropped the redundant Apache license classifier in favour of the SPDX
93
+ `license` expression.
94
+
95
+ ### Fixed
96
+
97
+ - Removed a broken demo image reference from the README.
98
+
99
+ ## [0.1.0] - 2026-07-24
100
+
101
+ First release.
102
+
103
+ ### Added
104
+
105
+ - `check`: breaking-change detection against a git ref, directory, MCAP bag or
106
+ lockfile baseline. Rules RS001-RS110 across the wire / source / introspection
107
+ / behaviour compatibility levels, with cumulative `--level` presets and
108
+ per-package semver bump advice.
109
+ - `lint`: rosidl-aligned naming and style rules (RSL001-RSL010), with `--rules`
110
+ overrides.
111
+ - `fmt`: canonical, idempotent formatting that never changes what `check`
112
+ reports; inline `# roschema: ignore=` suppressions survive formatting.
113
+ - `bag-check`: check a recorded MCAP bag against the current environment
114
+ (ament index), with optional source targets overlaid.
115
+ - `freeze`: lockfile baselines for non-git distribution; `--deep` reports
116
+ transitive breakage (RS110).
117
+ - `ls`: discovery diagnostics.
118
+ - Configuration via `roschema.toml` / `pyproject.toml`, inline suppression and
119
+ reasoned allowlists; text / JSON / GitHub output formats; a composite GitHub
120
+ Action.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: roschema
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Breaking-change detection, linting and formatting for ROS 2 interface definitions
5
5
  Project-URL: Homepage, https://github.com/justagist/roschema
6
6
  Project-URL: Documentation, https://github.com/justagist/roschema#readme
@@ -124,7 +124,9 @@ roschema check [TARGET] --against REF [--level LEVEL] [--format text|json|github
124
124
  `TARGET` is a path, a package name, or a glob (`'src/**'`) that expands to every
125
125
  package underneath; omit it for the current workspace. Discovery skips trees
126
126
  marked with `COLCON_IGNORE` or `AMENT_IGNORE`, including standard colcon build
127
- and install directories.
127
+ and install directories. Glob targets include matching packages deleted from a Git
128
+ or directory baseline. Lockfile and MCAP baselines have synthetic paths; use a
129
+ workspace target with those baselines instead of a path glob.
128
130
 
129
131
  `--against` accepts a git ref (validated with `git rev-parse`), a directory, an
130
132
  MCAP bag (`.mcap`) or a lockfile (`roschema.lock`); the type is auto-detected. A
@@ -171,7 +173,7 @@ roschema fmt [TARGET] [--check]
171
173
 
172
174
  `lint` checks a single tree (no baseline). Naming rules follow rosidl: field
173
175
  names snake_case (RSL001), constants UPPER_SNAKE (RSL002), interface names
174
- PascalCase (RSL003); it also flags deprecated `char`/scalar-`byte` (RSL005) and
176
+ PascalCase (RSL003); it also flags ambiguous `char`/scalar-`byte` (RSL005) and
175
177
  request/response field shadowing in services (RSL010). RSL004 (missing doc) and
176
178
  RSL006 (unbounded) are off by default; enable via `[lint.rules]` or per run with
177
179
  `--rules` (e.g. `--rules RSL004=warn --rules RSL001=off`; a bare `--rules RSL004`
@@ -200,10 +202,19 @@ definitions rather than reported as missing.
200
202
 
201
203
  Optional `TARGETS` (paths or package names) are overlaid on top of the
202
204
  environment, so a package you are editing can be checked from source instead of
203
- its installed copy. A bag type found in neither the environment nor any target
205
+ its installed copy. Each source package replaces its entire installed interface
206
+ set, so deleted source interfaces are reported as removed even when an installed
207
+ copy remains. If targets overlap, the last target wins for that package. A bag
208
+ type found in neither the environment nor any target
204
209
  is reported as removed (RS040). `ros2msg` schemas (including concatenated
205
210
  dependent definitions) are fully supported; `ros2idl` is a best-effort
206
- conversion of the msg-equivalent subset.
211
+ conversion of the msg-equivalent subset. Unsupported encodings or IDL constructs,
212
+ conflicting definitions of the same type (including embedded dependencies), and
213
+ bags without usable ROS schemas produce a tool error (exit `2`). Equivalent
214
+ definitions that differ only in formatting or comments are accepted, including
215
+ repeated NaN constants and defaults. IDL struct inheritance and constants outside
216
+ structs (including generated `T_Constants` modules) are rejected because their
217
+ conversion is incomplete.
207
218
 
208
219
  ## Compatibility levels
209
220
 
@@ -221,11 +232,17 @@ conversion of the msg-equivalent subset.
221
232
 
222
233
  `roschema.toml` at the workspace root, a per-package `roschema.toml`, or
223
234
  `[tool.roschema]` in `pyproject.toml`. Precedence: CLI > package > workspace >
224
- defaults.
235
+ defaults. Workspace discovery searches upwards to the Git repository root, or
236
+ the filesystem root outside Git. Package configurations override inherited
237
+ workspace values even when checking the package directly by path. At each
238
+ directory, `roschema.toml` takes precedence over `[tool.roschema]`. Unknown rule
239
+ IDs, rule IDs in the wrong section, and invalid settings produce configuration
240
+ errors (exit `2`).
225
241
 
226
242
  ```toml
227
243
  [check]
228
244
  level = "source"
245
+ deep = false # --deep / --no-deep overrides this
229
246
 
230
247
  [check.rules]
231
248
  RS008 = "off" # off | warn | error
@@ -290,9 +307,10 @@ jobs:
290
307
  - uses: actions/checkout@v4
291
308
  with:
292
309
  fetch-depth: 0 # needed for the git-ref baseline
293
- - uses: justagist/roschema/action@v1
310
+ - uses: justagist/roschema/action@main
294
311
  with:
295
312
  against: origin/${{ github.base_ref }}
313
+ # version: "roschema==0.2.0" # pin a release for reproducible results
296
314
  # level: source
297
315
  # deep: "true"
298
316
  # exclude: "vendor_msgs* legacy_msgs/msg/Old"
@@ -302,6 +320,10 @@ jobs:
302
320
  `roschema.lock`). A lockfile or directory baseline needs no `fetch-depth: 0`
303
321
  because it is a plain file in the checkout.
304
322
 
323
+ By default the Action installs the latest roschema release, so new rules can
324
+ change results without any change on your side. Uncomment `version` to pin a
325
+ release instead.
326
+
305
327
  ## Rule reference
306
328
 
307
329
  Every rule, its levels, default severity and rationale is documented in
@@ -90,7 +90,9 @@ roschema check [TARGET] --against REF [--level LEVEL] [--format text|json|github
90
90
  `TARGET` is a path, a package name, or a glob (`'src/**'`) that expands to every
91
91
  package underneath; omit it for the current workspace. Discovery skips trees
92
92
  marked with `COLCON_IGNORE` or `AMENT_IGNORE`, including standard colcon build
93
- and install directories.
93
+ and install directories. Glob targets include matching packages deleted from a Git
94
+ or directory baseline. Lockfile and MCAP baselines have synthetic paths; use a
95
+ workspace target with those baselines instead of a path glob.
94
96
 
95
97
  `--against` accepts a git ref (validated with `git rev-parse`), a directory, an
96
98
  MCAP bag (`.mcap`) or a lockfile (`roschema.lock`); the type is auto-detected. A
@@ -137,7 +139,7 @@ roschema fmt [TARGET] [--check]
137
139
 
138
140
  `lint` checks a single tree (no baseline). Naming rules follow rosidl: field
139
141
  names snake_case (RSL001), constants UPPER_SNAKE (RSL002), interface names
140
- PascalCase (RSL003); it also flags deprecated `char`/scalar-`byte` (RSL005) and
142
+ PascalCase (RSL003); it also flags ambiguous `char`/scalar-`byte` (RSL005) and
141
143
  request/response field shadowing in services (RSL010). RSL004 (missing doc) and
142
144
  RSL006 (unbounded) are off by default; enable via `[lint.rules]` or per run with
143
145
  `--rules` (e.g. `--rules RSL004=warn --rules RSL001=off`; a bare `--rules RSL004`
@@ -166,10 +168,19 @@ definitions rather than reported as missing.
166
168
 
167
169
  Optional `TARGETS` (paths or package names) are overlaid on top of the
168
170
  environment, so a package you are editing can be checked from source instead of
169
- its installed copy. A bag type found in neither the environment nor any target
171
+ its installed copy. Each source package replaces its entire installed interface
172
+ set, so deleted source interfaces are reported as removed even when an installed
173
+ copy remains. If targets overlap, the last target wins for that package. A bag
174
+ type found in neither the environment nor any target
170
175
  is reported as removed (RS040). `ros2msg` schemas (including concatenated
171
176
  dependent definitions) are fully supported; `ros2idl` is a best-effort
172
- conversion of the msg-equivalent subset.
177
+ conversion of the msg-equivalent subset. Unsupported encodings or IDL constructs,
178
+ conflicting definitions of the same type (including embedded dependencies), and
179
+ bags without usable ROS schemas produce a tool error (exit `2`). Equivalent
180
+ definitions that differ only in formatting or comments are accepted, including
181
+ repeated NaN constants and defaults. IDL struct inheritance and constants outside
182
+ structs (including generated `T_Constants` modules) are rejected because their
183
+ conversion is incomplete.
173
184
 
174
185
  ## Compatibility levels
175
186
 
@@ -187,11 +198,17 @@ conversion of the msg-equivalent subset.
187
198
 
188
199
  `roschema.toml` at the workspace root, a per-package `roschema.toml`, or
189
200
  `[tool.roschema]` in `pyproject.toml`. Precedence: CLI > package > workspace >
190
- defaults.
201
+ defaults. Workspace discovery searches upwards to the Git repository root, or
202
+ the filesystem root outside Git. Package configurations override inherited
203
+ workspace values even when checking the package directly by path. At each
204
+ directory, `roschema.toml` takes precedence over `[tool.roschema]`. Unknown rule
205
+ IDs, rule IDs in the wrong section, and invalid settings produce configuration
206
+ errors (exit `2`).
191
207
 
192
208
  ```toml
193
209
  [check]
194
210
  level = "source"
211
+ deep = false # --deep / --no-deep overrides this
195
212
 
196
213
  [check.rules]
197
214
  RS008 = "off" # off | warn | error
@@ -256,9 +273,10 @@ jobs:
256
273
  - uses: actions/checkout@v4
257
274
  with:
258
275
  fetch-depth: 0 # needed for the git-ref baseline
259
- - uses: justagist/roschema/action@v1
276
+ - uses: justagist/roschema/action@main
260
277
  with:
261
278
  against: origin/${{ github.base_ref }}
279
+ # version: "roschema==0.2.0" # pin a release for reproducible results
262
280
  # level: source
263
281
  # deep: "true"
264
282
  # exclude: "vendor_msgs* legacy_msgs/msg/Old"
@@ -268,6 +286,10 @@ jobs:
268
286
  `roschema.lock`). A lockfile or directory baseline needs no `fetch-depth: 0`
269
287
  because it is a plain file in the checkout.
270
288
 
289
+ By default the Action installs the latest roschema release, so new rules can
290
+ change results without any change on your side. Uncomment `version` to pin a
291
+ release instead.
292
+
271
293
  ## Rule reference
272
294
 
273
295
  Every rule, its levels, default severity and rationale is documented in
@@ -0,0 +1,65 @@
1
+ name: roschema
2
+ description: Catch breaking ROS 2 interface changes in the pull request.
3
+ branding:
4
+ icon: git-pull-request
5
+ color: purple
6
+
7
+ inputs:
8
+ target:
9
+ description: Path or package to check.
10
+ default: "."
11
+ against:
12
+ description: Baseline -- a git ref, directory, .mcap bag or roschema.lock.
13
+ default: origin/main
14
+ level:
15
+ description: >-
16
+ Reporting level (wire, source, introspection, behavior). Empty uses the
17
+ level configured in roschema.toml, else source.
18
+ default: ""
19
+ format:
20
+ description: Output format (text, json, github).
21
+ default: github
22
+ deep:
23
+ description: Resolve nested types and report transitive breakage (RS110).
24
+ default: "false"
25
+ exclude:
26
+ description: >-
27
+ Whitespace-separated globs of packages/interfaces to skip. Split on
28
+ whitespace only so patterns reach roschema literally.
29
+ default: ""
30
+ version:
31
+ description: >-
32
+ roschema requirement for uvx. Unpinned runs the latest release (refreshed
33
+ on every run, even with a uv cache); pin it (e.g. "roschema==0.2.0") for
34
+ reproducible results.
35
+ default: roschema
36
+
37
+ runs:
38
+ using: composite
39
+ steps:
40
+ - uses: astral-sh/setup-uv@v5
41
+ - shell: bash
42
+ env:
43
+ ROSCHEMA_TARGET: ${{ inputs.target }}
44
+ ROSCHEMA_AGAINST: ${{ inputs.against }}
45
+ ROSCHEMA_LEVEL: ${{ inputs.level }}
46
+ ROSCHEMA_FORMAT: ${{ inputs.format }}
47
+ ROSCHEMA_DEEP: ${{ inputs.deep }}
48
+ ROSCHEMA_EXCLUDE: ${{ inputs.exclude }}
49
+ ROSCHEMA_VERSION: ${{ inputs.version }}
50
+ run: |
51
+ set -euo pipefail
52
+ args=(check "$ROSCHEMA_TARGET"
53
+ --against "$ROSCHEMA_AGAINST"
54
+ --format "$ROSCHEMA_FORMAT")
55
+ if [ -n "$ROSCHEMA_LEVEL" ]; then
56
+ args+=(--level "$ROSCHEMA_LEVEL")
57
+ fi
58
+ if [ "$ROSCHEMA_DEEP" = "true" ]; then
59
+ args+=(--deep)
60
+ fi
61
+ read -r -a excludes <<< "${ROSCHEMA_EXCLUDE//$'\n'/ }"
62
+ for glob in "${excludes[@]}"; do
63
+ args+=(--exclude "$glob")
64
+ done
65
+ uvx --refresh-package roschema --from "$ROSCHEMA_VERSION" roschema "${args[@]}"
@@ -26,7 +26,7 @@ int32 a
26
26
 
27
27
  A field was added to an interface section.
28
28
 
29
- - Levels: wire, source
29
+ - Levels: wire
30
30
  - Default severity: error
31
31
 
32
32
  *Rationale:* Appending a field is source-compatible but still changes the CDR wire layout, so a new publisher and an old subscriber cannot interoperate.
@@ -91,12 +91,12 @@ float32 yaw
91
91
 
92
92
  ### RS006 -- Array kind changed
93
93
 
94
- A field changed between fixed, bounded and unbounded array forms.
94
+ A field changed between scalar, fixed-size array and sequence forms.
95
95
 
96
96
  - Levels: wire, source
97
97
  - Default severity: error
98
98
 
99
- *Rationale:* Fixed, bounded and unbounded arrays use different CDR encodings and generate different container types.
99
+ *Rationale:* A sequence carries a length prefix that a fixed-size array and a scalar do not, so the CDR layout and the generated container type both change. Adding or removing a sequence bound is reported as RS007 or RS008 instead.
100
100
 
101
101
  ```
102
102
  # before
@@ -107,7 +107,7 @@ int32[8] data
107
107
 
108
108
  ### RS007 -- Bound tightened
109
109
 
110
- A bounded array or string had its capacity reduced.
110
+ A sequence or string gained a bound or had its bound reduced.
111
111
 
112
112
  - Levels: wire
113
113
  - Default severity: error
@@ -123,7 +123,7 @@ string<=16 name
123
123
 
124
124
  ### RS008 -- Bound loosened
125
125
 
126
- A bounded array or string had its capacity increased.
126
+ A sequence or string lost its bound or had its bound increased.
127
127
 
128
128
  - Levels: wire
129
129
  - Default severity: warn
@@ -239,7 +239,7 @@ int32 b
239
239
 
240
240
  ### RS110 -- Transitively broken by an embedded type
241
241
 
242
- An interface embeds (directly or through nested types) another interface whose wire layout changed, so it is broken even though its own definition did not change. Reported only with --deep.
242
+ An interface embeds (directly or through nested types) another interface whose wire layout changed, so it is broken even though its own definition did not change. Reported only with --deep, with the severity of the reported change it traces back to; suppressed changes are not followed.
243
243
 
244
244
  - Levels: wire
245
245
  - Default severity: error
@@ -290,15 +290,15 @@ A field has no trailing documentation comment.
290
290
 
291
291
  ### RSL005 -- Discouraged type
292
292
 
293
- Uses `char`, or a scalar `byte`, both deprecated aliases in ROS 2.
293
+ Uses `char`, or a scalar `byte`, whose generated types are ambiguous.
294
294
 
295
295
  - Default severity: warn
296
296
 
297
- *Rationale:* ROS 2 deprecated `char` and `byte` as scalar types in favour of `uint8`/`int8`; `byte[]` remains idiomatic for opaque binary data.
297
+ *Rationale:* rosidl generates `char` as `uint8` although the ROS 2 type table lists it as a DDS `char`, and a scalar `byte` becomes a Python `bytes` object rather than an integer; `uint8` states the intent. `byte[]` remains idiomatic for opaque binary data.
298
298
 
299
299
  ### RSL006 -- Unbounded string or array
300
300
 
301
- A field is an unbounded array or unbounded string.
301
+ A field is an unbounded array or contains unbounded strings.
302
302
 
303
303
  - Default severity: off
304
304
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "roschema"
7
- dynamic = ["version"]
7
+ version = "0.2.0"
8
8
  description = "Breaking-change detection, linting and formatting for ROS 2 interface definitions"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -46,9 +46,6 @@ Documentation = "https://github.com/justagist/roschema#readme"
46
46
  Repository = "https://github.com/justagist/roschema"
47
47
  Issues = "https://github.com/justagist/roschema/issues"
48
48
 
49
- [tool.hatch.version]
50
- path = "src/roschema/__init__.py"
51
-
52
49
  [tool.hatch.build.targets.wheel]
53
50
  packages = ["src/roschema"]
54
51
 
@@ -67,4 +64,6 @@ files = ["src"]
67
64
 
68
65
  [tool.pytest.ini_options]
69
66
  testpaths = ["tests"]
70
- addopts = "-q"
67
+ # A sourced ROS environment auto-loads its launch testing plugins, which fail to
68
+ # import in a virtualenv without ROS's Python dependencies (e.g. PyYAML).
69
+ addopts = "-q -p no:launch_testing -p no:launch_ros -p no:launch_pytest"
@@ -0,0 +1,8 @@
1
+ """roschema: breaking-change detection, linting and formatting for ROS 2 interfaces."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("roschema")
7
+ except PackageNotFoundError: # Uninstalled source checkout.
8
+ __version__ = "0.0.0.dev"