oscura 0.5.0__py3-none-any.whl → 0.6.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 (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
oscura/reporting/plots.py CHANGED
@@ -101,93 +101,114 @@ class PlotGenerator:
101
101
  ... output_manager
102
102
  ... )
103
103
  """
104
- from oscura.reporting.config import AnalysisDomain
105
104
 
106
- # Get plot format and DPI from config
105
+ plot_format, plot_dpi = self._get_plot_settings()
106
+ saved_paths: list[Path] = []
107
+
108
+ saved_paths.extend(
109
+ self._generate_registered_plots(results, domain, output_manager, plot_format, plot_dpi)
110
+ )
111
+
112
+ saved_paths.extend(
113
+ self._generate_domain_plots(domain, results, output_manager, plot_format, plot_dpi)
114
+ )
115
+
116
+ return saved_paths
117
+
118
+ def _get_plot_settings(self) -> tuple[str, int]:
119
+ """Get plot format and DPI from config."""
107
120
  plot_format = self.config.plot_format if self.config else "png"
108
121
  plot_dpi = self.config.plot_dpi if self.config else 150
122
+ return plot_format, plot_dpi
109
123
 
124
+ def _generate_registered_plots(
125
+ self,
126
+ results: dict[str, Any],
127
+ domain: AnalysisDomain,
128
+ output_manager: OutputManager,
129
+ plot_format: str,
130
+ plot_dpi: int,
131
+ ) -> list[Path]:
132
+ """Generate plots using registered plot functions."""
110
133
  saved_paths: list[Path] = []
111
134
 
112
135
  for analysis_name, result_data in results.items():
113
- # Skip non-dict results
114
136
  if not isinstance(result_data, dict):
115
137
  continue
116
138
 
117
- # Check if we have a registered plot function
118
139
  key = (domain, analysis_name)
119
140
  if key in PLOT_REGISTRY:
120
- plot_func = PLOT_REGISTRY[key]
121
- try:
122
- fig = plot_func(result_data)
123
- if fig is not None:
124
- path = output_manager.save_plot(
125
- domain,
126
- analysis_name,
127
- fig,
128
- format=plot_format,
129
- dpi=plot_dpi,
130
- )
131
- saved_paths.append(path)
132
- plt.close(fig) # Prevent memory leaks
133
- except Exception as e:
134
- # Log error but continue with other plots
135
- logger.warning("Failed to generate %s plot: %s", analysis_name, e)
136
- continue
137
-
138
- # Also try generic domain-level plots
139
- try:
140
- if domain == AnalysisDomain.SPECTRAL:
141
- saved_paths.extend(
142
- self._generate_spectral_plots(
143
- results, domain, output_manager, plot_format, plot_dpi
144
- )
145
- )
146
- elif domain == AnalysisDomain.WAVEFORM:
147
- saved_paths.extend(
148
- self._generate_waveform_plots(
149
- results, domain, output_manager, plot_format, plot_dpi
150
- )
151
- )
152
- elif domain == AnalysisDomain.DIGITAL:
153
- saved_paths.extend(
154
- self._generate_digital_plots(
155
- results, domain, output_manager, plot_format, plot_dpi
156
- )
157
- )
158
- elif domain == AnalysisDomain.STATISTICS:
159
- saved_paths.extend(
160
- self._generate_statistics_plots(
161
- results, domain, output_manager, plot_format, plot_dpi
162
- )
163
- )
164
- elif domain == AnalysisDomain.JITTER:
165
- saved_paths.extend(
166
- self._generate_jitter_plots(
167
- results, domain, output_manager, plot_format, plot_dpi
168
- )
169
- )
170
- elif domain == AnalysisDomain.EYE:
171
- saved_paths.extend(
172
- self._generate_eye_plots(results, domain, output_manager, plot_format, plot_dpi)
173
- )
174
- elif domain == AnalysisDomain.PATTERNS:
175
- saved_paths.extend(
176
- self._generate_pattern_plots(
177
- results, domain, output_manager, plot_format, plot_dpi
178
- )
179
- )
180
- elif domain == AnalysisDomain.POWER:
181
- saved_paths.extend(
182
- self._generate_power_plots(
183
- results, domain, output_manager, plot_format, plot_dpi
184
- )
141
+ path = self._try_generate_plot(
142
+ PLOT_REGISTRY[key],
143
+ result_data,
144
+ domain,
145
+ analysis_name,
146
+ output_manager,
147
+ plot_format,
148
+ plot_dpi,
185
149
  )
186
- except Exception as e:
187
- logger.warning("Error in domain-level plot generation for %s: %s", domain.value, e)
150
+ if path is not None:
151
+ saved_paths.append(path)
188
152
 
189
153
  return saved_paths
190
154
 
155
+ def _try_generate_plot(
156
+ self,
157
+ plot_func: Callable[[dict[str, Any]], Figure | None],
158
+ result_data: dict[str, Any],
159
+ domain: AnalysisDomain,
160
+ analysis_name: str,
161
+ output_manager: OutputManager,
162
+ plot_format: str,
163
+ plot_dpi: int,
164
+ ) -> Path | None:
165
+ """Attempt to generate and save a single plot."""
166
+ try:
167
+ fig = plot_func(result_data)
168
+ if fig is None:
169
+ return None
170
+
171
+ path = output_manager.save_plot(
172
+ domain, analysis_name, fig, format=plot_format, dpi=plot_dpi
173
+ )
174
+ plt.close(fig)
175
+ return path
176
+ except Exception as e:
177
+ logger.warning("Failed to generate %s plot: %s", analysis_name, e)
178
+ return None
179
+
180
+ def _generate_domain_plots(
181
+ self,
182
+ domain: AnalysisDomain,
183
+ results: dict[str, Any],
184
+ output_manager: OutputManager,
185
+ plot_format: str,
186
+ plot_dpi: int,
187
+ ) -> list[Path]:
188
+ """Generate domain-specific plots based on analysis domain."""
189
+ from oscura.reporting.config import AnalysisDomain
190
+
191
+ domain_generators = {
192
+ AnalysisDomain.SPECTRAL: self._generate_spectral_plots,
193
+ AnalysisDomain.WAVEFORM: self._generate_waveform_plots,
194
+ AnalysisDomain.DIGITAL: self._generate_digital_plots,
195
+ AnalysisDomain.STATISTICS: self._generate_statistics_plots,
196
+ AnalysisDomain.JITTER: self._generate_jitter_plots,
197
+ AnalysisDomain.EYE: self._generate_eye_plots,
198
+ AnalysisDomain.PATTERNS: self._generate_pattern_plots,
199
+ AnalysisDomain.POWER: self._generate_power_plots,
200
+ }
201
+
202
+ generator = domain_generators.get(domain)
203
+ if generator is None:
204
+ return []
205
+
206
+ try:
207
+ return generator(results, domain, output_manager, plot_format, plot_dpi)
208
+ except Exception as e:
209
+ logger.warning("Error in domain-level plot generation for %s: %s", domain.value, e)
210
+ return []
211
+
191
212
  def _generate_spectral_plots(
192
213
  self,
193
214
  results: dict[str, Any],
@@ -199,52 +220,110 @@ class PlotGenerator:
199
220
  """Generate spectral analysis plots (FFT, PSD, spectrogram)."""
200
221
  paths: list[Path] = []
201
222
 
202
- # FFT plot
203
- if "fft" in results and isinstance(results["fft"], dict):
204
- fft_data = results["fft"]
205
- if "frequencies" in fft_data and "magnitude_db" in fft_data:
206
- try:
207
- fig = self._plot_spectrum(fft_data, title="FFT Magnitude Spectrum")
208
- path = output_manager.save_plot(
209
- domain, "fft_spectrum", fig, format=plot_format, dpi=plot_dpi
210
- )
211
- paths.append(path)
212
- plt.close(fig)
213
- except Exception:
214
- pass
223
+ # Generate FFT plot
224
+ fft_path = self._try_generate_fft_plot(
225
+ results, domain, output_manager, plot_format, plot_dpi
226
+ )
227
+ if fft_path:
228
+ paths.append(fft_path)
215
229
 
216
- # PSD plot
217
- if "psd" in results and isinstance(results["psd"], dict):
218
- psd_data = results["psd"]
219
- if "frequencies" in psd_data and "psd" in psd_data:
220
- try:
221
- fig = self._plot_spectrum(
222
- psd_data, title="Power Spectral Density", ylabel="PSD (dB/Hz)"
223
- )
224
- path = output_manager.save_plot(
225
- domain, "psd_spectrum", fig, format=plot_format, dpi=plot_dpi
226
- )
227
- paths.append(path)
228
- plt.close(fig)
229
- except Exception:
230
- pass
230
+ # Generate PSD plot
231
+ psd_path = self._try_generate_psd_plot(
232
+ results, domain, output_manager, plot_format, plot_dpi
233
+ )
234
+ if psd_path:
235
+ paths.append(psd_path)
231
236
 
232
- # Spectrogram
233
- if "spectrogram" in results and isinstance(results["spectrogram"], dict):
234
- spec_data = results["spectrogram"]
235
- if "times" in spec_data and "frequencies" in spec_data and "Sxx_db" in spec_data:
236
- try:
237
- fig = self._plot_spectrogram(spec_data)
238
- path = output_manager.save_plot(
239
- domain, "spectrogram", fig, format=plot_format, dpi=plot_dpi
240
- )
241
- paths.append(path)
242
- plt.close(fig)
243
- except Exception:
244
- pass
237
+ # Generate spectrogram
238
+ spec_path = self._try_generate_spectrogram(
239
+ results, domain, output_manager, plot_format, plot_dpi
240
+ )
241
+ if spec_path:
242
+ paths.append(spec_path)
245
243
 
246
244
  return paths
247
245
 
246
+ def _try_generate_fft_plot(
247
+ self,
248
+ results: dict[str, Any],
249
+ domain: AnalysisDomain,
250
+ output_manager: OutputManager,
251
+ plot_format: str,
252
+ plot_dpi: int,
253
+ ) -> Path | None:
254
+ """Try to generate FFT spectrum plot."""
255
+ if "fft" not in results or not isinstance(results["fft"], dict):
256
+ return None
257
+
258
+ fft_data = results["fft"]
259
+ if "frequencies" not in fft_data or "magnitude_db" not in fft_data:
260
+ return None
261
+
262
+ try:
263
+ fig = self._plot_spectrum(fft_data, title="FFT Magnitude Spectrum")
264
+ path = output_manager.save_plot(
265
+ domain, "fft_spectrum", fig, format=plot_format, dpi=plot_dpi
266
+ )
267
+ plt.close(fig)
268
+ return path
269
+ except Exception:
270
+ return None
271
+
272
+ def _try_generate_psd_plot(
273
+ self,
274
+ results: dict[str, Any],
275
+ domain: AnalysisDomain,
276
+ output_manager: OutputManager,
277
+ plot_format: str,
278
+ plot_dpi: int,
279
+ ) -> Path | None:
280
+ """Try to generate PSD plot."""
281
+ if "psd" not in results or not isinstance(results["psd"], dict):
282
+ return None
283
+
284
+ psd_data = results["psd"]
285
+ if "frequencies" not in psd_data or "psd" not in psd_data:
286
+ return None
287
+
288
+ try:
289
+ fig = self._plot_spectrum(
290
+ psd_data, title="Power Spectral Density", ylabel="PSD (dB/Hz)"
291
+ )
292
+ path = output_manager.save_plot(
293
+ domain, "psd_spectrum", fig, format=plot_format, dpi=plot_dpi
294
+ )
295
+ plt.close(fig)
296
+ return path
297
+ except Exception:
298
+ return None
299
+
300
+ def _try_generate_spectrogram(
301
+ self,
302
+ results: dict[str, Any],
303
+ domain: AnalysisDomain,
304
+ output_manager: OutputManager,
305
+ plot_format: str,
306
+ plot_dpi: int,
307
+ ) -> Path | None:
308
+ """Try to generate spectrogram plot."""
309
+ if "spectrogram" not in results or not isinstance(results["spectrogram"], dict):
310
+ return None
311
+
312
+ spec_data = results["spectrogram"]
313
+ required_keys = ["times", "frequencies", "Sxx_db"]
314
+ if not all(key in spec_data for key in required_keys):
315
+ return None
316
+
317
+ try:
318
+ fig = self._plot_spectrogram(spec_data)
319
+ path = output_manager.save_plot(
320
+ domain, "spectrogram", fig, format=plot_format, dpi=plot_dpi
321
+ )
322
+ plt.close(fig)
323
+ return path
324
+ except Exception:
325
+ return None
326
+
248
327
  def _generate_waveform_plots(
249
328
  self,
250
329
  results: dict[str, Any],
@@ -77,62 +77,103 @@ def create_executive_summary_section(
77
77
  References:
78
78
  REPORT-004, REPORT-006
79
79
  """
80
- content_parts = []
80
+ content_parts: list[str] = []
81
81
 
82
- # Overall status
83
- if "pass_count" in results and "total_count" in results:
84
- pass_count = results["pass_count"]
85
- total = results["total_count"]
86
-
87
- if pass_count == total:
88
- content_parts.append(f"All {total} tests passed with satisfactory margins.")
89
- else:
90
- fail_count = total - pass_count
91
- content_parts.append(
92
- f"{fail_count} of {total} tests failed ({fail_count / total * 100:.0f}% failure rate)."
93
- )
94
-
95
- # Key findings
96
- if key_findings:
97
- content_parts.append("\n**Key Findings:**")
98
- for finding in key_findings[:5]: # Top 5
99
- content_parts.append(f"- {finding}")
100
-
101
- # Margin analysis
102
- if "min_margin" in results:
103
- margin = results["min_margin"]
104
- content_parts.append("\n**Margin Analysis:**")
105
- if margin < 0:
106
- content_parts.append(f"Critical: Minimum margin is {margin:.1f}% (violation).")
107
- elif margin < 10:
108
- content_parts.append(
109
- f"Warning: Minimum margin is {margin:.1f}% (below recommended 10%)."
110
- )
111
- elif margin < 20:
112
- content_parts.append(f"Acceptable: Minimum margin is {margin:.1f}% (below target 20%).")
113
- else:
114
- content_parts.append(f"Good: Minimum margin is {margin:.1f}% (exceeds target 20%).")
115
-
116
- # Recommendations (for detailed summary)
117
- if length == "detailed" and "violations" in results:
118
- violations = results["violations"]
119
- if violations:
120
- content_parts.append("\n**Recommendations:**")
121
- for violation in violations[:3]:
122
- content_parts.append(
123
- f"- Address {violation.get('parameter', 'measurement')} violation"
124
- )
125
-
126
- content = "\n".join(content_parts)
82
+ _add_test_status(content_parts, results)
83
+ _add_key_findings(content_parts, key_findings)
84
+ _add_margin_analysis(content_parts, results)
85
+ _add_recommendations(content_parts, results, length)
127
86
 
128
87
  return Section(
129
88
  title="Executive Summary",
130
- content=content,
89
+ content="\n".join(content_parts),
131
90
  level=1,
132
91
  visible=True,
133
92
  )
134
93
 
135
94
 
95
+ def _add_test_status(content_parts: list[str], results: dict[str, Any]) -> None:
96
+ """Add test status to summary.
97
+
98
+ Args:
99
+ content_parts: List to append to.
100
+ results: Results dictionary.
101
+ """
102
+ if "pass_count" not in results or "total_count" not in results:
103
+ return
104
+
105
+ pass_count = results["pass_count"]
106
+ total = results["total_count"]
107
+
108
+ if pass_count == total:
109
+ content_parts.append(f"All {total} tests passed with satisfactory margins.")
110
+ else:
111
+ fail_count = total - pass_count
112
+ fail_rate = fail_count / total * 100
113
+ content_parts.append(
114
+ f"{fail_count} of {total} tests failed ({fail_rate:.0f}% failure rate)."
115
+ )
116
+
117
+
118
+ def _add_key_findings(content_parts: list[str], key_findings: list[str] | None) -> None:
119
+ """Add key findings to summary.
120
+
121
+ Args:
122
+ content_parts: List to append to.
123
+ key_findings: Findings to add.
124
+ """
125
+ if not key_findings:
126
+ return
127
+
128
+ content_parts.append("\n**Key Findings:**")
129
+ for finding in key_findings[:5]:
130
+ content_parts.append(f"- {finding}")
131
+
132
+
133
+ def _add_margin_analysis(content_parts: list[str], results: dict[str, Any]) -> None:
134
+ """Add margin analysis to summary.
135
+
136
+ Args:
137
+ content_parts: List to append to.
138
+ results: Results dictionary.
139
+ """
140
+ if "min_margin" not in results:
141
+ return
142
+
143
+ margin = results["min_margin"]
144
+ content_parts.append("\n**Margin Analysis:**")
145
+
146
+ if margin < 0:
147
+ content_parts.append(f"Critical: Minimum margin is {margin:.1f}% (violation).")
148
+ elif margin < 10:
149
+ content_parts.append(f"Warning: Minimum margin is {margin:.1f}% (below recommended 10%).")
150
+ elif margin < 20:
151
+ content_parts.append(f"Acceptable: Minimum margin is {margin:.1f}% (below target 20%).")
152
+ else:
153
+ content_parts.append(f"Good: Minimum margin is {margin:.1f}% (exceeds target 20%).")
154
+
155
+
156
+ def _add_recommendations(content_parts: list[str], results: dict[str, Any], length: str) -> None:
157
+ """Add recommendations to summary if detailed.
158
+
159
+ Args:
160
+ content_parts: List to append to.
161
+ results: Results dictionary.
162
+ length: Summary length mode.
163
+ """
164
+ if length != "detailed" or "violations" not in results:
165
+ return
166
+
167
+ violations = results["violations"]
168
+ if not violations:
169
+ return
170
+
171
+ content_parts.append("\n**Recommendations:**")
172
+ for violation in violations[:3]:
173
+ param = violation.get("parameter", "measurement")
174
+ content_parts.append(f"- Address {param} violation")
175
+
176
+
136
177
  def create_measurement_results_section(
137
178
  measurements: dict[str, Any],
138
179
  *,