wireforge 0.1.0__py3-none-any.whl

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 (96) hide show
  1. wireforge/__init__.py +3 -0
  2. wireforge/_data/cmake/WireForgeConfig.cmake +29 -0
  3. wireforge/_data/cmake/WireForgeConfigVersion.cmake +15 -0
  4. wireforge/_data/cmake/WireForgeGenerate.cmake +64 -0
  5. wireforge/_data/runtime/LICENSE +21 -0
  6. wireforge/_data/runtime/include/wireforge/array_access.hpp +35 -0
  7. wireforge/_data/runtime/include/wireforge/bit_utils.hpp +226 -0
  8. wireforge/_data/runtime/include/wireforge/byte_io.hpp +387 -0
  9. wireforge/_data/runtime/include/wireforge/checksum.hpp +201 -0
  10. wireforge/_data/runtime/include/wireforge/codec_support.hpp +412 -0
  11. wireforge/_data/runtime/include/wireforge/config.hpp +41 -0
  12. wireforge/_data/runtime/include/wireforge/delegate.hpp +101 -0
  13. wireforge/_data/runtime/include/wireforge/dispatcher.hpp +339 -0
  14. wireforge/_data/runtime/include/wireforge/expr_support.hpp +38 -0
  15. wireforge/_data/runtime/include/wireforge/frame_decoder.hpp +262 -0
  16. wireforge/_data/runtime/include/wireforge/frame_layout.hpp +20 -0
  17. wireforge/_data/runtime/include/wireforge/link.hpp +223 -0
  18. wireforge/_data/runtime/include/wireforge/lock.hpp +146 -0
  19. wireforge/_data/runtime/include/wireforge/ring_buffer.hpp +226 -0
  20. wireforge/_data/runtime/include/wireforge/span.hpp +116 -0
  21. wireforge/_data/runtime/include/wireforge/static_vector.hpp +264 -0
  22. wireforge/_data/runtime/include/wireforge/status.hpp +75 -0
  23. wireforge/_data/runtime/include/wireforge/transport.hpp +95 -0
  24. wireforge/_data/runtime/include/wireforge/validation.hpp +153 -0
  25. wireforge/_data/runtime/include/wireforge/version.hpp +22 -0
  26. wireforge/_data/runtime/requirements.yaml +438 -0
  27. wireforge/_data/schema/deviations.schema.json +47 -0
  28. wireforge/_data/schema/fragment.schema.json +79 -0
  29. wireforge/_data/schema/llr_ids.schema.json +25 -0
  30. wireforge/_data/schema/project.schema.json +1240 -0
  31. wireforge/_data/schema/runtime_requirements.schema.json +34 -0
  32. wireforge/_data/ui/assets/index-6Pomu6U4.js +44 -0
  33. wireforge/_data/ui/assets/index-GVdDyUAG.css +1 -0
  34. wireforge/_data/ui/index.html +13 -0
  35. wireforge/bundle.py +177 -0
  36. wireforge/ci_check.py +155 -0
  37. wireforge/cli.py +617 -0
  38. wireforge/codec.py +897 -0
  39. wireforge/compat.py +536 -0
  40. wireforge/coverage_points.py +64 -0
  41. wireforge/cpp.py +273 -0
  42. wireforge/cpp_codec.py +440 -0
  43. wireforge/cpp_interface.py +874 -0
  44. wireforge/cpp_tests.py +1993 -0
  45. wireforge/differential.py +586 -0
  46. wireforge/docs_markdown.py +72 -0
  47. wireforge/expr.py +598 -0
  48. wireforge/findings.py +187 -0
  49. wireforge/fmt.py +351 -0
  50. wireforge/generate.py +644 -0
  51. wireforge/golden.py +253 -0
  52. wireforge/hlr_import.py +112 -0
  53. wireforge/impact.py +249 -0
  54. wireforge/importers.py +402 -0
  55. wireforge/issues.py +27 -0
  56. wireforge/layout.py +681 -0
  57. wireforge/lifecycle.py +1266 -0
  58. wireforge/lifecycle_results.py +205 -0
  59. wireforge/lifecycle_rules.py +78 -0
  60. wireforge/lifecycle_runtime.py +375 -0
  61. wireforge/llr.py +693 -0
  62. wireforge/llr_ids.py +104 -0
  63. wireforge/loader.py +422 -0
  64. wireforge/mcp_server.py +357 -0
  65. wireforge/model.py +322 -0
  66. wireforge/plain.py +15 -0
  67. wireforge/py.typed +0 -0
  68. wireforge/reqif.py +289 -0
  69. wireforge/resources.py +43 -0
  70. wireforge/results.py +377 -0
  71. wireforge/runtime_reqs.py +147 -0
  72. wireforge/schema.py +151 -0
  73. wireforge/server/__init__.py +1 -0
  74. wireforge/server/app.py +619 -0
  75. wireforge/server/oidc.py +99 -0
  76. wireforge/server/storage.py +146 -0
  77. wireforge/service.py +1354 -0
  78. wireforge/templates/command/project.yaml +38 -0
  79. wireforge/templates/raw/project.yaml +28 -0
  80. wireforge/templates/telemetry/project.yaml +51 -0
  81. wireforge/test_cases.py +475 -0
  82. wireforge/test_project.py +478 -0
  83. wireforge/trace.py +442 -0
  84. wireforge/validate.py +1241 -0
  85. wireforge/vcs.py +44 -0
  86. wireforge/vectors.py +222 -0
  87. wireforge/versioning.py +137 -0
  88. wireforge/wireshark.py +792 -0
  89. wireforge/wireshark_runtime.lua +725 -0
  90. wireforge-0.1.0.dist-info/METADATA +91 -0
  91. wireforge-0.1.0.dist-info/RECORD +96 -0
  92. wireforge-0.1.0.dist-info/WHEEL +5 -0
  93. wireforge-0.1.0.dist-info/entry_points.txt +2 -0
  94. wireforge-0.1.0.dist-info/licenses/LICENSE +98 -0
  95. wireforge-0.1.0.dist-info/licenses/runtime/LICENSE +21 -0
  96. wireforge-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,357 @@
