oscura 0.5.1__py3-none-any.whl → 0.7.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 (497) 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/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -61,6 +61,12 @@ from oscura.reporting.core_formats import ( # core_formats/ directory
61
61
  from oscura.reporting.engine import (
62
62
  AnalysisEngine,
63
63
  )
64
+ from oscura.reporting.enhanced_reports import (
65
+ EnhancedReportGenerator,
66
+ )
67
+ from oscura.reporting.enhanced_reports import (
68
+ ReportConfig as EnhancedReportConfig,
69
+ )
64
70
  from oscura.reporting.export import (
65
71
  batch_export_formats,
66
72
  export_multiple_reports,
@@ -158,6 +164,9 @@ __all__ = [
158
164
  "ColorScheme",
159
165
  "DataOutputConfig",
160
166
  "DomainConfig",
167
+ # Enhanced Reports (Feature 6)
168
+ "EnhancedReportConfig",
169
+ "EnhancedReportGenerator",
161
170
  "ExecutiveSummary",
162
171
  # Summary Generation
163
172
  "Finding",
@@ -49,8 +49,7 @@ def analyze(
49
49
  Args:
50
50
  input_path: Path to input data file (any supported format).
51
51
  data: In-memory data (Trace, bytes, list of packets).
52
- output_dir: Base directory for output. Default: input file's directory
53
- or current directory for in-memory data.
52
+ output_dir: Base directory for output.
54
53
  config: Analysis configuration. Default: analyze all applicable domains.
55
54
  progress_callback: Called with progress updates during analysis.
56
55
 
@@ -62,122 +61,288 @@ def analyze(
62
61
  ValueError: If neither or both input_path and data are provided.
63
62
 
64
63
  Examples:
65
- # From file
66
- result = analyze("capture.wfm")
67
- print(result.output_dir) # 20260101_120000_capture_analysis/
64
+ >>> result = analyze("capture.wfm")
65
+ >>> result = analyze(data=my_trace, output_dir="/reports")
66
+ >>> config = AnalysisConfig(domains=[AnalysisDomain.SPECTRAL])
67
+ >>> result = analyze("capture.wfm", config=config)
68
+ """
69
+ config = config or AnalysisConfig()
70
+ _validate_inputs(input_path, data)
71
+ start_time = time.time()
68
72
 
69
- # From in-memory data
70
- result = analyze(data=my_waveform_trace, output_dir="/reports")
73
+ # Setup and load data
74
+ output_manager, input_name, input_type, loaded_data, resolved_path = _setup_analysis(
75
+ input_path, data, output_dir, progress_callback, start_time
76
+ )
71
77
 
72
- # With configuration
73
- config = AnalysisConfig(domains=[AnalysisDomain.SPECTRAL])
74
- result = analyze("capture.wfm", config=config)
78
+ # Run analysis pipeline
79
+ engine_result, plot_paths, saved_paths = _run_analysis_pipeline(
80
+ config,
81
+ resolved_path,
82
+ loaded_data,
83
+ input_name,
84
+ input_type,
85
+ output_manager,
86
+ progress_callback,
87
+ start_time,
88
+ )
75
89
 
76
- # With progress callback
77
- def on_progress(info):
78
- print(f"{info.domain}: {info.percent}%")
79
- result = analyze("capture.wfm", progress_callback=on_progress)
80
- """
81
- # Validate inputs
90
+ # Build and finalize result
91
+ result = _finalize_result(
92
+ output_manager,
93
+ resolved_path,
94
+ input_type,
95
+ engine_result,
96
+ saved_paths,
97
+ plot_paths,
98
+ config,
99
+ progress_callback,
100
+ start_time,
101
+ )
102
+
103
+ logger.info(f"Analysis complete. Output: {result.output_dir}")
104
+ return result
105
+
106
+
107
+ def _run_analysis_pipeline(
108
+ config: AnalysisConfig,
109
+ resolved_path: Path | None,
110
+ loaded_data: Any,
111
+ input_name: str,
112
+ input_type: InputType,
113
+ output_manager: OutputManager,
114
+ progress_callback: Callable[[ProgressInfo], None] | None,
115
+ start_time: float,
116
+ ) -> tuple[dict[str, Any], list[Path], dict[str, Any]]:
117
+ """Run complete analysis pipeline."""
118
+ engine_result = _run_analysis_engine(
119
+ config, resolved_path, loaded_data, input_name, input_type, progress_callback
120
+ )
121
+ plot_paths = _generate_plots(
122
+ config, engine_result, output_manager, progress_callback, start_time
123
+ )
124
+ saved_paths = _save_all_outputs(
125
+ output_manager,
126
+ input_name,
127
+ input_type,
128
+ resolved_path,
129
+ datetime.now(),
130
+ engine_result,
131
+ config,
132
+ start_time,
133
+ )
134
+ return engine_result, plot_paths, saved_paths
135
+
136
+
137
+ def _finalize_result(
138
+ output_manager: OutputManager,
139
+ resolved_path: Path | None,
140
+ input_type: InputType,
141
+ engine_result: dict[str, Any],
142
+ saved_paths: dict[str, Any],
143
+ plot_paths: list[Path],
144
+ config: AnalysisConfig,
145
+ progress_callback: Callable[[ProgressInfo], None] | None,
146
+ start_time: float,
147
+ ) -> AnalysisResult:
148
+ """Build and finalize analysis result."""
149
+ partial_result = _build_result(
150
+ output_manager,
151
+ resolved_path,
152
+ input_type,
153
+ engine_result,
154
+ saved_paths["summary_json"],
155
+ saved_paths["summary_yaml"],
156
+ saved_paths["metadata_json"],
157
+ saved_paths["config_yaml"],
158
+ saved_paths["domain_dirs"],
159
+ plot_paths,
160
+ saved_paths["error_log"],
161
+ start_time,
162
+ )
163
+ index_paths = _generate_index(
164
+ output_manager, partial_result, config, progress_callback, start_time
165
+ )
166
+ result = _build_final_result(partial_result, index_paths, time.time() - start_time)
167
+
168
+ _report_progress(
169
+ progress_callback,
170
+ "complete",
171
+ None,
172
+ None,
173
+ 100.0,
174
+ f"Analysis complete: {result.successful_analyses}/{result.total_analyses} successful",
175
+ time.time() - start_time,
176
+ )
177
+ return result
178
+
179
+
180
+ def _setup_analysis(
181
+ input_path: str | Path | None,
182
+ data: Trace | bytes | list[Any] | None,
183
+ output_dir: str | Path | None,
184
+ progress_callback: Callable[[ProgressInfo], None] | None,
185
+ start_time: float,
186
+ ) -> tuple[OutputManager, str, InputType, Any, Path | None]:
187
+ """Setup analysis environment and load data."""
188
+ input_name, input_type, loaded_data, resolved_path = _prepare_input(input_path, data)
189
+ base_dir = _determine_output_dir(resolved_path, output_dir)
190
+ output_manager = OutputManager(base_dir, input_name, datetime.now())
191
+ output_manager.create()
192
+ _report_progress(
193
+ progress_callback,
194
+ "initializing",
195
+ None,
196
+ None,
197
+ 0.0,
198
+ "Initializing analysis",
199
+ time.time() - start_time,
200
+ )
201
+ return output_manager, input_name, input_type, loaded_data, resolved_path
202
+
203
+
204
+ def _save_all_outputs(
205
+ output_manager: OutputManager,
206
+ input_name: str,
207
+ input_type: InputType,
208
+ resolved_path: Path | None,
209
+ timestamp: datetime,
210
+ engine_result: Any,
211
+ config: AnalysisConfig,
212
+ start_time: float,
213
+ ) -> dict[str, Any]:
214
+ """Save all analysis outputs and return paths."""
215
+ summary_json, summary_yaml = _save_summary(
216
+ output_manager,
217
+ input_name,
218
+ input_type,
219
+ resolved_path,
220
+ timestamp,
221
+ engine_result,
222
+ config,
223
+ start_time,
224
+ )
225
+ return {
226
+ "summary_json": summary_json,
227
+ "summary_yaml": summary_yaml,
228
+ "metadata_json": _save_metadata(
229
+ output_manager, resolved_path, input_type, timestamp, engine_result, start_time
230
+ ),
231
+ "config_yaml": _save_config(output_manager, config, input_type),
232
+ "domain_dirs": _save_domain_results(output_manager, engine_result),
233
+ "error_log": _save_errors(output_manager, engine_result),
234
+ }
235
+
236
+
237
+ def _validate_inputs(input_path: str | Path | None, data: Trace | bytes | list[Any] | None) -> None:
238
+ """Validate that exactly one input source is provided."""
82
239
  if input_path is None and data is None:
83
240
  raise ValueError("Either input_path or data must be provided")
84
241
  if input_path is not None and data is not None:
85
242
  raise ValueError("Provide input_path OR data, not both")
86
243
 
87
- # Use default config if not provided
88
- if config is None:
89
- config = AnalysisConfig()
90
244
 
91
- # Track timing
92
- start_time = time.time()
245
+ def _prepare_input(
246
+ input_path: str | Path | None, data: Trace | bytes | list[Any] | None
247
+ ) -> tuple[str, InputType, Any, Path | None]:
248
+ """Prepare input data and determine type.
93
249
 
94
- # Determine input name and type
250
+ Returns:
251
+ Tuple of (input_name, input_type, loaded_data, resolved_path)
252
+ """
95
253
  if input_path is not None:
96
- input_path = Path(input_path)
97
- if not input_path.exists():
98
- raise FileNotFoundError(f"Input file not found: {input_path}")
99
- input_name = input_path.stem
100
- input_type = _detect_input_type_from_file(input_path)
101
- loaded_data = _load_input_file(input_path, input_type)
254
+ path = Path(input_path)
255
+ if not path.exists():
256
+ raise FileNotFoundError(f"Input file not found: {path}")
257
+ input_name = path.stem
258
+ input_type = _detect_input_type_from_file(path)
259
+ loaded_data = _load_input_file(path, input_type)
260
+ return input_name, input_type, loaded_data, path
102
261
  else:
103
262
  input_name = "memory_data"
104
263
  input_type = _detect_input_type_from_data(data)
105
264
  loaded_data = data
265
+ return input_name, input_type, loaded_data, None
106
266
 
107
- # Determine output directory
108
- if output_dir is None:
109
- if input_path is not None:
110
- base_dir = input_path.parent
111
- else:
112
- base_dir = Path.cwd()
113
- else:
114
- base_dir = Path(output_dir)
115
-
116
- # Create output manager with timestamp
117
- timestamp = datetime.now()
118
- output_manager = OutputManager(base_dir, input_name, timestamp)
119
- output_manager.create()
120
-
121
- # Report progress: starting
122
- _report_progress(
123
- progress_callback,
124
- phase="initializing",
125
- domain=None,
126
- function=None,
127
- percent=0.0,
128
- message="Initializing analysis",
129
- elapsed=time.time() - start_time,
130
- )
131
267
 
132
- # Determine applicable domains
268
+ def _determine_output_dir(input_path: Path | None, output_dir: str | Path | None) -> Path:
269
+ """Determine output directory based on input path or override."""
270
+ if output_dir is not None:
271
+ return Path(output_dir)
272
+ if input_path is not None:
273
+ return input_path.parent
274
+ return Path.cwd()
275
+
276
+
277
+ def _run_analysis_engine(
278
+ config: AnalysisConfig,
279
+ input_path: Path | None,
280
+ loaded_data: Any,
281
+ input_name: str,
282
+ input_type: InputType,
283
+ progress_callback: Callable[[ProgressInfo], None] | None,
284
+ ) -> dict[str, Any]:
285
+ """Run analysis engine on data."""
133
286
  applicable_domains = get_available_analyses(input_type)
134
287
  enabled_domains = [d for d in applicable_domains if config.is_domain_enabled(d)]
135
288
 
136
289
  logger.info(f"Running analysis on {input_name} ({input_type.value})")
137
290
  logger.info(f"Enabled domains: {[d.value for d in enabled_domains]}")
138
291
 
139
- # Execute analysis engine
140
292
  from oscura.reporting.engine import AnalysisEngine
141
293
 
142
294
  engine = AnalysisEngine(config)
143
- engine_result = engine.run(
295
+ return engine.run(
144
296
  input_path=input_path,
145
297
  data=loaded_data,
146
298
  progress_callback=progress_callback,
147
299
  )
148
300
 
149
- # Generate plots
150
- plot_paths: list[Path] = []
151
- if config.generate_plots:
152
- _report_progress(
153
- progress_callback,
154
- phase="plotting",
155
- domain=None,
156
- function=None,
157
- percent=70.0,
158
- message="Generating visualizations",
159
- elapsed=time.time() - start_time,
160
- )
161
301
 
162
- from oscura.reporting.plots import PlotGenerator
302
+ def _generate_plots(
303
+ config: AnalysisConfig,
304
+ engine_result: dict[str, Any],
305
+ output_manager: OutputManager,
306
+ progress_callback: Callable[[ProgressInfo], None] | None,
307
+ start_time: float,
308
+ ) -> list[Path]:
309
+ """Generate plots if configured."""
310
+ plot_paths: list[Path] = []
163
311
 
164
- plot_gen = PlotGenerator(config)
165
- for domain, results in engine_result["results"].items():
166
- domain_plots = plot_gen.generate_plots(domain, results, output_manager)
167
- plot_paths.extend(domain_plots)
312
+ if not config.generate_plots:
313
+ return plot_paths
168
314
 
169
- # Save data outputs
170
315
  _report_progress(
171
316
  progress_callback,
172
- phase="saving",
173
- domain=None,
174
- function=None,
175
- percent=85.0,
176
- message="Saving analysis results",
177
- elapsed=time.time() - start_time,
317
+ "plotting",
318
+ None,
319
+ None,
320
+ 70.0,
321
+ "Generating visualizations",
322
+ time.time() - start_time,
178
323
  )
179
324
 
180
- # Save summary data
325
+ from oscura.reporting.plots import PlotGenerator
326
+
327
+ plot_gen = PlotGenerator(config)
328
+ for domain, results in engine_result["results"].items():
329
+ domain_plots = plot_gen.generate_plots(domain, results, output_manager)
330
+ plot_paths.extend(domain_plots)
331
+
332
+ return plot_paths
333
+
334
+
335
+ def _save_summary(
336
+ output_manager: OutputManager,
337
+ input_name: str,
338
+ input_type: InputType,
339
+ input_path: Path | None,
340
+ timestamp: datetime,
341
+ engine_result: dict[str, Any],
342
+ config: AnalysisConfig,
343
+ start_time: float,
344
+ ) -> tuple[Path, Path | None]:
345
+ """Save summary data."""
181
346
  summary_data = {
182
347
  "input": {
183
348
  "name": input_name,
@@ -195,7 +360,18 @@ def analyze(
195
360
  if "yaml" in config.output_formats:
196
361
  summary_yaml = output_manager.save_yaml("summary", summary_data)
197
362
 
198
- # Save metadata
363
+ return summary_json, summary_yaml
364
+
365
+
366
+ def _save_metadata(
367
+ output_manager: OutputManager,
368
+ input_path: Path | None,
369
+ input_type: InputType,
370
+ timestamp: datetime,
371
+ engine_result: dict[str, Any],
372
+ start_time: float,
373
+ ) -> Path:
374
+ """Save metadata."""
199
375
  metadata = {
200
376
  "oscura_version": _get_version(),
201
377
  "analysis_version": "2.0",
@@ -208,9 +384,16 @@ def analyze(
208
384
  "failed": engine_result["stats"]["failed_analyses"],
209
385
  "skipped": engine_result["stats"].get("skipped_analyses", 0),
210
386
  }
211
- metadata_json = output_manager.save_json("metadata", metadata)
387
+ return output_manager.save_json("metadata", metadata)
388
+
389
+
390
+ def _save_config(
391
+ output_manager: OutputManager, config: AnalysisConfig, input_type: InputType
392
+ ) -> Path:
393
+ """Save configuration."""
394
+ applicable_domains = get_available_analyses(input_type)
395
+ enabled_domains = [d for d in applicable_domains if config.is_domain_enabled(d)]
212
396
 
213
- # Save configuration
214
397
  config_data = {
215
398
  "domains": [d.value for d in enabled_domains],
216
399
  "generate_plots": config.generate_plots,
@@ -219,34 +402,57 @@ def analyze(
219
402
  "output_formats": config.output_formats,
220
403
  "index_formats": config.index_formats,
221
404
  }
222
- config_yaml = output_manager.save_yaml("config", config_data)
405
+ return output_manager.save_yaml("config", config_data)
223
406
 
224
- # Save domain results
407
+
408
+ def _save_domain_results(
409
+ output_manager: OutputManager, engine_result: dict[str, Any]
410
+ ) -> dict[AnalysisDomain, Path]:
411
+ """Save domain-specific results."""
225
412
  domain_dirs: dict[AnalysisDomain, Path] = {}
226
413
  for domain, results in engine_result["results"].items():
227
414
  domain_dir = output_manager.create_domain_dir(domain)
228
415
  domain_dirs[domain] = domain_dir
229
416
  output_manager.save_json("results", results, subdir=domain.value)
417
+ return domain_dirs
418
+
230
419
 
231
- # Save errors if any
232
- error_log: Path | None = None
420
+ def _save_errors(output_manager: OutputManager, engine_result: dict[str, Any]) -> Path | None:
421
+ """Save error log if errors occurred."""
233
422
  errors: list[AnalysisError] = engine_result["errors"]
234
- if errors:
235
- error_list = [
236
- {
237
- "domain": e.domain.value,
238
- "function": e.function,
239
- "error_type": e.error_type,
240
- "error_message": e.error_message,
241
- "duration_ms": e.duration_ms,
242
- }
243
- for e in errors
244
- ]
245
- error_data = {"errors": error_list, "count": len(error_list)}
246
- error_log = output_manager.save_json("failed_analyses", error_data, subdir="errors")
247
-
248
- # Build AnalysisResult for index generation
249
- partial_result = AnalysisResult(
423
+ if not errors:
424
+ return None
425
+
426
+ error_list = [
427
+ {
428
+ "domain": e.domain.value,
429
+ "function": e.function,
430
+ "error_type": e.error_type,
431
+ "error_message": e.error_message,
432
+ "duration_ms": e.duration_ms,
433
+ }
434
+ for e in errors
435
+ ]
436
+ error_data = {"errors": error_list, "count": len(error_list)}
437
+ return output_manager.save_json("failed_analyses", error_data, subdir="errors")
438
+
439
+
440
+ def _build_result(
441
+ output_manager: OutputManager,
442
+ input_path: Path | None,
443
+ input_type: InputType,
444
+ engine_result: dict[str, Any],
445
+ summary_json: Path,
446
+ summary_yaml: Path | None,
447
+ metadata_json: Path,
448
+ config_yaml: Path,
449
+ domain_dirs: dict[AnalysisDomain, Path],
450
+ plot_paths: list[Path],
451
+ error_log: Path | None,
452
+ start_time: float,
453
+ ) -> AnalysisResult:
454
+ """Build partial AnalysisResult for index generation."""
455
+ return AnalysisResult(
250
456
  output_dir=output_manager.root,
251
457
  index_html=None,
252
458
  index_md=None,
@@ -266,63 +472,63 @@ def analyze(
266
472
  skipped_analyses=engine_result["stats"].get("skipped_analyses", 0),
267
473
  duration_seconds=time.time() - start_time,
268
474
  domain_summaries=engine_result["results"],
269
- errors=errors,
475
+ errors=engine_result["errors"],
270
476
  )
271
477
 
272
- # Generate index files
478
+
479
+ def _generate_index(
480
+ output_manager: OutputManager,
481
+ partial_result: AnalysisResult,
482
+ config: AnalysisConfig,
483
+ progress_callback: Callable[[ProgressInfo], None] | None,
484
+ start_time: float,
485
+ ) -> dict[str, Path]:
486
+ """Generate index files."""
273
487
  _report_progress(
274
488
  progress_callback,
275
- phase="indexing",
276
- domain=None,
277
- function=None,
278
- percent=95.0,
279
- message="Generating index files",
280
- elapsed=time.time() - start_time,
489
+ "indexing",
490
+ None,
491
+ None,
492
+ 95.0,
493
+ "Generating index files",
494
+ time.time() - start_time,
281
495
  )
282
496
 
283
497
  from oscura.reporting.index import IndexGenerator
284
498
 
285
499
  index_gen = IndexGenerator(output_manager)
286
- index_paths = index_gen.generate(partial_result, config.index_formats)
500
+ return index_gen.generate(partial_result, config.index_formats)
287
501
 
288
- # Complete result
289
- result = AnalysisResult(
290
- output_dir=output_manager.root,
502
+
503
+ def _build_final_result(
504
+ partial_result: AnalysisResult,
505
+ index_paths: dict[str, Path],
506
+ duration: float,
507
+ ) -> AnalysisResult:
508
+ """Build final AnalysisResult with index paths."""
509
+ return AnalysisResult(
510
+ output_dir=partial_result.output_dir,
291
511
  index_html=index_paths.get("html"),
292
512
  index_md=index_paths.get("md"),
293
513
  index_pdf=index_paths.get("pdf"),
294
- summary_json=summary_json,
295
- summary_yaml=summary_yaml,
296
- metadata_json=metadata_json,
297
- config_yaml=config_yaml,
298
- domain_dirs=domain_dirs,
299
- plot_paths=plot_paths,
300
- error_log=error_log,
301
- input_file=str(input_path) if input_path else None,
302
- input_type=input_type,
303
- total_analyses=engine_result["stats"]["total_analyses"],
304
- successful_analyses=engine_result["stats"]["successful_analyses"],
305
- failed_analyses=engine_result["stats"]["failed_analyses"],
306
- skipped_analyses=engine_result["stats"].get("skipped_analyses", 0),
307
- duration_seconds=time.time() - start_time,
308
- domain_summaries=engine_result["results"],
309
- errors=errors,
310
- )
311
-
312
- # Report completion
313
- _report_progress(
314
- progress_callback,
315
- phase="complete",
316
- domain=None,
317
- function=None,
318
- percent=100.0,
319
- message=f"Analysis complete: {result.successful_analyses}/{result.total_analyses} successful",
320
- elapsed=time.time() - start_time,
514
+ summary_json=partial_result.summary_json,
515
+ summary_yaml=partial_result.summary_yaml,
516
+ metadata_json=partial_result.metadata_json,
517
+ config_yaml=partial_result.config_yaml,
518
+ domain_dirs=partial_result.domain_dirs,
519
+ plot_paths=partial_result.plot_paths,
520
+ error_log=partial_result.error_log,
521
+ input_file=partial_result.input_file,
522
+ input_type=partial_result.input_type,
523
+ total_analyses=partial_result.total_analyses,
524
+ successful_analyses=partial_result.successful_analyses,
525
+ failed_analyses=partial_result.failed_analyses,
526
+ skipped_analyses=partial_result.skipped_analyses,
527
+ duration_seconds=duration,
528
+ domain_summaries=partial_result.domain_summaries,
529
+ errors=partial_result.errors,
321
530
  )
322
531
 
323
- logger.info(f"Analysis complete. Output: {result.output_dir}")
324
- return result
325
-
326
532
 
327
533
  def _detect_input_type_from_file(path: Path) -> InputType:
328
534
  """Detect input type from file extension."""
@@ -398,7 +604,7 @@ def _load_input_file(path: Path, input_type: InputType) -> Any:
398
604
 
399
605
  return load_pcap(path)
400
606
  elif input_type == InputType.SPARAMS:
401
- from oscura.analyzers.signal_integrity.sparams import load_touchstone
607
+ from oscura.loaders.touchstone import load_touchstone
402
608
 
403
609
  return load_touchstone(path)
404
610
  else: