firmwareloop 0.0.8__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.
@@ -0,0 +1,455 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Final acceptance scenario runner (Spec §33 + M6). Drives the complete
4
+ pipeline in one call and writes a final report with all nine mandated
5
+ sections. In simulator mode every leg runs against the real compiled
6
+ artifact / simulated instruments and is fully verifiable offline; with
7
+ real hardware, -Hardware agentic-hil swaps the flash/reset/UART legs to
8
+ Agentic HIL MCP-equivalent CLI verbs.
9
+
10
+ Pipeline (Spec §33):
11
+ build -> flash -> reset -> UART -> logic -> scope -> pytest -> report
12
+
13
+ Usage:
14
+ .\tools\acceptance-scenario.ps1 -Json # simulator
15
+ .\tools\acceptance-scenario.ps1 -Hardware agentic-hil -Json # real DUT
16
+
17
+ Output: artifacts/runs/<run_id>/final-report.json
18
+ #>
19
+ [CmdletBinding()]
20
+ param(
21
+ [ValidateSet('simulator', 'real')]
22
+ [string]$Mode = 'simulator',
23
+ [ValidateSet('simulator', 'agentic-hil')]
24
+ [string]$Hardware, # deprecated alias for -Mode
25
+ [string]$Scenario = 'SPI Flash JEDEC ID occasionally fails - fix driver, rebuild, verify',
26
+ [switch]$Json
27
+ )
28
+
29
+ if ($Hardware) {
30
+ $Mode = if ($Hardware -eq 'agentic-hil') { 'real' } else { 'simulator' }
31
+ Write-Warning '-Hardware is deprecated; use -Mode simulator|real'
32
+ }
33
+
34
+ Set-StrictMode -Version Latest
35
+ $ErrorActionPreference = 'Stop'
36
+ Import-Module (Join-Path $PSScriptRoot 'common\fw.psm1') -Force
37
+
38
+ $repoRoot = Get-FwRepoRoot
39
+ $python = Resolve-FwPython -RepoRoot $repoRoot
40
+ if (-not $python) {
41
+ $err = New-FwError -ErrorClass 'CONFIG_ERROR' -Message 'No Python found; run tools/doctor.ps1.'
42
+ if ($Json) { Write-FwJson $err -Compact } else { Write-FwJson $err }
43
+ exit 2
44
+ }
45
+
46
+ function Invoke-Tool {
47
+ param([string]$Script, [string[]]$ToolArgs)
48
+ $scriptPath = Join-Path $PSScriptRoot $Script
49
+ if ($Script -like '*.py') {
50
+ $filePath = $python
51
+ $runArgs = @($scriptPath) + $ToolArgs
52
+ } elseif ($Script -like '*.ps1') {
53
+ $filePath = 'pwsh'
54
+ $runArgs = @('-NoProfile', '-NonInteractive', '-File', $scriptPath) + $ToolArgs
55
+ } else {
56
+ $filePath = $scriptPath
57
+ $runArgs = $ToolArgs
58
+ }
59
+ $res = Invoke-FwProcess -FilePath $filePath -Arguments $runArgs `
60
+ -WorkingDirectory $repoRoot -TimeoutMs 600000
61
+ if ($res.exit_code -ne 0 -and $res.exit_code -ne 1) {
62
+ Write-Warning "$Script exited $($res.exit_code): $($res.stderr)"
63
+ }
64
+ # last JSON line of stdout
65
+ $jsonLine = $null
66
+ foreach ($l in ($res.stdout -split "`r?`n")) {
67
+ if ($l.TrimStart().StartsWith('{')) { $jsonLine = $l }
68
+ }
69
+ if ($jsonLine) {
70
+ try { return ($jsonLine | ConvertFrom-Json) }
71
+ catch {
72
+ Write-Warning "$Script JSON parse failed: $($_.Exception.Message) | line=[$jsonLine] | rc=$($res.exit_code) | stderr=[$($res.stderr)]"
73
+ return [pscustomobject]@{ ok = $false; error = "json parse: $($_.Exception.Message)"; raw_line = $jsonLine }
74
+ }
75
+ }
76
+ return [pscustomobject]@{ ok = $false; error = "no JSON from $Script"; raw = $res.stdout }
77
+ }
78
+
79
+ # ---- run identity --------------------------------------------------------------
80
+ $runId = Get-FwRunId
81
+ $runDir = Join-Path $repoRoot "artifacts\runs\$runId"
82
+ New-Item -ItemType Directory -Force -Path $runDir | Out-Null
83
+ $env:FW_RUN_DIR = $runDir
84
+
85
+ $report = [ordered]@{
86
+ schema = 'firmware-acceptance-report/v1'
87
+ run_id = $runId
88
+ scenario = $Scenario
89
+ execution_mode = $Mode
90
+ steps = [ordered]@{}
91
+ generated_at = Get-FwTimestamp
92
+ }
93
+
94
+ # ---- 1. build --------------------------------------------------------------------
95
+ $build = Invoke-Tool 'build.ps1' @('-Configuration', 'Debug', '-Json')
96
+ $report.steps.build = [ordered]@{
97
+ ok = $build.ok
98
+ errors = $build.errors
99
+ warnings = $build.warnings
100
+ artifact = $build.artifact
101
+ sha256 = $build.artifact_sha256
102
+ detail = if ($build.ok) { $null } else { $build.diagnostics }
103
+ }
104
+
105
+ # --- evidence: build.json + hardware.json (Spec §25; identity probing in real
106
+ # mode via Agentic HIL read-only commands - release-fix #8) -------------------
107
+ $build | Add-Member -NotePropertyName generated_at -NotePropertyValue (Get-FwTimestamp) -ErrorAction SilentlyContinue
108
+ $build | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $runDir 'build.json') -Encoding utf8
109
+
110
+ $probeSerial = $null
111
+ $comPort = $null
112
+ $targetIdentity = $null
113
+ if ($Mode -eq 'real') {
114
+ $ahilTool = $null
115
+ $g2 = Get-Command agentic-hil -ErrorAction SilentlyContinue
116
+ if ($g2) { $ahilTool = $g2.Source }
117
+ if (-not $ahilTool -and (Test-Path -LiteralPath (Join-Path $repoRoot '.venv\Scripts\agentic-hil.exe'))) {
118
+ $ahilTool = Join-Path $repoRoot '.venv\Scripts\agentic-hil.exe'
119
+ }
120
+ if ($ahilTool) {
121
+ $probes = Invoke-FwProcess -FilePath $ahilTool -Arguments @('debugger-probes') -WorkingDirectory $repoRoot -TimeoutMs 60000
122
+ if ($probes.exit_code -eq 0) {
123
+ try {
124
+ $pj = $probes.stdout | ConvertFrom-Json
125
+ if ($pj.Probes) { $probeSerial = ($pj.Probes -join ',') }
126
+ elseif ($pj.probes) { $probeSerial = ($pj.probes -join ',') }
127
+ } catch { }
128
+ }
129
+ $ports = Invoke-FwProcess -FilePath $ahilTool -Arguments @('com-ports') -WorkingDirectory $repoRoot -TimeoutMs 60000
130
+ if ($ports.exit_code -eq 0) {
131
+ try {
132
+ $pt = $ports.stdout | ConvertFrom-Json
133
+ if ($pt.ports) { $comPort = (@($pt.ports | ForEach-Object { $_.device }) -join ',') }
134
+ } catch { }
135
+ }
136
+ }
137
+ }
138
+ $hardwareInfo = [ordered]@{
139
+ schema = 'firmware-hardware-info/v1'
140
+ execution_mode = $Mode
141
+ target_identity = $targetIdentity
142
+ probe_serial = $probeSerial
143
+ com_port = $comPort
144
+ source = if ($Mode -eq 'real') { 'agentic-hil' } else { 'simulator' }
145
+ generated_at = Get-FwTimestamp
146
+ }
147
+ $hardwareInfo | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $runDir 'hardware.json') -Encoding utf8
148
+
149
+ # ---- 2+3+4. real lane: Agentic HIL Test Reactor (GAP-004) -------------------------
150
+ # The plan (test-plans/real-smoke.yaml) drives flash -> reset -> UART expect
151
+ # with logical devices only. Missing devices make the reactor fail - never a
152
+ # simulator stand-in, never a PASS without hardware.
153
+ $reactor = $null
154
+ if ($Mode -eq 'real') {
155
+ $ahil = Join-Path $repoRoot '.venv\Scripts\agentic-hil.exe'
156
+ if (-not (Test-Path -LiteralPath $ahil)) {
157
+ $g = Get-Command agentic-hil -ErrorAction SilentlyContinue
158
+ if ($g) { $ahil = $g.Source }
159
+ }
160
+ if (-not $ahil) {
161
+ $report.steps.flash = [ordered]@{ ok = $false; error_class = 'REAL_HARDWARE_REQUIRED'; error = 'agentic-hil not installed; real mode requires the Agentic HIL test reactor' }
162
+ $report.steps.reset = [ordered]@{ ok = $false; error_class = 'REAL_HARDWARE_REQUIRED'; error = 'not executed (reactor unavailable)' }
163
+ $report.steps.uart = [ordered]@{ ok = $false; error_class = 'REAL_HARDWARE_REQUIRED'; error = 'not executed (reactor unavailable)' }
164
+ } else {
165
+ $plan = Join-Path $repoRoot 'test-plans\real-smoke.yaml'
166
+ # release-fix #3: resolve the REAL primary artifact from the
167
+ # firmware-artifacts/v1 manifest (build step) and inject it into a
168
+ # run-local copy of the plan - never hardcode firmware.elf in the plan.
169
+ $artifactManifest = Join-Path $repoRoot 'artifacts\build\artifacts.json'
170
+ $planPath = $plan
171
+ if (Test-Path -LiteralPath $artifactManifest) {
172
+ try {
173
+ $manifest = Get-Content -LiteralPath $artifactManifest -Raw | ConvertFrom-Json
174
+ $primaryProp = $manifest.primary
175
+ if ($primaryProp -and $primaryProp.native_path -and (Test-Path -LiteralPath $primaryProp.native_path)) {
176
+ $runPlan = Join-Path $runDir 'real-smoke.runtime.yaml'
177
+ $planText = Get-Content -LiteralPath $plan -Raw
178
+ $planText = [regex]::Replace($planText, '(?m)^(\s*image_path:\s*).*$', "`${1}$($primaryProp.native_path)")
179
+ $planText | Set-Content -LiteralPath $runPlan -Encoding utf8
180
+ $planPath = $runPlan
181
+ $report.steps.flash = [ordered]@{ artifact_source = 'firmware-artifacts/v1 manifest'; artifact = $primaryProp.native_path }
182
+ }
183
+ } catch { }
184
+ }
185
+ $reactor = Invoke-FwProcess -FilePath $ahil -Arguments @('test-reactor', '--test-config', $planPath, '--wait-s', '0') -WorkingDirectory $repoRoot -TimeoutMs 600000
186
+ $reactorOk = $reactor.exit_code -eq 0
187
+ $reactorText = ($reactor.stdout + $reactor.stderr)
188
+ # structured summary of the reactor verdict
189
+ $verdict = if ($reactorText -match '(?s)\{[^{}]*"ok"\s*:\s*(true|false)[^{}]*\}') {
190
+ $Matches[0]
191
+ } else { $reactorText }
192
+ $report.steps.flash = [ordered]@{ ok = $reactorOk; backend = 'agentic-hil-test-reactor'; plan = $planPath; result = ($verdict | Select-Object -First 1) }
193
+ $report.steps.reset = [ordered]@{ ok = $reactorOk; backend = 'agentic-hil-test-reactor'; note = 'flash+reset+UART expect driven by the reactor plan' }
194
+ $report.steps.uart = [ordered]@{ ok = $reactorOk; backend = 'agentic-hil-test-reactor'; note = 'UART expect inside plan (Application started / JEDEC EF 40 18)' }
195
+ }
196
+ } else {
197
+ $report.steps.flash = [ordered]@{
198
+ ok = $true
199
+ note = "simulator mode: artifact is the DUT; flash verified via firmware build + HIL run (real flash requires -Mode real)"
200
+ }
201
+ $reset = Invoke-Tool 'reset.ps1' @('-Backend', 'simulator', '-Json')
202
+ $report.steps.reset = [ordered]@{ ok = $reset.ok; backend = $reset.backend }
203
+
204
+ # ---- simulator UART leg -----------------------------------------------------
205
+ $artifact = Join-Path $repoRoot 'artifacts\build\firmware.elf'
206
+ $probe = Invoke-FwProcess -FilePath $python -Arguments @((Join-Path $PSScriptRoot 'common\uart_probe.py'), $artifact, 'jedec') -WorkingDirectory $repoRoot -TimeoutMs 60000
207
+ try { $uartResult = $probe.stdout | ConvertFrom-Json } catch { $uartResult = [ordered]@{ ok = $false; error = $probe.stderr } }
208
+ if ($probe.exit_code -eq 0) {
209
+ Save-FwLog -Path (Join-Path $runDir 'uart.log') -Content "RX: $($uartResult.banner -join ' | ')"
210
+ Save-FwLog -Path (Join-Path $runDir 'uart.log') -Content "TX: jedec"
211
+ Save-FwLog -Path (Join-Path $runDir 'uart.log') -Content "RX: $($uartResult.reply)"
212
+ }
213
+ $report.steps.uart = $uartResult
214
+ }
215
+
216
+ # ---- 5. logic analyzer ------------------------------------------------------------
217
+ # --- evidence: flash.json (Spec §25) ----------------------------------------------
218
+ $flashBackendProp = $report.steps.flash.PSObject.Properties['backend']
219
+ $flashEvidence = [ordered]@{
220
+ schema = 'firmware-flash-result/v1'
221
+ ok = $report.steps.flash.ok
222
+ backend = if ($flashBackendProp) { $flashBackendProp.Value } else { 'simulator' }
223
+ artifact = $report.steps.build.artifact
224
+ generated_at = Get-FwTimestamp
225
+ }
226
+ $flashEvidence | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $runDir 'flash.json') -Encoding utf8
227
+ # real mode: sigrok (or Saleae MCP in Qoder); simulator data is NEVER used to
228
+ # fabricate real evidence (GAP-007/008). Missing backend => REAL_HARDWARE_REQUIRED.
229
+ $logic = [ordered]@{ ok = $false }
230
+ if ($Mode -eq 'real') {
231
+ $cap = Invoke-Tool 'logic_capture.ps1' @('-Protocol', 'spi', '-Backend', 'sigrok', '-Json')
232
+ if (-not $cap.ok -or $cap.error_class) {
233
+ $logic = [ordered]@{
234
+ ok = $false
235
+ error_class = if ($cap.error_class) { $cap.error_class } else { 'REAL_HARDWARE_REQUIRED' }
236
+ error = 'real logic capture needs sigrok-cli or Saleae; simulator captures are not real evidence'
237
+ }
238
+ } else {
239
+ $decode = Invoke-Tool 'logic_decode.ps1' @('-Capture', (Split-Path $cap.capture), '-Expect', '9FEF4018', '-ExpectKind', 'hex', '-Json')
240
+ $logic = [ordered]@{
241
+ ok = $decode.ok
242
+ protocol = 'spi'
243
+ backend = 'sigrok'
244
+ real_hardware = $true
245
+ bytes_hex = $decode.bytes_hex
246
+ frames = $decode.frames
247
+ expectation = '9FEF4018'
248
+ capture = $cap.capture
249
+ decoded = if ($decode.out_file) { $decode.out_file } else { $null }
250
+ }
251
+ }
252
+ } else {
253
+ $cap = Invoke-Tool 'logic_capture.ps1' @('-Protocol', 'spi', '-Json')
254
+ if ($cap.ok) {
255
+ $decode = Invoke-Tool 'logic_decode.ps1' @('-Capture', (Split-Path $cap.capture), '-Expect', '9FEF4018', '-ExpectKind', 'hex', '-Json')
256
+ $logic = [ordered]@{
257
+ ok = $decode.ok
258
+ protocol = 'spi'
259
+ backend = 'simulator'
260
+ real_hardware = $false
261
+ bytes_hex = $decode.bytes_hex
262
+ frames = $decode.frames
263
+ expectation = '9FEF4018'
264
+ capture = $cap.capture
265
+ decoded = if ($decode.out_file) { $decode.out_file } else { $null }
266
+ }
267
+ }
268
+ }
269
+ $report.steps.logic = $logic
270
+
271
+ # ---- 6. scope / psu ---------------------------------------------------------------
272
+ # real mode reads lab/lab.yaml instruments (visa backend); without a trusted
273
+ # bench policy instrument writes fail closed (GAP-009), reads without a
274
+ # configured visa instrument report INSTRUMENT_NOT_FOUND - never simulator data.
275
+ $freq = Invoke-Tool 'instrument_cli.py' @('scope', 'measure-frequency', '--instrument', 'scope1') # run via python
276
+ $freqOk = $false
277
+ $freqVal = $null
278
+ if ($freq.ok) { $freqOk = $freq.ok; $freqVal = $freq.value }
279
+ $vpp = Invoke-Tool 'instrument_cli.py' @('scope', 'measure-vpp', '--instrument', 'scope1')
280
+ # real mode: measurements must come from a visa backend - simulator data is
281
+ # never real evidence (GAP-007/011)
282
+ $realFreq = ($freq.ok -and $freq.execution_mode -eq 'real')
283
+ $realVpp = ($vpp.ok -and $vpp.execution_mode -eq 'real')
284
+ $report.steps.scope = [ordered]@{
285
+ frequency_hz = $freqVal
286
+ vpp_v = if ($vpp.ok) { $vpp.value } else { $null }
287
+ ok = if ($Mode -eq 'real') { $realFreq -and $realVpp } else { $freqOk -and $vpp.ok }
288
+ execution_mode = $Mode
289
+ note = if ($Mode -eq 'real' -and -not ($realFreq -and $realVpp)) { 'measurements were not visa-backed; simulator data is not real evidence' } else { $null }
290
+ }
291
+
292
+ # persist instrument evidence (Spec §25 instruments/measurements.json)
293
+ $measPath = Join-Path $runDir 'measurements.json'
294
+ $meas = @()
295
+ if (Test-Path -LiteralPath $measPath) {
296
+ try { $meas = @(Get-Content -LiteralPath $measPath -Raw | ConvertFrom-Json) } catch { $meas = @() }
297
+ }
298
+ $freq | Add-Member -NotePropertyName timestamp -NotePropertyValue (Get-FwTimestamp) -ErrorAction SilentlyContinue
299
+ $meas += $freq
300
+ if ($vpp.ok) { $vpp | Add-Member -NotePropertyName timestamp -NotePropertyValue (Get-FwTimestamp) -ErrorAction SilentlyContinue; $meas += $vpp }
301
+ $meas | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $measPath -Encoding utf8
302
+
303
+ # ---- 7. pytest HIL -----------------------------------------------------------------
304
+ # real mode MUST run through test.ps1 -Mode real: required HIL tests that are
305
+ # skipped become failures there (release-fix #2), and the reported
306
+ # execution_mode is verified below - simulator/skip can never count as PASS.
307
+ $hilArgs = @('-Json')
308
+ if ($Mode -eq 'real') { $hilArgs = @('-Mode', 'real', '-Json') }
309
+ $hil = Invoke-Tool 'test.ps1' $hilArgs
310
+ $pytestOk = $hil.ok
311
+ $modeNote = $null
312
+ if ($Mode -eq 'real') {
313
+ $reported = $hil.execution_mode
314
+ if ($reported -ne 'real') {
315
+ $pytestOk = $false
316
+ $modeNote = "test.ps1 reported execution_mode='$reported' instead of 'real'; simulator/skip cannot count as real PASS"
317
+ } elseif (-not $hil.ok) {
318
+ $modeNote = 'real-mode HIL failed (skips count as failures)'
319
+ }
320
+ }
321
+ $report.steps.pytest = [ordered]@{
322
+ ok = $pytestOk
323
+ execution_mode = if ($Mode -eq 'real') { $hil.execution_mode } else { 'simulator' }
324
+ total = @($hil.tests).Count
325
+ passed = @($hil.tests | Where-Object status -eq 'passed').Count
326
+ failed = @($hil.tests | Where-Object status -eq 'failed').Count
327
+ skipped = @($hil.tests | Where-Object status -eq 'skipped').Count
328
+ summary = (Join-Path $runDir 'summary.json')
329
+ failures = @($hil.tests | Where-Object status -eq 'failed')
330
+ note = $modeNote
331
+ }
332
+
333
+ # ---- final report (Spec §33) --------------------------------------------------------
334
+ $git = Get-FwGitInfo -RepoRoot $repoRoot
335
+ $codeChanges = @()
336
+ $statusLines = git -C $repoRoot status --porcelain 2>$null
337
+ foreach ($line in $statusLines) {
338
+ if ($line -match '^..\s+(.+)$') { $codeChanges += $Matches[1].Trim('"') }
339
+ }
340
+
341
+ $finalReport = [ordered]@{
342
+ schema = 'firmware-acceptance-report/v1'
343
+ run_id = $runId
344
+ scenario = $Scenario
345
+ execution_mode = $Mode
346
+ root_cause_analysis = if ($Mode -eq 'real') {
347
+ if ($report.steps.pytest.ok) {
348
+ 'No defect reproduced on real hardware: JEDEC read returns 3-byte ID, UART/spi evidence captured from the DUT.'
349
+ } else {
350
+ 'Reproduced defect on real hardware: inspect steps.pytest.failures (expected/actual captured) and uart/logic evidence.'
351
+ }
352
+ } elseif ($report.steps.pytest.ok) {
353
+ 'No defect reproduced in simulated scenario: JEDEC read returns 3-byte ID, SPI decode 9F EF 40 18, PWM 20 kHz within limits.'
354
+ } else {
355
+ 'Reproduced defect: inspect steps.pytest.failures (expected/actual captured) and uart/logic evidence.'
356
+ }
357
+ code_changes = $codeChanges
358
+ build_result = @{
359
+ ok = $report.steps.build.ok
360
+ errors = $report.steps.build.errors
361
+ warnings = $report.steps.build.warnings
362
+ }
363
+ firmware_artifact = @{
364
+ path = $report.steps.build.artifact
365
+ sha256 = $report.steps.build.sha256
366
+ }
367
+ uart_evidence = if ($report.steps.uart.ok) { $report.steps.uart } else { $report.steps.uart }
368
+ logic_evidence = $report.steps.logic
369
+ scope_measurements = $report.steps.scope
370
+ test_result = $report.steps.pytest
371
+ remaining_risks = if ($Mode -eq 'real') {
372
+ @(
373
+ 'Real mode: verify the reactor report and physical evidence before trusting PASS.',
374
+ 'Instrument writes require an authoritative bench policy (%APPDATA%\FirmwareLoop\benches\<id>\limits.yaml or FIRMWARELOOP_BENCH_CONFIG).'
375
+ )
376
+ } else {
377
+ @(
378
+ 'Simulator mode: no silicon exercised; run with -Mode real against the real DUT for final acceptance.',
379
+ 'Real instruments (scope/psu) use simulator backend until VISA resources are configured in lab/lab.yaml.'
380
+ )
381
+ }
382
+ steps = $report.steps
383
+ git = $git
384
+ generated_at = Get-FwTimestamp
385
+ }
386
+
387
+ $reportPath = Join-Path $runDir 'final-report.json'
388
+ $finalReport | ConvertTo-Json -Depth 14 | Set-Content -LiteralPath $reportPath -Encoding utf8
389
+ $reportBody = [pscustomobject]$finalReport
390
+ $reportBody | Add-Member -NotePropertyName report_file -NotePropertyValue (Resolve-Path -LiteralPath $reportPath).Path
391
+
392
+ # ---- evidence layout (Spec §25): logic/ + instruments/ + final-report.md ------
393
+ $logicDir = Join-Path $runDir 'logic'
394
+ $instDir = Join-Path $runDir 'instruments'
395
+ New-Item -ItemType Directory -Force -Path $logicDir, $instDir | Out-Null
396
+ $logicCapProp = $report.steps.logic.PSObject.Properties['capture']
397
+ $logicDecProp = $report.steps.logic.PSObject.Properties['decoded']
398
+ if ($logicCapProp -and $logicCapProp.Value -and (Test-Path -LiteralPath $logicCapProp.Value)) {
399
+ Copy-Item -LiteralPath $logicCapProp.Value -Destination (Join-Path $logicDir 'capture.csv') -Force -ErrorAction SilentlyContinue
400
+ }
401
+ if ($logicDecProp -and $logicDecProp.Value -and (Test-Path -LiteralPath $logicDecProp.Value)) {
402
+ Copy-Item -LiteralPath $logicDecProp.Value -Destination (Join-Path $logicDir 'decode.json') -Force -ErrorAction SilentlyContinue
403
+ }
404
+ if (Test-Path -LiteralPath (Join-Path $runDir 'measurements.json')) {
405
+ Copy-Item -LiteralPath (Join-Path $runDir 'measurements.json') -Destination (Join-Path $instDir 'measurements.json') -Force -ErrorAction SilentlyContinue
406
+ }
407
+
408
+ $md = @()
409
+ $md += "# FirmwareLoop Acceptance Report — $runId"
410
+ $md += ""
411
+ $md += "- **scenario**: $Scenario"
412
+ $md += "- **execution_mode**: $Mode"
413
+ $md += "- **root cause**: $($finalReport.root_cause_analysis)"
414
+ $md += "- **git**: $($git.commit) (dirty=$($git.dirty))"
415
+ $md += ""
416
+ $md += "## Build"
417
+ $md += ""
418
+ $md += "- ok: $($finalReport.build_result.ok), errors: $($finalReport.build_result.errors), warnings: $($finalReport.build_result.warnings)"
419
+ $md += "- artifact: $($finalReport.firmware_artifact.path)"
420
+ $md += "- sha256: $($finalReport.firmware_artifact.sha256)"
421
+ $md += ""
422
+ $md += "## UART"
423
+ $md += ""
424
+ $uartReply = $finalReport.uart_evidence.PSObject.Properties['reply']
425
+ $uartErr = $finalReport.uart_evidence.PSObject.Properties['error']
426
+ if ($finalReport.uart_evidence.ok -and $uartReply) {
427
+ $md += "- reply: $($uartReply.Value)"
428
+ } else {
429
+ $md += "- ok: $($finalReport.uart_evidence.ok) ($(if ($uartErr) { $uartErr.Value } else { 'n/a' }))"
430
+ }
431
+ $md += ""
432
+ $md += "## Logic"
433
+ $md += ""
434
+ $logicHex = $finalReport.logic_evidence.PSObject.Properties['bytes_hex']
435
+ $logicProto = $finalReport.logic_evidence.PSObject.Properties['protocol']
436
+ $md += "- ok: $($finalReport.logic_evidence.ok), protocol: $(if ($logicProto) { $logicProto.Value } else { 'n/a' }), bytes: $(if ($logicHex) { $logicHex.Value } else { 'n/a' })"
437
+ $md += ""
438
+ $md += "## Scope"
439
+ $md += ""
440
+ $md += "- frequency: $($finalReport.scope_measurements.frequency_hz) Hz, vpp: $($finalReport.scope_measurements.vpp_v) V"
441
+ $md += ""
442
+ $md += "## Tests"
443
+ $md += ""
444
+ $md += "- ok: $($finalReport.test_result.ok), total: $($finalReport.test_result.total), failed: $($finalReport.test_result.failed)"
445
+ $md += ""
446
+ $md += "## Remaining risks"
447
+ $md += ""
448
+ foreach ($risk in $finalReport.remaining_risks) { $md += "- $risk" }
449
+ $md | Set-Content -LiteralPath (Join-Path $runDir 'final-report.md') -Encoding utf8
450
+
451
+ if ($Json) { Write-FwJson $reportBody -Compact } else { Write-FwJson $reportBody }
452
+
453
+ $allOk = $report.steps.build.ok -and $report.steps.logic.ok -and $report.steps.scope.ok -and $report.steps.pytest.ok
454
+ if ($Mode -eq 'real') { $allOk = $allOk -and $report.steps.flash.ok -and $report.steps.uart.ok }
455
+ exit $(if ($allOk) { 0 } else { 1 })
tools/build.ps1 ADDED
@@ -0,0 +1,208 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Build adapter (Spec §8, v0.0.2 GAP-002). Dispatches to the project's
4
+ existing build system across seven backends and returns structured
5
+ firmware-build-result/v1 JSON plus a firmware-artifacts/v1 manifest.
6
+
7
+ Backends: cmake | make | platformio | keil | iar | zephyr | esp-idf
8
+
9
+ Detection precedence (GAP-002):
10
+ 1. -Backend (explicit)
11
+ 2. project config (lab/lab.yaml -> project.build_backend)
12
+ 3. strong project marker (west.yml -> zephyr, sdkconfig/idf.py ->
13
+ esp-idf, platformio.ini, *.uvproj*, *.ewp, Makefile, CMakeLists.txt)
14
+ 4. generic detection
15
+
16
+ Usage:
17
+ .\tools\build.ps1 -Configuration Debug -Json
18
+ .\tools\build.ps1 -Backend make -SourceDir .\demo-make -Json
19
+ .\tools\build.ps1 -Backend keil -DryRun -Json # command construction
20
+ .\tools\build.ps1 -Backend keil -BackendTool C:\fake\UV4.cmd -Json
21
+
22
+ Exit codes:
23
+ 0 build ok (or dry-run construction ok)
24
+ 1 build failed (compile/link errors)
25
+ 2 configuration/toolchain error
26
+ #>
27
+ [CmdletBinding()]
28
+ param(
29
+ [ValidateSet('cmake', 'make', 'platformio', 'keil', 'iar', 'zephyr', 'esp-idf')]
30
+ [string]$Backend,
31
+ [string]$Configuration = 'Debug',
32
+ [switch]$Json,
33
+ [string]$SourceDir,
34
+ [string]$ArtifactDir,
35
+ [string]$BuildDir,
36
+ [string]$BackendTool, # override tool path (fake executable in tests)
37
+ [int]$TimeoutMs = 600000,
38
+ [switch]$SkipConfigure,
39
+ [switch]$Clean,
40
+ [switch]$DryRun # print constructed command, do not execute
41
+ )
42
+
43
+ Set-StrictMode -Version Latest
44
+ $ErrorActionPreference = 'Stop'
45
+ Import-Module (Join-Path $PSScriptRoot 'common\fw.psm1') -Force
46
+ Import-Module (Join-Path $PSScriptRoot 'common\build-backends.psm1') -Force
47
+
48
+ $repoRoot = Get-FwRepoRoot
49
+ $sw = [System.Diagnostics.Stopwatch]::StartNew()
50
+
51
+ function Exit-WithError {
52
+ param([string]$Class, [string]$Message, [string]$Detail)
53
+ $body = New-FwError -ErrorClass $Class -Message $Message -Detail $Detail
54
+ if ($Json) { Write-FwJson $body -Compact } else { Write-FwJson $body; Write-Error "$Class : $Message" -ErrorAction Continue }
55
+ exit 2
56
+ }
57
+
58
+ # ------------------------------------------------------- 1. backend resolution
59
+ $projBackend = $null
60
+ $labCfgObj = Get-FwLabConfig -RepoRoot $repoRoot
61
+ if ($labCfgObj) {
62
+ $projProp = $labCfgObj.PSObject.Properties['project']
63
+ if ($projProp) {
64
+ $bbProp = $projProp.Value.PSObject.Properties['build_backend']
65
+ if ($bbProp) { $projBackend = [string]$bbProp.Value }
66
+ }
67
+ }
68
+ $detect = Get-FwBackendDetect -ExplicitBackend $Backend -ProjectBackend $projBackend -SourceDir $SourceDir -RepoRoot $repoRoot
69
+ $backend = $detect.backend
70
+ $src = $detect.src
71
+ $projectFile = $detect.project_file
72
+ $detectSource = $detect.source
73
+
74
+ if (-not $backend) {
75
+ Exit-WithError -Class 'CONFIG_ERROR' -Message 'No build system detected (west.yml / sdkconfig / platformio.ini / *.uvproj* / *.ewp / Makefile / CMakeLists.txt). Pass -Backend and/or -SourceDir.' `
76
+ -Detail "project config backend: $projBackend"
77
+ }
78
+
79
+ if (-not $BuildDir) { $BuildDir = Join-Path $repoRoot "artifacts\build\$backend-$Configuration" }
80
+ if (-not $ArtifactDir) { $ArtifactDir = Join-Path $repoRoot 'artifacts\build' }
81
+ New-Item -ItemType Directory -Force -Path $BuildDir, $ArtifactDir, (Join-Path $repoRoot 'artifacts\logs') | Out-Null
82
+ if ($Clean) {
83
+ Remove-Item -Recurse -Force -LiteralPath $BuildDir -ErrorAction SilentlyContinue
84
+ New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
85
+ }
86
+
87
+ # ------------------------------------------------------- 2. command construction
88
+ $cmd = Get-FwBackendCommand -Backend $backend -Src $src -ProjectFile $projectFile `
89
+ -Configuration $Configuration -BuildDir $BuildDir -Tool $BackendTool `
90
+ -AllowMissingTool:$DryRun
91
+
92
+ if ($cmd.ContainsKey('missing')) {
93
+ $need = $cmd.missing
94
+ $hint = switch ($need) {
95
+ 'cmake' { 'winget install Kitware.CMake' }
96
+ 'make' { 'winget install GnuWin32.Make (or use CI: apt install make)' }
97
+ 'platformio' { 'pip install platformio' }
98
+ 'keil' { 'install Keil uVision (UV4.exe must be on PATH)' }
99
+ 'iar' { 'install IAR EWARM (IarBuild.exe must be on PATH)' }
100
+ 'zephyr' { 'pip install west && west init' }
101
+ 'esp-idf' { 'install ESP-IDF (idf.py must be on PATH)' }
102
+ 'keil-project' { 'no .uvproj* found in source dir' }
103
+ 'iar-project' { 'no .ewp found in source dir' }
104
+ 'zephyr-board' { $cmd.detail }
105
+ default { "tool '$need' not found" }
106
+ }
107
+ Exit-WithError -Class 'CONFIG_ERROR' -Message "backend '$backend' is unavailable: $need" -Detail $hint
108
+ }
109
+
110
+ $stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
111
+ $logFile = Join-Path $repoRoot "artifacts\logs\build-$stamp.log"
112
+ $logCanonical = Join-Path $repoRoot 'artifacts\logs\build.log'
113
+
114
+ if ($DryRun) {
115
+ $dry = [ordered]@{
116
+ schema = 'firmware-build-result/v1'
117
+ ok = $true
118
+ dry_run = $true
119
+ configuration = $Configuration
120
+ backend = $backend
121
+ command = $cmd.label
122
+ command_line = $cmd.file + ' ' + ($cmd.args -join ' ')
123
+ cwd = if ($cmd.cwd) { $cmd.cwd } else { $repoRoot }
124
+ artifact = $null
125
+ warnings = 0
126
+ errors = 0
127
+ duration_ms = 0
128
+ log = $null
129
+ git = Get-FwGitInfo -RepoRoot $repoRoot
130
+ generated_at = Get-FwTimestamp
131
+ }
132
+ if ($Json) { Write-FwJson ([pscustomobject]$dry) -Compact } else { Write-FwJson ([pscustomobject]$dry) }
133
+ exit 0
134
+ }
135
+
136
+ # ------------------------------------------------------- 3. execute
137
+ $res = Invoke-FwProcess -FilePath $cmd.file -Arguments $cmd.args `
138
+ -WorkingDirectory $(if ($cmd.cwd) { $cmd.cwd } else { $repoRoot }) `
139
+ -TimeoutMs $TimeoutMs -StdoutFile $logFile
140
+ # follow-up steps (e.g. cmake --build after configure)
141
+ $hasSteps = ($cmd -is [hashtable]) -and $cmd.ContainsKey('steps')
142
+ if ($res.exit_code -eq 0 -and $hasSteps) {
143
+ foreach ($step in $cmd.steps) {
144
+ $stepRes = Invoke-FwProcess -FilePath $step.file -Arguments $step.args `
145
+ -WorkingDirectory $(if ($step.cwd) { $step.cwd } else { $repoRoot }) `
146
+ -TimeoutMs $TimeoutMs -StdoutFile $logFile
147
+ $res = $stepRes
148
+ if ($res.exit_code -ne 0) { break }
149
+ }
150
+ }
151
+ Copy-Item -LiteralPath $logFile -Destination $logCanonical -Force -ErrorAction SilentlyContinue
152
+
153
+ $sw.Stop()
154
+ $durationMs = $sw.ElapsedMilliseconds
155
+
156
+ $allLines = @(($res.stdout -split "`r?`n") + ($res.stderr -split "`r?`n") | Where-Object { $_ })
157
+ $diags = Get-FwDiagnostics -Lines $allLines -BaseDir (Resolve-Path $src).Path
158
+ $errors = @($diags | Where-Object severity -eq 'error').Count
159
+ $warnings = @($diags | Where-Object severity -eq 'warning').Count
160
+ $timedOut = $res.timed_out
161
+
162
+ # ------------------------------------------------------- 4. artifacts
163
+ $manifest = Get-FwBackendArtifacts -Backend $backend -ArtifactDir $ArtifactDir -Src $src -BuildDir $BuildDir
164
+ $manifest.configuration = $Configuration
165
+ if ($projectFile) { $manifest.project_file = $projectFile } else { $manifest.project_file = $null }
166
+ $manifestJson = Join-Path $ArtifactDir 'artifacts.json'
167
+ $manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestJson -Encoding utf8
168
+
169
+ $artifact = if ($manifest.primary) { $manifest.primary.native_path } else { $null }
170
+ $artifactHash = $null
171
+ if ($artifact) {
172
+ $artifactHash = (Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash.ToLowerInvariant()
173
+ }
174
+
175
+ $git = Get-FwGitInfo -RepoRoot $repoRoot
176
+
177
+ # ------------------------------------------------------- 5. result
178
+ $base = [ordered]@{
179
+ schema = 'firmware-build-result/v1'
180
+ configuration = $Configuration
181
+ backend = $backend
182
+ artifact = $artifact
183
+ artifact_sha256 = $artifactHash
184
+ warnings = $warnings
185
+ errors = $errors
186
+ duration_ms = $durationMs
187
+ log = $logCanonical
188
+ artifacts = $manifest
189
+ git = $git
190
+ generated_at = Get-FwTimestamp
191
+ }
192
+
193
+ if ($timedOut) {
194
+ Exit-WithError -Class 'BUILD_ERROR' -Message "Build exceeded timeout (${TimeoutMs} ms) and was killed." -Detail $logFile
195
+ }
196
+ if ($res.exit_code -ne 0) {
197
+ $base.ok = $false
198
+ $base.artifact = $null
199
+ $base.diagnostics = @($diags | Select-Object file, line, col, severity, message)
200
+ if ($Json) { Write-FwJson ([pscustomobject]$base) -Compact } else { Write-FwJson ([pscustomobject]$base) }
201
+ exit 1
202
+ }
203
+ if (-not $artifact) {
204
+ Exit-WithError -Class 'ARTIFACT_NOT_FOUND' -Message "Build reported success but no $backend artifact was produced." -Detail $manifestJson
205
+ }
206
+ $base.ok = $true
207
+ if ($Json) { Write-FwJson ([pscustomobject]$base) -Compact } else { Write-FwJson ([pscustomobject]$base) }
208
+ exit 0