flashgate 0.5.0__tar.gz → 0.5.2__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 (29) hide show
  1. {flashgate-0.5.0 → flashgate-0.5.2}/PKG-INFO +10 -2
  2. {flashgate-0.5.0 → flashgate-0.5.2}/README.md +5 -0
  3. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate/cli.py +1 -1
  4. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate/mcp_server.py +91 -23
  5. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate.egg-info/PKG-INFO +10 -2
  6. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate.egg-info/requires.txt +1 -1
  7. {flashgate-0.5.0 → flashgate-0.5.2}/pyproject.toml +11 -2
  8. {flashgate-0.5.0 → flashgate-0.5.2}/tests/test_cli.py +19 -9
  9. {flashgate-0.5.0 → flashgate-0.5.2}/tests/test_mcp.py +30 -0
  10. {flashgate-0.5.0 → flashgate-0.5.2}/LICENSE +0 -0
  11. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate/__init__.py +0 -0
  12. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate/__main__.py +0 -0
  13. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate/board.py +0 -0
  14. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate/flasher.py +0 -0
  15. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate/gatestate.py +0 -0
  16. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate/probes.py +0 -0
  17. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate/results.py +0 -0
  18. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate/serialmon.py +0 -0
  19. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate/sttools.py +0 -0
  20. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate/swdsig.py +0 -0
  21. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate.egg-info/SOURCES.txt +0 -0
  22. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate.egg-info/dependency_links.txt +0 -0
  23. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate.egg-info/entry_points.txt +0 -0
  24. {flashgate-0.5.0 → flashgate-0.5.2}/flashgate.egg-info/top_level.txt +0 -0
  25. {flashgate-0.5.0 → flashgate-0.5.2}/setup.cfg +0 -0
  26. {flashgate-0.5.0 → flashgate-0.5.2}/tests/test_board.py +0 -0
  27. {flashgate-0.5.0 → flashgate-0.5.2}/tests/test_gatestate.py +0 -0
  28. {flashgate-0.5.0 → flashgate-0.5.2}/tests/test_probes.py +0 -0
  29. {flashgate-0.5.0 → flashgate-0.5.2}/tests/test_swdsig.py +0 -0
@@ -1,15 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashgate
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Hardware-in-the-loop verification gate: your agent can't claim the firmware works until the board says so.
5
5
  License: MIT
6
+ Project-URL: Homepage, https://github.com/Lion-1209/flashgate
7
+ Project-URL: Repository, https://github.com/Lion-1209/flashgate
8
+ Project-URL: Issues, https://github.com/Lion-1209/flashgate/issues
6
9
  Requires-Python: >=3.11
7
10
  Description-Content-Type: text/markdown
8
11
  License-File: LICENSE
9
12
  Requires-Dist: pyserial>=3.5
10
13
  Requires-Dist: PyYAML>=6.0
11
14
  Provides-Extra: mcp
12
- Requires-Dist: mcp>=1.2; extra == "mcp"
15
+ Requires-Dist: mcp<3,>=1.2; extra == "mcp"
13
16
  Provides-Extra: dev
14
17
  Requires-Dist: pytest>=8; extra == "dev"
15
18
  Dynamic: license-file
@@ -83,6 +86,11 @@ fresh clone verifies out of the box once wired up.
83
86
  | 6 | environment error (no ST-Link / serial / tools) — including probes explicitly required via `--probe`/`--all-probes` but the console UART is unavailable: a check that cannot run never counts as a pass |
84
87
  | 7 | functional probe failed |
85
88
 
89
+ ## Contributing
90
+
91
+ Board profiles, probes, fixes and docs are welcome — see
92
+ [CONTRIBUTING.md](CONTRIBUTING.md) for the setup and contribution terms.
93
+
86
94
  ## The Stop hook
87
95
 
88
96
  `hooks/flashgate_stop.py` is a Claude Code Stop hook (any harness
@@ -67,6 +67,11 @@ fresh clone verifies out of the box once wired up.
67
67
  | 6 | environment error (no ST-Link / serial / tools) — including probes explicitly required via `--probe`/`--all-probes` but the console UART is unavailable: a check that cannot run never counts as a pass |
68
68
  | 7 | functional probe failed |
69
69
 
70
+ ## Contributing
71
+
72
+ Board profiles, probes, fixes and docs are welcome — see
73
+ [CONTRIBUTING.md](CONTRIBUTING.md) for the setup and contribution terms.
74
+
70
75
  ## The Stop hook
71
76
 
72
77
  `hooks/flashgate_stop.py` is a Claude Code Stop hook (any harness
@@ -400,7 +400,7 @@ def _verify_uart(board: Board, probe_names: list[str] | None) -> int:
400
400
  return _run_probes(board, None if all_probes else probe_names, conn)
401
401
 
402
402
  print(_green(f"[verify] PASS — the board itself confirms the firmware booted "
403
- f"(git={got})"))
403
+ f"(git={info.get('git')})"))
404
404
  return EXIT_OK
405
405
  finally:
406
406
  conn.close()
@@ -14,7 +14,8 @@ Tools: board_info, doctor, build, flash, verify, probe, console_send,
14
14
  console_read.
15
15
 
16
16
  Every tool returns a `flashgate.results.Result` envelope (schema_version,
17
- status, stable code, summary, CLI exit_code, data, warnings, policy) —
17
+ status, stable code, summary, CLI exit_code, data, evidence, warnings,
18
+ policy) —
18
19
  never a bare log the model has to parse. With mcp 2.x the envelope is
19
20
  emitted as MCP structuredContent (registered via structured_output=True)
20
21
  plus a JSON text block; on the mcp 1.x fallback the JSON text is still
@@ -138,8 +139,17 @@ def _safe(policy: dict):
138
139
  @_register(annotations=_ann(readOnlyHint=True), structured=True)
139
140
  @_safe(P_READ)
140
141
  def board_info(board: str | None = None) -> results.Result:
141
- """Show the active board profile: firmware dir, artifact, watch globs,
142
- banner contract, and the functional probes it defines."""
142
+ """Discover what hardware you are working with: the active board
143
+ profile.
144
+
145
+ Call this FIRST in any session — it tells you the board identity
146
+ (name, MCU), where the firmware builds (firmware_dir, artifact), how
147
+ it flashes (flash connection), and — most importantly — which
148
+ functional probes exist (the "probes" list holds the names the probe
149
+ tool accepts). Read-only; touches neither host nor board.
150
+
151
+ Result: data carries the full profile; warnings flag an unloadable
152
+ probes section (fix the board YAML before relying on probes)."""
143
153
  try:
144
154
  b = _board(board)
145
155
  except BoardError as exc:
@@ -149,7 +159,7 @@ def board_info(board: str | None = None) -> results.Result:
149
159
  warnings: list[str] = []
150
160
  try:
151
161
  probes = list(probe_mod.load_probes(b.yaml_path))
152
- except (OSError, ValueError) as exc:
162
+ except (OSError, ValueError, probe_mod.ProbeError) as exc:
153
163
  warnings.append(f"probes unloadable: {exc}")
