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
wireforge/vcs.py ADDED
@@ -0,0 +1,44 @@
1
+ """What git says about a project directory: commit, repository root and local changes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ GIT_TIMEOUT_SECONDS = 20
10
+
11
+
12
+ def _git(directory: Path, *arguments: str) -> str | None:
13
+ git = shutil.which("git")
14
+ if git is None:
15
+ return None
16
+ try:
17
+ done = subprocess.run(
18
+ [git, *arguments],
19
+ cwd=directory,
20
+ capture_output=True,
21
+ text=True,
22
+ check=False,
23
+ timeout=GIT_TIMEOUT_SECONDS,
24
+ )
25
+ except (OSError, subprocess.SubprocessError):
26
+ return None
27
+ return done.stdout.strip() if done.returncode == 0 else None
28
+
29
+
30
+ def commit(directory: Path) -> str | None:
31
+ """Return the commit the working copy is at (None outside a repository)."""
32
+ return _git(directory, "rev-parse", "HEAD")
33
+
34
+
35
+ def root(directory: Path) -> Path | None:
36
+ """Return the top directory of the repository."""
37
+ top = _git(directory, "rev-parse", "--show-toplevel")
38
+ return Path(top).resolve() if top else None
39
+
40
+
41
+ def dirty(directory: Path) -> bool:
42
+ """Return whether files below `directory` differ from the commit (untracked ones count)."""
43
+ changes = _git(directory, "status", "--porcelain", "--", ".")
44
+ return bool(changes)
wireforge/vectors.py ADDED
@@ -0,0 +1,222 @@
1
+ """Requirement test vectors: the user's expectations checked against the reference codec.
2
+
3
+ A vector is bytes from the ICD with what they must decode to (or what a value must encode to).
4
+ `run()` plays every vector through the reference codec in each profile it applies to and `check()`
5
+ reports the disagreements as SR-22, so a wrong vector or a wrong model is found before code is
6
+ generated. The codec never replaces the user's expectation: the generated C++ test compares with
7
+ what the vector says, not with what the codec answered.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import struct
13
+ from dataclasses import dataclass
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ from wireforge import expr
17
+ from wireforge.codec import CodecError, ReferenceCodec
18
+ from wireforge.expr import ExprError
19
+ from wireforge.issues import Issue, errors_only
20
+ from wireforge.loader import load_project
21
+ from wireforge.validate import Validator
22
+
23
+ if TYPE_CHECKING:
24
+ from pathlib import Path
25
+
26
+ from wireforge.codec import ChecksumPlugin
27
+ from wireforge.model import Field, Project, Struct, Vector
28
+
29
+ OK = "Ok"
30
+ NO_PLUGIN = "no plugin for custom checksum"
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class VectorRun:
35
+ """One vector played through the reference codec in one profile.
36
+
37
+ `status` is the codec's answer in `phase` (`decode` or `encode`): the first direction that
38
+ failed, or the last one that ran when all agree (`Ok`). `decoded` is the decoded value and
39
+ `encoded` the produced bytes.
40
+ """
41
+
42
+ vector: Vector
43
+ profile: str | None
44
+ phase: str
45
+ status: str
46
+ detail: str
47
+ decoded: dict[str, Any] | None
48
+ encoded: bytes | None
49
+
50
+
51
+ def profiles_of(project: Project, vector: Vector) -> list[str | None]:
52
+ """Return the profiles a vector applies to (all of them unless the vector names one)."""
53
+ if vector.profile is not None:
54
+ return [vector.profile]
55
+ return list(project.profiles) or [None]
56
+
57
+
58
+ def f32(value: float) -> float:
59
+ """Round a number to the nearest 32-bit float."""
60
+ return float(struct.unpack(">f", struct.pack(">f", value))[0])
61
+
62
+
63
+ def _holds(codec: ReferenceCodec, message: Struct) -> bool:
64
+ if message.condition is None:
65
+ return True
66
+ try:
67
+ return expr.evaluate(expr.parse_condition(message.condition), codec.variant.scope) != 0
68
+ except ExprError:
69
+ return True
70
+
71
+
72
+ def _decode(codec: ReferenceCodec, vector: Vector, data: bytes) -> tuple[str, str, Any]:
73
+ try:
74
+ if vector.interface is None:
75
+ return OK, "", codec.decode_message(vector.message, data)
76
+ frame = codec.decode_frame(vector.interface, data)
77
+ except CodecError as error:
78
+ return error.status, str(error), None
79
+ if frame.message != vector.message:
80
+ return "UnknownMessageId", f"the frame carries message {frame.message}", None
81
+ return OK, "", frame.value
82
+
83
+
84
+ def _encode(codec: ReferenceCodec, vector: Vector) -> tuple[str, str, bytes | None]:
85
+ value = vector.fields or {}
86
+ try:
87
+ if vector.interface is None:
88
+ return OK, "", codec.encode_message(vector.message, value)
89
+ return OK, "", codec.encode_frame(vector.interface, vector.message, value)
90
+ except CodecError as error:
91
+ return error.status, str(error), None
92
+
93
+
94
+ def run(project: Project, plugins: dict[str, ChecksumPlugin] | None = None) -> list[VectorRun]:
95
+ """Play every well-formed vector through the reference codec, once per profile."""
96
+ runs: list[VectorRun] = []
97
+ for vector in project.vectors.values():
98
+ message = project.messages.get(vector.message)
99
+ known = vector.interface is None or vector.interface in project.interfaces
100
+ if message is None or vector.data is None or not known:
101
+ continue # the validator reports it (SR-22)
102
+ for profile in profiles_of(project, vector):
103
+ if profile is not None and profile not in project.profiles:
104
+ continue
105
+ codec = ReferenceCodec(project, profile, plugins)
106
+ if not _holds(codec, message):
107
+ continue
108
+ runs.append(_play(codec, vector, vector.data, profile))
109
+ return runs
110
+
111
+
112
+ def _play(codec: ReferenceCodec, vector: Vector, data: bytes, profile: str | None) -> VectorRun:
113
+ phase, status, detail, decoded, encoded = "decode", OK, "", None, None
114
+ if vector.direction in {"decode", "both"}:
115
+ status, detail, decoded = _decode(codec, vector, data)
116
+ if vector.direction in {"encode", "both"} and status == OK:
117
+ phase = "encode"
118
+ status, detail, encoded = _encode(codec, vector)
119
+ return VectorRun(vector, profile, phase, status, detail, decoded, encoded)
120
+
121
+
122
+ # --- comparison --------------------------------------------------------------------------------
123
+
124
+
125
+ def _expected_value(project: Project, item: Field, value: object) -> object:
126
+ """Round the floats the way the codec does (f32 fields hold 32-bit values)."""
127
+ inner = project.types.get(item.type)
128
+ if isinstance(value, list):
129
+ return [_expected_value(project, _single(item), element) for element in value]
130
+ if inner is not None and isinstance(value, dict):
131
+ return _expected_fields(project, inner, value)
132
+ if item.type == "f32" and isinstance(value, (int, float)) and not isinstance(value, bool):
133
+ return f32(float(value))
134
+ return value
135
+
136
+
137
+ def _single(item: Field) -> Field:
138
+ """Return the field as one element (arrays are compared element by element)."""
139
+ return type(item)(item.name, item.type, item.source)
140
+
141
+
142
+ def _expected_fields(project: Project, struct_: Struct, values: dict[str, Any]) -> dict[str, Any]:
143
+ by_name = {item.name: item for item in struct_.fields}
144
+ return {
145
+ name: _expected_value(project, by_name[name], value) if name in by_name else value
146
+ for name, value in values.items()
147
+ }
148
+
149
+
150
+ def _differences(expected: object, actual: object, path: str) -> list[str]:
151
+ """List where `actual` differs from `expected` (a mapping is compared by its keys only)."""
152
+ if isinstance(expected, dict):
153
+ if not isinstance(actual, dict):
154
+ return [f"{path or 'value'} is {actual!r}, expected {expected!r}"]
155
+ found: list[str] = []
156
+ for key, value in expected.items():
157
+ if key not in actual:
158
+ found.append(f"{path}{key} is absent, expected {value!r}")
159
+ else:
160
+ found += _differences(value, actual[key], f"{path}{key}.")
161
+ return found
162
+ if isinstance(expected, list):
163
+ if not isinstance(actual, list) or len(actual) != len(expected):
164
+ size = len(actual) if isinstance(actual, list) else actual
165
+ return [f"{path.rstrip('.')} has {size} elements, expected {len(expected)}"]
166
+ found = []
167
+ for index, value in enumerate(expected):
168
+ found += _differences(value, actual[index], f"{path.rstrip('.')}[{index}].")
169
+ return found
170
+ if expected != actual or isinstance(expected, bool) != isinstance(actual, bool):
171
+ return [f"{path.rstrip('.')} is {actual!r}, expected {expected!r}"]
172
+ return []
173
+
174
+
175
+ def _problems(project: Project, item: VectorRun) -> list[str]:
176
+ vector = item.vector
177
+ if vector.status is not None:
178
+ if item.status == vector.status:
179
+ return []
180
+ return [f"decodes with status {item.status}, expected {vector.status}"]
181
+ if item.status != OK:
182
+ word = "decodes" if item.phase == "decode" else "encodes"
183
+ return [f"{word} with status {item.status}, expected Ok"]
184
+ problems: list[str] = []
185
+ if item.decoded is not None and vector.fields is not None:
186
+ message = project.messages[vector.message]
187
+ expected = _expected_fields(project, message, vector.fields)
188
+ problems += [f"field {text}" for text in _differences(expected, item.decoded, "")]
189
+ if item.encoded is not None and item.encoded != vector.data:
190
+ problems.append(f"encodes to {item.encoded.hex(' ')}, expected {vector.text.strip()}")
191
+ return problems
192
+
193
+
194
+ def check(project: Project, plugins: dict[str, ChecksumPlugin] | None = None) -> list[Issue]:
195
+ """Return SR-22 issues where the codec and a vector disagree.
196
+
197
+ Without plugins a vector that needs a custom checksum is skipped with a warning.
198
+ """
199
+ issues: list[Issue] = []
200
+ for item in run(project, plugins):
201
+ source = item.vector.source
202
+ label = f"vector {item.vector.name}"
203
+ label += f" (profile {item.profile})" if item.profile else ""
204
+ if NO_PLUGIN in item.detail:
205
+ text = f"[SR-22] {label}: skipped, a custom checksum needs its plugin"
206
+ issues.append(Issue(source.file, source.line, source.pointer, text, "warning"))
207
+ continue
208
+ issues += [source.issue("SR-22", f"{label}: {text}") for text in _problems(project, item)]
209
+ return issues
210
+
211
+
212
+ def validate_project(
213
+ location: Path, plugins: dict[str, ChecksumPlugin] | None = None
214
+ ) -> list[Issue]:
215
+ """Load a project and return its structural, semantic and vector issues."""
216
+ result = load_project(location)
217
+ if result.project is None:
218
+ return result.issues
219
+ issues = result.issues + Validator(result.project).run()
220
+ if not errors_only(issues):
221
+ issues += check(result.project, plugins)
222
+ return issues
@@ -0,0 +1,137 @@
1
+ """File-format identity, compatibility policy and migrations.
2
+
3
+ Policy (rule SR-19):
4
+ - major newer than this tool supports -> error (upgrade WireForge)
5
+ - major older -> error unless imported through `migrate_document`
6
+ - same major, minor newer than supported -> error
7
+ - generator newer than this tool -> warning only
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from contextlib import contextmanager
14
+ from typing import TYPE_CHECKING
15
+
16
+ from wireforge import __version__
17
+ from wireforge.issues import Issue
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Callable, Iterator, MutableMapping
21
+
22
+ FORMAT_MAJOR = 1
23
+ FORMAT_MINOR = 2
24
+ GENERATOR_NAME = "wireforge"
25
+ TOOL_VERSION = __version__
26
+
27
+ _MIGRATIONS: dict[int, Callable[[MutableMapping[str, object]], None]] = {}
28
+ _SEMVER = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
29
+
30
+
31
+ def register_migration(
32
+ from_major: int, step: Callable[[MutableMapping[str, object]], None]
33
+ ) -> None:
34
+ """Register the step upgrading a document from `from_major` to `from_major + 1` in place."""
35
+ _MIGRATIONS[from_major] = step
36
+
37
+
38
+ @contextmanager
39
+ def temporary_migration(
40
+ from_major: int, step: Callable[[MutableMapping[str, object]], None]
41
+ ) -> Iterator[None]:
42
+ """Register a migration for the duration of a `with` block (tests and tooling)."""
43
+ previous = _MIGRATIONS.get(from_major)
44
+ _MIGRATIONS[from_major] = step
45
+ try:
46
+ yield
47
+ finally:
48
+ if previous is None:
49
+ _MIGRATIONS.pop(from_major, None)
50
+ else:
51
+ _MIGRATIONS[from_major] = previous
52
+
53
+
54
+ def migration_path(from_major: int) -> list[int] | None:
55
+ """Majors whose steps must run to reach FORMAT_MAJOR, or None when a step is missing."""
56
+ path = list(range(from_major, FORMAT_MAJOR))
57
+ return path if all(major in _MIGRATIONS for major in path) else None
58
+
59
+
60
+ def _semver_tuple(text: object) -> tuple[int, int, int] | None:
61
+ match = _SEMVER.match(text) if isinstance(text, str) else None
62
+ return None if match is None else (int(match[1]), int(match[2]), int(match[3]))
63
+
64
+
65
+ def check_document(
66
+ document: MutableMapping[str, object], *, file: str, line: int | None = None
67
+ ) -> list[Issue]:
68
+ """Apply the compatibility policy to the version stamp of one document."""
69
+ version = document.get("schema_version")
70
+ if isinstance(version, bool) or not isinstance(version, int):
71
+ message = "[SR-19] missing or invalid schema_version: the file does not say its format"
72
+ return [Issue(file, line, "/schema_version", message)]
73
+ if version > FORMAT_MAJOR:
74
+ message = (
75
+ f"[SR-19] file uses format version {version} but this WireForge {TOOL_VERSION} "
76
+ f"supports up to {FORMAT_MAJOR}; upgrade WireForge"
77
+ )
78
+ return [Issue(file, line, "/schema_version", message)]
79
+ if version < FORMAT_MAJOR:
80
+ hint = (
81
+ "run `wireforge import` to migrate it"
82
+ if migration_path(version) is not None
83
+ else "no migration path is available"
84
+ )
85
+ message = f"[SR-19] file uses older format version {version}; {hint}"
86
+ return [Issue(file, line, "/schema_version", message)]
87
+ issues: list[Issue] = []
88
+ minor = document.get("schema_minor", 0)
89
+ if isinstance(minor, int) and minor > FORMAT_MINOR:
90
+ message = (
91
+ f"[SR-19] file needs format {version}.{minor} but this WireForge supports "
92
+ f"{FORMAT_MAJOR}.{FORMAT_MINOR}"
93
+ )
94
+ issues.append(Issue(file, line, "/schema_minor", message))
95
+ generator = document.get("generator")
96
+ generator_version = generator.get("version") if isinstance(generator, dict) else None
97
+ written = _semver_tuple(generator_version)
98
+ current = _semver_tuple(TOOL_VERSION)
99
+ if written is not None and current is not None and written[:2] > current[:2]:
100
+ message = (
101
+ f"file was written by WireForge {generator_version}, newer than {TOOL_VERSION}; "
102
+ "unknown features may be rejected"
103
+ )
104
+ issues.append(Issue(file, line, "/generator", message, severity="warning"))
105
+ return issues
106
+
107
+
108
+ def migrate_document(document: MutableMapping[str, object]) -> list[str]:
109
+ """Upgrade `document` in place to the current format; returns the applied steps.
110
+
111
+ Raises TypeError without an integer schema_version, ValueError when the file is newer than
112
+ supported or a migration step is missing.
113
+ """
114
+ version = document.get("schema_version")
115
+ if isinstance(version, bool) or not isinstance(version, int):
116
+ message = "document has no integer schema_version"
117
+ raise TypeError(message)
118
+ if version > FORMAT_MAJOR:
119
+ message = f"format {version} is newer than supported {FORMAT_MAJOR}"
120
+ raise ValueError(message)
121
+ path = migration_path(version)
122
+ if path is None:
123
+ message = f"no migration path from format {version} to {FORMAT_MAJOR}"
124
+ raise ValueError(message)
125
+ applied = []
126
+ for major in path:
127
+ _MIGRATIONS[major](document)
128
+ applied.append(f"format {major} -> {major + 1}")
129
+ stamp(document)
130
+ return applied
131
+
132
+
133
+ def stamp(document: MutableMapping[str, object]) -> None:
134
+ """Write the current format and generator stamp into `document`."""
135
+ document["schema_version"] = FORMAT_MAJOR
136
+ document["schema_minor"] = FORMAT_MINOR
137
+ document["generator"] = {"name": GENERATOR_NAME, "version": TOOL_VERSION}