1
+ """MCP server: WireForge tools for AI agents over a workspace of projects.
2
+
3
+ Every tool is a thin wrapper of `wireforge.service` and has a CLI equivalent (docs/MCP.md). Tools
4
+ that write accept `dry_run` and return the unified diff of the change; every result carries
5
+ `ok`, `issues`, `data` and `text`. Run with `wireforge mcp --workspace <dir>` (stdio transport).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import functools
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from mcp.server.mcpserver import MCPServer
15
+ from mcp.server.mcpserver.exceptions import ToolError
16
+
17
+ from wireforge import golden, hlr_import, schema, service, test_project
18
+ from wireforge.codec import ReferenceCodec
19
+ from wireforge.loader import SECTIONS, load_project
20
+ from wireforge.service import CodecTarget, EditOptions, Result, ServiceError
21
+
22
+ if TYPE_CHECKING:
23
+ from collections.abc import Callable
24
+
25
+ INSTRUCTIONS = """\
26
+ WireForge defines binary messages in YAML and generates C++17 code and tests from them.
27
+ Typical flow: create_project, then upsert_entry (checksums, enums, interfaces, profiles) and
28
+ upsert_struct (types, messages), validate_project until it reports no issues, generate_code,
29
+ run_tests, lifecycle_docs (requirements, design, test and traceability documents). Use
30
+ dry_run=true to preview a change as a diff. Field definitions follow
31
+ schema/project.schema.json: {name, type, bits?, min?, max?, default?, occurs?: {max, count?,
32
+ fill?}, checksum?: {use, covers: {scope}}, if?, description?}.
33
+ """
34
+
35
+
36
+ class Tools:
37
+ """The tool implementations, bound to a workspace directory."""
38
+
39
+ def __init__(self, workspace: Path) -> None:
40
+ """Serve the projects below `workspace` (one directory per project)."""
41
+ self.workspace = workspace
42
+ self.output = workspace / ".wireforge" / "generated"
43
+
44
+ def project_dir(self, project: str) -> Path:
45
+ """Directory of a project of the workspace."""
46
+ directory = (self.workspace / project).resolve()
47
+ if self.workspace.resolve() not in directory.parents:
48
+ message = f"{project!r} is not a project of the workspace"
49
+ raise ServiceError(message)
50
+ if not (directory / schema.ROOT_FILE_NAME).exists():
51
+ message = f"no project {project!r} in the workspace"
52
+ raise ServiceError(message)
53
+ return directory
54
+
55
+ def list_projects(self) -> dict[str, Any]:
56
+ """Return the projects of the workspace."""
57
+ names = sorted(
58
+ path.name
59
+ for path in self.workspace.iterdir()
60
+ if (path / schema.ROOT_FILE_NAME).exists()
61
+ )
62
+ return Result(ok=True, data={"projects": names}, text="\n".join(names)).to_dict()
63
+
64
+ def create_project(
65
+ self,
66
+ project: str,
67
+ name: str,
68
+ namespace: str | None = None,
69
+ version: str = "0.1.0",
70
+ template: str | None = None,
71
+ ) -> dict[str, Any]:
72
+ """Create a project directory with its root file, empty or from a template.
73
+
74
+ `template` is one of `telemetry`, `command` or `raw` (a working first project to edit).
75
+ """
76
+ directory = self.workspace / project
77
+ if template is not None:
78
+ return service.init_project(directory, template, name, namespace, version).to_dict()
79
+ if directory.exists():
80
+ message = f"{project} already exists"
81
+ raise ServiceError(message)
82
+ directory.mkdir(parents=True)
83
+ lines = ["schema_version: 1", f"project: {name}", f'version: "{version}"']
84
+ if namespace:
85
+ lines.append(f"namespace: {namespace}")
86
+ (directory / schema.ROOT_FILE_NAME).write_text("\n".join(lines) + "\n", encoding="utf-8")
87
+ return service.validate(directory).to_dict()
88
+
89
+ def list_messages(self, project: str) -> dict[str, Any]:
90
+ """Return the messages (with ids), types and interfaces of a project."""
91
+ loaded = load_project(self.project_dir(project))
92
+ model = loaded.project
93
+ if model is None:
94
+ return Result(ok=False, issues=loaded.issues).to_dict()
95
+ messages = [
96
+ {"name": item.name, "id": item.message.id if item.message else None}
97
+ for item in model.messages.values()
98
+ ]
99
+ data = {
100
+ "messages": messages,
101
+ "types": list(model.types),
102
+ "user_types": list(model.user_types),
103
+ "interfaces": list(model.interfaces),
104
+ }
105
+ text = "\n".join(f"{item['name']} id={item['id']}" for item in messages)
106
+ return Result(ok=True, data=data, text=text).to_dict()
107
+
108
+ def get_message(self, project: str, name: str) -> dict[str, Any]:
109
+ """Return the definition of a message (or of a type) as written in the files."""
110
+ directory = self.project_dir(project)
111
+ for section in ("messages", "types"):
112
+ try:
113
+ result = service.get(directory, f"/{section}/{name}")
114
+ except ServiceError:
115
+ continue
116
+ result.data["section"] = section
117
+ return result.to_dict()
118
+ message = f"no message or type {name!r}"
119
+ raise ServiceError(message)
120
+
121
+ def upsert_struct(
122
+ self,
123
+ project: str,
124
+ section: str,
125
+ name: str,
126
+ definition: dict[str, Any],
127
+ *,
128
+ dry_run: bool = False,
129
+ ) -> dict[str, Any]:
130
+ """Create or update a type or message ({description?, id?, fields: [...]})."""
131
+ if section not in {"types", "messages"}:
132
+ message = "section must be 'types' or 'messages'"
133
+ raise ServiceError(message)
134
+ options = EditOptions(dry_run=dry_run)
135
+ return service.upsert(
136
+ self.project_dir(project), section, name, definition, options
137
+ ).to_dict()
138
+
139
+ def upsert_entry(
140
+ self,
141
+ project: str,
142
+ section: str,
143
+ name: str,
144
+ definition: object,
145
+ *,
146
+ dry_run: bool = False,
147
+ ) -> dict[str, Any]:
148
+ """Create or update an entry of any section (checksums, enums, interfaces, ...)."""
149
+ if section not in SECTIONS:
150
+ message = f"section must be one of {', '.join(SECTIONS)}"
151
+ raise ServiceError(message)
152
+ options = EditOptions(dry_run=dry_run)
153
+ pointer = f"/{section}/{name}"
154
+ return service.set_value(self.project_dir(project), pointer, definition, options).to_dict()
155
+
156
+ def validate_project(self, project: str) -> dict[str, Any]:
157
+ """Validate a project; issues carry file, line and JSON pointer."""
158
+ return service.validate(self.project_dir(project)).to_dict()
159
+
160
+ def generate_code(self, project: str, *, tests: bool = True) -> dict[str, Any]:
161
+ """Generate the C++ code (and the GoogleTest project) below the workspace output."""
162
+ directory = self.project_dir(project)
163
+ options = test_project.TestOptions(plugins=service.find_plugins(directory))
164
+ result = service.generate_code(directory, self.output, options if tests else None)
165
+ name = str(result.data.get("project", ""))
166
+ target = self.output / name
167
+ files = sorted(
168
+ path.relative_to(target).as_posix() for path in target.rglob("*") if path.is_file()
169
+ )
170
+ result.data["files"] = files if result.ok else []
171
+ return result.to_dict()
172
+
173
+ def run_tests(
174
+ self, project: str, *, coverage: bool = False, coverage_runtime: bool = False
175
+ ) -> dict[str, Any]:
176
+ """Generate, build and run the generated tests (with `coverage` the coverage target).
177
+
178
+ `coverage_runtime` adds the runtime headers to the coverage report. The results are written
179
+ below the returned `build_dir`, which `lifecycle_docs` accepts as `results_dir`.
180
+ """
181
+ directory = self.project_dir(project)
182
+ options = test_project.TestOptions(plugins=service.find_plugins(directory))
183
+ build = service.BuildOptions(
184
+ coverage=coverage or coverage_runtime, coverage_runtime=coverage_runtime
185
+ )
186
+ result = service.run_tests(directory, self.output, options, build)
187
+ return result.to_dict()
188
+
189
+ def wireshark_dissector(self, project: str) -> dict[str, Any]:
190
+ """Write the Wireshark Lua dissector plugin of a project below the workspace output."""
191
+ target = self.output / "wireshark" / f"{project}.lua"
192
+ return service.wireshark_dissector(self.project_dir(project), target).to_dict()
193
+
194
+ def lifecycle_docs(
195
+ self,
196
+ project: str,
197
+ *,
198
+ strict: bool = False,
199
+ results_dir: str | None = None,
200
+ rule_findings: list[str] | None = None,
201
+ ) -> dict[str, Any]:
202
+ """Write the requirements, design, test and traceability documents of a project.
203
+
204
+ `results_dir` is the `build_dir` that `run_tests` returned; it must lie below the workspace
205
+ output, so a tool call cannot read other places. `rule_findings` lists SARIF or cppcheck
206
+ XML reports of your coding-rule checker; they must lie inside the workspace.
207
+ """
208
+ reports = []
209
+ for name in rule_findings or []:
210
+ report = Path(name).resolve()
211
+ if self.workspace.resolve() not in report.parents:
212
+ message = "rule_findings must be files inside the workspace"
213
+ raise ServiceError(message)
214
+ reports.append(report)
215
+ target = self.output.parent / "docs" / project
216
+ results = None
217
+ if results_dir is not None:
218
+ results = Path(results_dir).resolve()
219
+ if self.output.resolve() not in results.parents:
220
+ message = "results_dir must be a build directory below the workspace output"
221
+ raise ServiceError(message)
222
+ options = service.DocsOptions(rule_findings=tuple(reports))
223
+ return service.lifecycle_docs(
224
+ self.project_dir(project), target, strict=strict, results=results, options=options
225
+ ).to_dict()
226
+
227
+ def requirements_import(
228
+ self, project: str, path: str, baseline: str, tool: str | None = None
229
+ ) -> dict[str, Any]:
230
+ """Import the high-level requirements of a ReqIF or CSV file inside the workspace."""
231
+ source = Path(path).resolve()
232
+ if self.workspace.resolve() not in source.parents:
233
+ message = "path must be a file inside the workspace"
234
+ raise ServiceError(message)
235
+ options = hlr_import.ImportOptions(baseline, tool)
236
+ directory = self.project_dir(project)
237
+ return service.requirements_import(directory, source, options).to_dict()
238
+
239
+ def requirements_lock(self, project: str) -> dict[str, Any]:
240
+ """Write llr_ids.lock, which numbers the low-level requirements (append-only)."""
241
+ return service.requirements_lock(self.project_dir(project)).to_dict()
242
+
243
+ def analyze_layout(
244
+ self, project: str, profile: str | None = None, export: str = "markdown"
245
+ ) -> dict[str, Any]:
246
+ """Return the layout report: sizes, offsets, bit maps, frame sizes and link RAM."""
247
+ if export not in {"markdown", "csv", "json"}:
248
+ message = "export must be markdown, csv or json"
249
+ raise ServiceError(message)
250
+ return service.analyze(self.project_dir(project), profile, export).to_dict()
251
+
252
+ def compat_check(
253
+ self, project: str, old_project: str | None = None, revision: str | None = None
254
+ ) -> dict[str, Any]:
255
+ """Compare an older version (another project or a git revision) with the project."""
256
+ directory = self.project_dir(project)
257
+ if old_project is not None:
258
+ return service.compatibility(self.project_dir(old_project), directory).to_dict()
259
+ if revision is None or revision.startswith("-"):
260
+ message = "give old_project or a git revision"
261
+ raise ServiceError(message)
262
+ return service.compatibility_with_revision(directory, revision).to_dict()
263
+
264
+ def _target(
265
+ self, directory: Path, message: str | None, interface: str | None, profile: str | None
266
+ ) -> CodecTarget:
267
+ plugins = service.load_plugins(service.find_plugins(directory))
268
+ return CodecTarget(message, interface, profile, plugins)
269
+
270
+ def encode_sample(
271
+ self,
272
+ project: str,
273
+ message: str,
274
+ value: dict[str, Any] | None = None,
275
+ interface: str | None = None,
276
+ profile: str | None = None,
277
+ ) -> dict[str, Any]:
278
+ """Encode a message value; without `value` a sample with every field set is used."""
279
+ directory = self.project_dir(project)
280
+ target = self._target(directory, message, interface, profile)
281
+ if value is None:
282
+ model = load_project(directory).project
283
+ if model is None or message not in model.messages:
284
+ text = f"no message {message!r}"
285
+ raise ServiceError(text)
286
+ codec = ReferenceCodec(model, profile, target.plugins)
287
+ value = golden.Sampler(codec).struct(model.messages[message].fields)
288
+ result = service.encode(directory, value, target)
289
+ result.data["value"] = value
290
+ return result.to_dict()
291
+
292
+ def decode_hex(
293
+ self,
294
+ project: str,
295
+ hex_bytes: str,
296
+ message: str | None = None,
297
+ interface: str | None = None,
298
+ profile: str | None = None,
299
+ ) -> dict[str, Any]:
300
+ """Decode hex bytes: a payload with `message`, otherwise a complete frame of `interface`."""
301
+ directory = self.project_dir(project)
302
+ try:
303
+ data = bytes.fromhex(hex_bytes)
304
+ except ValueError as error:
305
+ text = f"{hex_bytes!r} is not hex"
306
+ raise ServiceError(text) from error
307
+ return service.decode(
308
+ directory, data, self._target(directory, message, interface, profile)
309
+ ).to_dict()
310
+
311
+
312
+ TOOL_NAMES = (
313
+ "list_projects",
314
+ "create_project",
315
+ "list_messages",
316
+ "get_message",
317
+ "upsert_struct",
318
+ "upsert_entry",
319
+ "validate_project",
320
+ "generate_code",
321
+ "run_tests",
322
+ "analyze_layout",
323
+ "wireshark_dissector",
324
+ "lifecycle_docs",
325
+ "requirements_import",
326
+ "requirements_lock",
327
+ "compat_check",
328
+ "encode_sample",
329
+ "decode_hex",
330
+ )
331
+
332
+
333
+ def _guarded(function: Callable[..., dict[str, Any]]) -> Callable[..., dict[str, Any]]:
334
+ """Report service errors (bad names, missing projects, ...) as tool errors."""
335
+
336
+ @functools.wraps(function)
337
+ def call(*args: object, **kwargs: object) -> dict[str, Any]:
338
+ try:
339
+ return function(*args, **kwargs)
340
+ except (ServiceError, OSError, ValueError) as error:
341
+ raise ToolError(str(error)) from error
342
+
343
+ return call
344
+
345
+
346
+ def create_server(workspace: Path) -> MCPServer:
347
+ """Return the MCP server with every tool bound to `workspace`."""
348
+ server = MCPServer("wireforge", instructions=INSTRUCTIONS)
349
+ tools = Tools(workspace)
350
+ for name in TOOL_NAMES:
351
+ server.add_tool(_guarded(getattr(tools, name)), name=name)
352
+ return server
353
+
354
+
355
+ def main(workspace: Path) -> None:
356
+ """Run the server on stdio."""
357
+ create_server(workspace).run("stdio")
wireforge/model.py ADDED
@@ -0,0 +1,322 @@
1
+ """In-memory project model built by `wireforge.loader`.
2
+
3
+ The model keeps the values as written (expressions stay strings) together with their source
4
+ location, so the validator and later stages can report `file:line: pointer` for every element.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+ from wireforge.issues import Issue
13
+
14
+ # Wire width in bits of every builtin type.
15
+ BUILTIN_BITS = {
16
+ "bool": 8,
17
+ "char": 8,
18
+ "u8": 8,
19
+ "u16": 16,
20
+ "u32": 32,
21
+ "u64": 64,
22
+ "i8": 8,
23
+ "i16": 16,
24
+ "i32": 32,
25
+ "i64": 64,
26
+ "f32": 32,
27
+ "f64": 64,
28
+ }
29
+ UNSIGNED_TYPES = frozenset({"u8", "u16", "u32", "u64"})
30
+ SIGNED_TYPES = frozenset({"i8", "i16", "i32", "i64"})
31
+ INTEGER_TYPES = UNSIGNED_TYPES | SIGNED_TYPES
32
+ FLOAT_TYPES = frozenset({"f32", "f64"})
33
+
34
+ # Raw value of a model attribute that accepts a number or an expression string.
35
+ Value = int | float | str
36
+ Scalar = int | float | bool | str
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class Source:
41
+ """Where an element was written."""
42
+
43
+ file: str
44
+ line: int | None
45
+ pointer: str
46
+
47
+ def issue(self, rule: str, message: str, suffix: str = "") -> Issue:
48
+ """Error located at this element (optionally at a sub-pointer)."""
49
+ return Issue(self.file, self.line, self.pointer + suffix, f"[{rule}] {message}")
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class Occurs:
54
+ """How often a field occurs: optional (max 1), fixed or counted array, or payload fill."""
55
+
56
+ max: Value
57
+ min: Value | None = None
58
+ count: Value | None = None
59
+ fill: bool = False
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class ChecksumUse:
64
+ """`checksum: {use, covers}` of a field."""
65
+
66
+ use: str
67
+ scope: str | None = None
68
+ first: str | None = None
69
+ last: str | None = None
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class Field:
74
+ """One field of a struct, message, header or trailer."""
75
+
76
+ name: str
77
+ type: str
78
+ source: Source
79
+ bits: int | None = None
80
+ min: Value | None = None
81
+ max: Value | None = None
82
+ default: Scalar | list[Scalar] | None = None
83
+ const: Scalar | None = None
84
+ reserved: bool = False
85
+ occurs: Occurs | None = None
86
+ checksum: ChecksumUse | None = None
87
+ role: str | None = None
88
+ condition: str | None = None
89
+ description: str | None = None
90
+ units: str | None = None
91
+ reqs: tuple[str, ...] = ()
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class MessageInfo:
96
+ """Attributes only messages carry."""
97
+
98
+ id: int | None = None
99
+ endian: str | None = None
100
+ bit_order: str | None = None
101
+ range_check: dict[str, bool] | None = None
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class Struct:
106
+ """A `types` entry, or a message when `message` is set."""
107
+
108
+ name: str
109
+ fields: tuple[Field, ...]
110
+ source: Source
111
+ condition: str | None = None
112
+ message: MessageInfo | None = None
113
+ description: str | None = None
114
+ reqs: tuple[str, ...] = ()
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class Enum:
119
+ """Enumeration with an integer underlying type."""
120
+
121
+ name: str
122
+ type: str
123
+ values: dict[str, int]
124
+ source: Source
125
+ reqs: tuple[str, ...] = ()
126
+
127
+
128
+ @dataclass(frozen=True)
129
+ class Checksum:
130
+ """Checksum algorithm declaration."""
131
+
132
+ name: str
133
+ algo: str
134
+ source: Source
135
+ width: int | None = None
136
+ poly: int | None = None
137
+ init: int = 0
138
+ refin: bool = False
139
+ refout: bool = False
140
+ xorout: int = 0
141
+ check: int | None = None
142
+ impl: str | None = None
143
+ include: str | None = None
144
+ size_bytes: int | None = None
145
+ reqs: tuple[str, ...] = ()
146
+
147
+
148
+ @dataclass(frozen=True)
149
+ class Framing:
150
+ """Framing of one interface."""
151
+
152
+ kind: str = "none"
153
+ frame_size: Value | None = None
154
+ max_frame_size: Value | None = None
155
+ length_counts: str = "payload"
156
+ resync: bool = False
157
+ reqs: tuple[str, ...] = ()
158
+
159
+
160
+ @dataclass(frozen=True)
161
+ class InterfaceMessage:
162
+ """Entry of an interface `messages` list."""
163
+
164
+ name: str
165
+ source: Source
166
+ id: int | None = None
167
+ condition: str | None = None
168
+ reqs: tuple[str, ...] = ()
169
+
170
+
171
+ @dataclass(frozen=True)
172
+ class Interface:
173
+ """One communication endpoint. `header` is None for `header: none`."""
174
+
175
+ name: str
176
+ version: str
177
+ direction: str
178
+ source: Source
179
+ messages: tuple[InterfaceMessage, ...]
180
+ header: tuple[Field, ...] | None = None
181
+ trailer: tuple[Field, ...] = ()
182
+ framing: Framing = field(default_factory=Framing)
183
+ buffers: dict[str, Value] = field(default_factory=dict)
184
+ endian: str | None = None
185
+ bit_order: str | None = None
186
+ allow_trailing_bytes: bool = False
187
+ description: str | None = None
188
+ reqs: tuple[str, ...] = ()
189
+
190
+
191
+ @dataclass(frozen=True)
192
+ class Requirement:
193
+ """High-level requirement of the user's catalog (`requirements` section)."""
194
+
195
+ id: str
196
+ text: str
197
+ source: Source
198
+ reference: str | None = None
199
+ verification: str = "test"
200
+
201
+
202
+ @dataclass(frozen=True)
203
+ class Vector:
204
+ """A requirement test vector: bytes from the ICD and what they must decode to or encode from.
205
+
206
+ `data` is None when `text` is not valid hex (reported by the validator).
207
+ """
208
+
209
+ name: str
210
+ message: str
211
+ text: str
212
+ data: bytes | None
213
+ source: Source
214
+ interface: str | None = None
215
+ profile: str | None = None
216
+ fields: dict[str, Any] | None = None
217
+ status: str | None = None
218
+ direction: str = "decode"
219
+ reqs: tuple[str, ...] = ()
220
+ description: str | None = None
221
+
222
+
223
+ @dataclass(frozen=True)
224
+ class Profile:
225
+ """Named set of define values."""
226
+
227
+ name: str
228
+ defines: dict[str, int]
229
+ source: Source
230
+
231
+
232
+ @dataclass(frozen=True)
233
+ class UserConstant:
234
+ """Integer constant owned by application code."""
235
+
236
+ name: str
237
+ cpp: str
238
+ source: Source
239
+ value: int | None = None
240
+ profile_values: dict[str, int] = field(default_factory=dict)
241
+
242
+
243
+ @dataclass(frozen=True)
244
+ class UserType:
245
+ """A field type whose wire form is opaque.
246
+
247
+ `size_bytes` raw bytes, backed by the user's own C++ type and its own `encode`/`decode` free
248
+ functions (found by ADL, like a generated struct).
249
+ """
250
+
251
+ name: str
252
+ cpp: str
253
+ size_bytes: int
254
+ source: Source
255
+ include: str | None = None
256
+ style: str = "quote"
257
+ description: str | None = None
258
+ reqs: tuple[str, ...] = ()
259
+
260
+
261
+ @dataclass(frozen=True)
262
+ class UserHeader:
263
+ """Header included by the generated user prelude."""
264
+
265
+ path: str
266
+ source: Source
267
+ style: str = "quote"
268
+ condition: str | None = None
269
+
270
+
271
+ @dataclass(frozen=True)
272
+ class IdMap:
273
+ """Numbers of the low-level requirements (`llr_ids.lock`): key -> displayed id.
274
+
275
+ `retired` keeps the numbers of requirements that no longer exist; a number is never reused.
276
+ """
277
+
278
+ format: str
279
+ next: int
280
+ ids: dict[str, str]
281
+ retired: dict[str, str]
282
+ source: Source
283
+
284
+
285
+ @dataclass(frozen=True)
286
+ class Define:
287
+ """Named integer constant emitted as an overridable macro."""
288
+
289
+ name: str
290
+ value: int
291
+ source: Source
292
+
293
+
294
+ @dataclass
295
+ class Project:
296
+ """A loaded project: root file plus merged fragments."""
297
+
298
+ name: str
299
+ source: Source
300
+ version: str | None = None
301
+ namespace: str | None = None
302
+ defaults: dict[str, object] = field(default_factory=dict)
303
+ requirements: dict[str, Requirement] = field(default_factory=dict)
304
+ defines: dict[str, Define] = field(default_factory=dict)
305
+ profiles: dict[str, Profile] = field(default_factory=dict)
306
+ checksums: dict[str, Checksum] = field(default_factory=dict)
307
+ enums: dict[str, Enum] = field(default_factory=dict)
308
+ types: dict[str, Struct] = field(default_factory=dict)
309
+ user_types: dict[str, UserType] = field(default_factory=dict)
310
+ messages: dict[str, Struct] = field(default_factory=dict)
311
+ interfaces: dict[str, Interface] = field(default_factory=dict)
312
+ vectors: dict[str, Vector] = field(default_factory=dict)
313
+ constants: dict[str, UserConstant] = field(default_factory=dict)
314
+ llr_ids: IdMap | None = None
315
+ requirements_source: dict[str, str] | None = None
316
+ headers: list[UserHeader] = field(default_factory=list)
317
+ include_dirs: list[tuple[str, Source]] = field(default_factory=list)
318
+ files: list[str] = field(default_factory=list)
319
+
320
+ def struct(self, name: str) -> Struct | None:
321
+ """Return the `types` entry with this name (messages are not usable as field types)."""
322
+ return self.types.get(name)
wireforge/plain.py ADDED
@@ -0,0 +1,15 @@
1
+ """Plain Python values from the objects ruamel.yaml loads."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def to_plain(value: object) -> object:
7
+ """Convert ruamel scalar subclasses (HexInt, ScalarFloat, ...) to plain Python values."""
8
+ if isinstance(value, list):
9
+ return [to_plain(item) for item in value]
10
+ if isinstance(value, dict):
11
+ return {str(key): to_plain(item) for key, item in value.items()}
12
+ for kind in (bool, int, float, str):
13
+ if isinstance(value, kind):
14
+ return kind(value)
15
+ return value
wireforge/py.typed ADDED
File without changes