154
164
  r = results.ok(
155
165
  f"{b.name} ({b.mcu}) — {len(probes)} probe(s) defined",
@@ -172,8 +182,14 @@ def board_info(board: str | None = None) -> results.Result:
172
182
  @_register(annotations=_ann(readOnlyHint=True), structured=True)
173
183
  @_safe(P_READ)
174
184
  def doctor(board: str | None = None) -> results.Result:
175
- """Check hardware prerequisites: ST-Link probe, console serial port,
176
- toolchain. Run this first when anything else fails (exit code 6)."""
185
+ """Diagnose the bench: is the ST-Link probe visible, is the console
186
+ serial port resolvable, is the cross toolchain installed?
187
+
188
+ Run this before anything else when a board operation fails — and
189
+ ALWAYS when a tool returns status "incomplete" or code
190
+ CAPABILITY_UNAVAILABLE; the report names the exact missing link.
191
+ Read-only. Findings are listed in data.log under "issues:"; an empty
192
+ problems list with exit_code 0 means every prerequisite is healthy."""
177
193
  try:
178
194
  rc, log = _capture(cli_mod.cmd_doctor, _board(board))
179
195
  except BoardError as exc:
@@ -185,7 +201,14 @@ def doctor(board: str | None = None) -> results.Result:
185
201
  @_register(annotations=_ann(idempotentHint=True), structured=True)
186
202
  @_safe(P_BUILD)
187
203
  def build(board: str | None = None) -> results.Result:
188
- """Build the firmware (incremental). Fails with code BUILD_FAILED."""
204
+ """Compile the board's firmware (incremental — recompiles only what
205
+ changed). No hardware is touched.
206
+
207
+ Mostly useful on its own for a fast failure loop; verify runs the
208
+ same build internally, so you don't need this before verify. Result:
209
+ succeeded + exit_code 0 = clean build; failed + BUILD_FAILED = the
210
+ compiler output in data.log names the offending file and line. The
211
+ binary lands at the profile's artifact path, ready for flash."""
189
212
  try:
190
213
  rc, log = _capture(cli_mod.cmd_build, _board(board))
191
214
  except BoardError as exc:
@@ -197,8 +220,16 @@ def build(board: str | None = None) -> results.Result:
197
220
  @_register(annotations=_ann(destructiveHint=True), structured=True)
198
221
  @_safe(P_FLASH)
199
222
  def flash(board: str | None = None) -> results.Result:
200
- """Flash + verify + start via ST-Link (with auto-retry). Destructive:
201
- replaces the firmware running on the board."""
223
+ """Write the built firmware to the board's flash over ST-Link,
224
+ verify the write, then start the application. DESTRUCTIVE: whatever
225
+ runs on the MCU is replaced.
226
+
227
+ Flashes the artifact from the last build — call build first if
228
+ sources changed since. Tries up to 3 times on transient probe
229
+ errors. Any failure — including a missing ST-Link or CubeProgrammer —
230
+ surfaces as failed + FLASH_FAILED, exit_code 2; data.log carries the
231
+ programmer output. Run doctor to tell an environment problem (no
232
+ probe / no tools) apart from a write failure."""
202
233
  try:
203
234
  rc, log = _capture(cli_mod.cmd_flash, _board(board))
204
235
  except BoardError as exc:
@@ -210,12 +241,27 @@ def flash(board: str | None = None) -> results.Result:
210
241
  @_register(annotations=_ann(destructiveHint=True), structured=True)
211
242
  @_safe(P_VERIFY)
212
243
  def verify(board: str | None = None) -> results.Result:
213
- """The full gate: build -> flash -> boot evidence -> identity check ->
214
- all functional probes. status=succeeded means the BOARD ITSELF confirms
215
- the firmware works. Failure codes: BUILD_FAILED, FLASH_FAILED,
216
- BOOT_EVIDENCE_TIMEOUT, BOOT_ERROR, IDENTITY_MISMATCH,
217
- CAPABILITY_UNAVAILABLE (console needed by probes but unavailable),
218
- PROBE_FAILED."""
244
+ """The hardware gate — the authoritative answer to "does this
245
+ firmware actually work on the board?". status=succeeded means the
246
+ BOARD ITSELF booted the exact tree you are on (its reported git sha
247
+ matches the working tree) and every functional probe passed. Trust no
248
+ other signal; a clean build is not a pass.
249
+
250
+ What it does: rebuild the tree, flash over ST-Link, start the app,
251
+ then require boot evidence proving WHICH build is running (UART
252
+ banner; or, without a serial cable, an SWD RAM signature — wiped
253
+ first, best-effort, so a stale one cannot lie), then run every
254
+ defined probe, asserting on the board's answers — including live
255
+ register readbacks where the board provides them. DESTRUCTIVE:
256
+ rewrites the board's flash.
257
+
258
+ The failure code names the broken stage — BUILD_FAILED (compile),
259
+ FLASH_FAILED (write/start), BOOT_EVIDENCE_TIMEOUT (board silent),
260
+ BOOT_ERROR (fault string on serial), IDENTITY_MISMATCH (board runs a
261
+ different tree), CAPABILITY_UNAVAILABLE (a required check could not
262
+ run — most often probes needing the console UART, which is missing or
263
+ held by another program), PROBE_FAILED (a functional assertion did
264
+ not hold). Full transcript in data.log."""
219
265
  try:
220
266
  rc, log = _capture(cli_mod.cmd_verify, _board(board), ["all"])
221
267
  except BoardError as exc:
@@ -227,10 +273,17 @@ def verify(board: str | None = None) -> results.Result:
227
273
  @_register(annotations=_ann(readOnlyHint=False), structured=True)
228
274
  @_safe(P_PROBE)
229
275
  def probe(names: list[str] | None = None, board: str | None = None) -> results.Result:
230
- """Run functional probes against the ALREADY RUNNING firmware (no
231
- rebuild/reflash). names=None or [] runs every probe (same semantics as
232
- the CLI). The probe stage needs the console UART; without it the result
233
- is incomplete / CAPABILITY_UNAVAILABLE — never a pass."""
276
+ """Exercise the firmware ALREADY RUNNING on the board with its
277
+ defined probes: each probe sends console commands and asserts on the
278
+ board's answers — including live register readbacks (e.g. the timer
279
+ value actually driving an LED). No rebuild, no reflash.
280
+
281
+ names picks specific probes (valid names are in board_info's
282
+ "probes" list); omit it or pass [] to run all. Probes need the
283
+ console UART: without it the result is incomplete +
284
+ CAPABILITY_UNAVAILABLE — never a pass. failed + PROBE_FAILED means an
285
+ assertion did not hold; data.log shows the exact step, the board's
286
+ last response, and which value was off."""
234
287
  try:
235
288
  b = _board(board)
236
289
  except BoardError as exc:
@@ -269,8 +322,17 @@ def probe(names: list[str] | None = None, board: str | None = None) -> results.R
269
322
  @_register(annotations=_ann(readOnlyHint=False, openWorldHint=True), structured=True)
270
323
  @_safe(P_CONSOLE_SEND)
271
324
  def console_send(line: str, wait_s: float = 1.0, board: str | None = None) -> results.Result:
272
- """Send ONE line to the firmware console (e.g. 'led0?' or 'selftest')
273
- and return the response lines received within wait_s seconds."""
325
+ """Talk to the firmware directly: send ONE command line on the
326
+ console UART and collect its answer.
327
+
328
+ Example: line "led0?" — the firmware replies with its LED state and
329
+ the live PWM register value (a hardware readback, not a cached
330
+ variable). The command set is board-specific — board_info and the
331
+ board's probe definitions show the known commands; unknown commands
332
+ typically answer "ERR unknown-cmd". The reply lands in
333
+ data.response within wait_s seconds; silence is reported as
334
+ "(no response)" in the summary + a warning — check wiring before
335
+ concluding the firmware is dead."""
274
336
  try:
275
337
  b = _board(board)
276
338
  except BoardError as exc:
@@ -306,8 +368,14 @@ def console_send(line: str, wait_s: float = 1.0, board: str | None = None) -> re
306
368
  @_register(annotations=_ann(readOnlyHint=True), structured=True)
307
369
  @_safe(P_READ)
308
370
  def console_read(seconds: float = 2.0, board: str | None = None) -> results.Result:
309
- """Read whatever the firmware prints on the console for N seconds
310
- (banner, self-test output, fault dumps)."""
371
+ """Listen passively: capture everything the firmware prints on the
372
+ console for the given number of seconds, sending nothing.
373
+
374
+ Use it to watch boot banners, self-test prints, or fault dumps
375
+ (HardFault traces appear here). Output lands in data.text; a fully
376
+ silent window is reported as "(console silent)" in the summary —
377
+ which for a just-reset board usually means wiring or baud-rate
378
+ trouble rather than a healthy quiet firmware. Read-only."""
311
379
  try:
312
380
  b = _board(board)
313
381
  except BoardError as exc:
@@ -1,15 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashgate
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Hardware-in-the-loop verification gate: your agent can't claim the firmware works until the board says so.
5
5
  License: MIT
6
+ Project-URL: Homepage, https://github.com/Lion-1209/flashgate
7
+ Project-URL: Repository, https://github.com/Lion-1209/flashgate
8
+ Project-URL: Issues, https://github.com/Lion-1209/flashgate/issues
6
9
  Requires-Python: >=3.11
7
10
  Description-Content-Type: text/markdown
8
11
  License-File: LICENSE
9
12
  Requires-Dist: pyserial>=3.5
10
13
  Requires-Dist: PyYAML>=6.0
11
14
  Provides-Extra: mcp
12
- Requires-Dist: mcp>=1.2; extra == "mcp"
15
+ Requires-Dist: mcp<3,>=1.2; extra == "mcp"
13
16
  Provides-Extra: dev
14
17
  Requires-Dist: pytest>=8; extra == "dev"
15
18
  Dynamic: license-file
@@ -83,6 +86,11 @@ fresh clone verifies out of the box once wired up.
83
86
  | 6 | environment error (no ST-Link / serial / tools) — including probes explicitly required via `--probe`/`--all-probes` but the console UART is unavailable: a check that cannot run never counts as a pass |
84
87
  | 7 | functional probe failed |
85
88
 
89
+ ## Contributing
90
+
91
+ Board profiles, probes, fixes and docs are welcome — see
92
+ [CONTRIBUTING.md](CONTRIBUTING.md) for the setup and contribution terms.
93
+
86
94
  ## The Stop hook
87
95
 
88
96
  `hooks/flashgate_stop.py` is a Claude Code Stop hook (any harness
@@ -5,4 +5,4 @@ PyYAML>=6.0
5
5
  pytest>=8
6
6
 
7
7
  [mcp]
8
- mcp>=1.2
8
+ mcp<3,>=1.2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flashgate"
7
- version = "0.5.0"
7
+ version = "0.5.2"
8
8
  description = "Hardware-in-the-loop verification gate: your agent can't claim the firmware works until the board says so."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -14,13 +14,22 @@ dependencies = [
14
14
  "PyYAML>=6.0",
15
15
  ]
16
16
 
17
+ [project.urls]
18
+ Homepage = "https://github.com/Lion-1209/flashgate"
19
+ Repository = "https://github.com/Lion-1209/flashgate"
20
+ Issues = "https://github.com/Lion-1209/flashgate/issues"
21
+
17
22
  [project.optional-dependencies]
18
- mcp = ["mcp>=1.2"]
23
+ mcp = ["mcp>=1.2,<3"]
19
24
  dev = ["pytest>=8"]
20
25
 
21
26
  [project.scripts]
22
27
  flashgate = "flashgate.cli:main"
23
28
  flashgate-mcp = "flashgate.mcp_server:main"
24
29
 
30
+ [tool.pytest.ini_options]
31
+ # keep pytest from climbing into parent workspaces for config
32
+ testpaths = ["tests"]
33
+
25
34
  [tool.setuptools.packages.find]
26
35
  include = ["flashgate*"]
@@ -148,12 +148,22 @@ class TestAutoRouting:
148
148
  assert seen["mode"] == "uart"
149
149
 
150
150
 
151
- class TestEnvelopeCoupling:
152
- def test_exit_env_maps_to_incomplete_never_succeeded(self):
153
- # Belt-and-suspenders (mutation finding M5): the constant every
154
- # fail-closed guard asserts must map to "incomplete" in the MCP
155
- # envelope. If the mapping ever flips to "succeeded", a check that
156
- # could not run would count as a pass.
157
- from flashgate import results
158
- assert results.from_exit(cli.EXIT_ENV, "x").status == "incomplete"
159
- assert results.from_exit(cli.EXIT_PROBE_FAIL, "x").status == "failed"
151
+ class TestUartPassPath:
152
+ def test_plain_uart_verify_passes_without_probe_flags(self, tmp_path, monkeypatch, capsys):
153
+ # Regression from 0.4.2: the identity-check refactor left a NameError
154
+ # on the uart PASS print (git={got} referenced a moved local) — verify
155
+ # crashed AFTER everything had passed, exit 1 (misread as build fail).
156
+ board = make_board(tmp_path)
157
+ monkeypatch.setattr(cli, "_console_port",
158
+ lambda b: ("COM77", "pinned")) # host serial topology must not matter
159
+ monkeypatch.setattr(cli, "_build", lambda b: cli.EXIT_OK)
160
+ monkeypatch.setattr(cli, "cmd_flash", lambda b: cli.EXIT_OK)
161
+ monkeypatch.setattr(cli.serialmon, "open_flush",
162
+ lambda *a, **k: SimpleNamespace(close=lambda: None))
163
+ monkeypatch.setattr(cli.serialmon, "wait_on",
164
+ lambda *a, **k: SimpleNamespace(
165
+ matched=True, error_hit=None, transcript="",
166
+ groups={"board": "test-board", "git": "abc1234"}))
167
+ monkeypatch.setattr(Board, "head_sha", lambda self: "abc1234")
168
+ assert cli._verify_uart(board, None) == cli.EXIT_OK
169
+ assert "PASS" in capsys.readouterr().out
@@ -136,6 +136,17 @@ class TestProbeTool:
136
136
  assert "COMX" in sc["summary"]
137
137
 
138
138
 
139
+ class TestExitCoupling:
140
+ def test_exit_env_maps_to_incomplete_never_succeeded(self):
141
+ # Belt-and-suspenders (mutation finding M5): the CLI constants the
142
+ # fail-closed guards assert must map to "incomplete"/"failed" in the
143
+ # envelope. If the mapping ever flips to "succeeded", a check that
144
+ # could not run would count as a pass.
145
+ from flashgate import cli
146
+ assert results.from_exit(cli.EXIT_ENV, "x").status == "incomplete"
147
+ assert results.from_exit(cli.EXIT_PROBE_FAIL, "x").status == "failed"
148
+
149
+
139
150
  class TestProbeEdgeCases:
140
151
  """Regressions from the adversarial review of 0.5.0."""
141
152
 
@@ -195,3 +206,22 @@ class TestCatchAllGuard:
195
206
  assert sc["status"] == "failed"
196
207
  assert sc["code"] == results.INTERNAL_ERROR
197
208
  assert "kaboom" in sc["summary"]
209
+
210
+
211
+ class TestBoardInfoProbesWarning:
212
+ def test_no_step_probe_becomes_warning_not_internal_error(self, tmp_path):
213
+ # F2 regression: board_info claimed "warnings flag an unloadable
214
+ # probes section" but ProbeError (probe with no steps) escaped to
215
+ # INTERNAL_ERROR. The except tuple must match the probe tool's.
216
+ yaml = tmp_path / "board.yaml"
217
+ yaml.write_text(
218
+ "board: t\nmcu: m\ndescription: d\nfirmware:\n dir: fw\n"
219
+ " build: ninja -C build\n artifact: build/fw.bin\nserial:\n"
220
+ " baudrate: 9600\n banner: 'BOOT {git}'\n"
221
+ "probes:\n broken: {}\n", encoding="utf-8")
222
+ (tmp_path / "fw").mkdir()
223
+ res = asyncio.run(srv.mcp.call_tool("board_info", {"board": str(yaml)}))
224
+ sc = res.structured_content
225
+ assert sc["status"] == "succeeded" # profile itself is fine
226
+ assert sc["data"]["probes"] == []
227
+ assert any("probes unloadable" in w for w in sc["warnings"])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes