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/core/cache.py CHANGED
@@ -21,7 +21,10 @@ from __future__ import annotations
21
21
 
22
22
  import contextlib
23
23
  import hashlib
24
+ import hmac
25
+ import logging
24
26
  import pickle
27
+ import secrets
25
28
  import tempfile
26
29
  import threading
27
30
  import time
@@ -32,10 +35,14 @@ from typing import TYPE_CHECKING, Any, TypeVar
32
35
 
33
36
  import numpy as np
34
37
 
38
+ from oscura.core.exceptions import SecurityError
39
+
35
40
  if TYPE_CHECKING:
36
41
  from collections.abc import Callable
37
42
 
38
43
 
44
+ logger = logging.getLogger(__name__)
45
+
39
46
  T = TypeVar("T")
40
47
 
41
48
 
@@ -180,6 +187,9 @@ class OscuraCache:
180
187
  self._disk_spills = 0
181
188
  self._current_memory = 0
182
189
 
190
+ # Security: HMAC signing key for cache integrity (SEC-003 fix)
191
+ self._cache_key = self._load_or_create_cache_key()
192
+
183
193
  def __enter__(self) -> OscuraCache:
184
194
  """Enter context."""
185
195
  return self
@@ -227,12 +237,21 @@ class OscuraCache:
227
237
 
228
238
  # Load from disk if needed
229
239
  if not entry.in_memory:
230
- entry.value = self._load_from_disk(entry.disk_path) # type: ignore[arg-type]
231
- entry.in_memory = True
232
- self._current_memory += entry.size_bytes
233
-
234
- # Check if we need to spill to make room
235
- self._ensure_memory_limit()
240
+ try:
241
+ entry.value = self._load_from_disk(entry.disk_path) # type: ignore[arg-type]
242
+ entry.in_memory = True
243
+ self._current_memory += entry.size_bytes
244
+
245
+ # Check if we need to spill to make room
246
+ self._ensure_memory_limit()
247
+ except SecurityError:
248
+ # Re-raise security errors (tampered data)
249
+ raise
250
+ except (OSError, pickle.UnpicklingError):
251
+ # Remove corrupted entry from cache for non-security errors
252
+ del self._cache[key]
253
+ self._misses += 1
254
+ return None
236
255
 
237
256
  return entry.value
238
257
 
@@ -441,8 +460,7 @@ class OscuraCache:
441
460
  self._evictions += 1
442
461
 
443
462
  def _spill_to_disk(self, key: str, value: Any) -> Path:
444
- """Write value to disk.
445
-
463
+ """Write value to disk with HMAC signature.
446
464
 
447
465
  Args:
448
466
  key: Cache key.
@@ -450,29 +468,88 @@ class OscuraCache:
450
468
 
451
469
  Returns:
452
470
  Path to disk file.
471
+
472
+ Security:
473
+ SEC-003 fix: Writes HMAC-SHA256 signature + pickled data.
474
+ Format: [32 bytes signature][pickled data]
475
+ Signature computed over pickled data using self._cache_key.
476
+
477
+ References:
478
+ MEM-031: Persistent Cache (Disk-Based)
453
479
  """
454
480
  disk_path = self.cache_dir / f"{key}.pkl"
481
+
482
+ # Serialize data
483
+ data = pickle.dumps(value, protocol=pickle.HIGHEST_PROTOCOL)
484
+
485
+ # Compute HMAC-SHA256 signature
486
+ signature = hmac.new(self._cache_key, data, hashlib.sha256).digest()
487
+
488
+ # Write signature + data
455
489
  with open(disk_path, "wb") as f:
456
- pickle.dump(value, f, protocol=pickle.HIGHEST_PROTOCOL)
490
+ f.write(signature) # First 32 bytes
491
+ f.write(data) # Rest is pickled data
492
+
457
493
  return disk_path
458
494
 
459
495
  def _load_from_disk(self, disk_path: Path) -> Any:
460
- """Load value from disk.
461
-
496
+ """Load value from disk with HMAC verification.
462
497
 
463
498
  Args:
464
499
  disk_path: Path to disk file.
465
500
 
466
501
  Returns:
467
502
  Loaded value.
503
+
504
+ Raises:
505
+ SecurityError: If HMAC verification fails (tampered cache file).
506
+
507
+ Security:
508
+ SEC-003 fix: Verifies HMAC-SHA256 signature before unpickling.
509
+ Prevents code execution from tampered cache files.
510
+ Uses constant-time comparison (hmac.compare_digest).
511
+
512
+ References:
513
+ MEM-031: Persistent Cache (Disk-Based)
468
514
  """
469
- with open(disk_path, "rb") as f:
470
- return pickle.load(f)
515
+ try:
516
+ with open(disk_path, "rb") as f:
517
+ signature = f.read(32) # SHA256 = 32 bytes
518
+ data = f.read()
519
+
520
+ # Check if file is too short (corrupted, not tampered)
521
+ if len(signature) < 32:
522
+ logger.warning(f"Cache file too short (corrupted): {disk_path.name}")
523
+ disk_path.unlink(missing_ok=True)
524
+ raise OSError(f"Cache file corrupted (too short): {disk_path.name}")
525
+
526
+ # Verify HMAC signature
527
+ expected_signature = hmac.new(self._cache_key, data, hashlib.sha256).digest()
528
+
529
+ if not hmac.compare_digest(signature, expected_signature):
530
+ logger.error(f"Cache integrity check failed for {disk_path.name}")
531
+ # Delete corrupted cache file
532
+ disk_path.unlink(missing_ok=True)
533
+ raise SecurityError(
534
+ f"Cache file integrity verification failed: {disk_path.name}. "
535
+ "File may have been tampered with and has been removed."
536
+ )
537
+
538
+ # Deserialize only after HMAC verification
539
+ return pickle.loads(data)
540
+
541
+ except SecurityError:
542
+ raise # Re-raise security errors
543
+ except Exception as e:
544
+ logger.warning(f"Failed to load cache file {disk_path.name}: {e}")
545
+ # Clean up corrupted file
546
+ disk_path.unlink(missing_ok=True)
547
+ raise
471
548
 
472
549
  def _estimate_size(self, value: Any) -> int:
473
550
  """Estimate size of value in bytes."""
474
551
  if isinstance(value, np.ndarray):
475
- return value.nbytes # type: ignore[no-any-return]
552
+ return value.nbytes
476
553
  elif isinstance(value, list | tuple):
477
554
  return sum(self._estimate_size(item) for item in value)
478
555
  elif isinstance(value, dict):
@@ -488,7 +565,7 @@ class OscuraCache:
488
565
  """Convert object to hashable bytes."""
489
566
  if isinstance(obj, np.ndarray):
490
567
  # Use array bytes for hashing
491
- return obj.tobytes() # type: ignore[no-any-return]
568
+ return obj.tobytes()
492
569
  elif isinstance(obj, str | bytes):
493
570
  return obj.encode() if isinstance(obj, str) else obj
494
571
  elif isinstance(obj, int | float | bool):
@@ -512,6 +589,40 @@ class OscuraCache:
512
589
  else:
513
590
  return int(memory_str)
514
591
 
592
+ def _load_or_create_cache_key(self) -> bytes:
593
+ """Load or create HMAC signing key for cache integrity.
594
+
595
+ Returns:
596
+ 256-bit signing key.
597
+
598
+ Security:
599
+ SEC-003 fix: Protects cached pickle files from tampering.
600
+ Key is persistent per cache directory and stored with 0o600 permissions.
601
+ Each cache directory has its own unique key.
602
+
603
+ References:
604
+ https://owasp.org/www-project-top-ten/
605
+ """
606
+ key_file = self.cache_dir / ".cache_key"
607
+
608
+ # Load existing key
609
+ if key_file.exists():
610
+ with open(key_file, "rb") as f:
611
+ return f.read()
612
+
613
+ # Create new 256-bit key
614
+ key = secrets.token_bytes(32)
615
+
616
+ # Save with restrictive permissions
617
+ with open(key_file, "wb") as f:
618
+ f.write(key)
619
+
620
+ # Set owner read/write only (0o600)
621
+ key_file.chmod(0o600)
622
+
623
+ logger.info(f"Created new cache signing key: {key_file}")
624
+ return key
625
+
515
626
 
516
627
  # Global cache instance
517
628
  _global_cache: OscuraCache | None = None
@@ -260,7 +260,7 @@ class CancellationManager:
260
260
  except KeyboardInterrupt:
261
261
  self.cancel("Interrupted by user (Ctrl+C)")
262
262
  self._cleanup()
263
- raise CancelledException( # noqa: B904
263
+ raise CancelledException(
264
264
  f"{name} interrupted by user",
265
265
  partial_results=self._partial_results,
266
266
  elapsed_time=time.time() - self._start_time,
@@ -10,23 +10,23 @@ This package provides:
10
10
 
11
11
 
12
12
  Example:
13
- >>> from oscura.config import load_config, validate_config
13
+ >>> from oscura.core.config import load_config, validate_config
14
14
  >>> config = load_config("pipeline.yaml")
15
15
  >>> validate_config(config, schema="pipeline")
16
16
  """
17
17
 
18
- from oscura.config.defaults import (
18
+ from oscura.core.config.defaults import (
19
19
  DEFAULT_CONFIG,
20
20
  get_effective_config,
21
21
  inject_defaults,
22
22
  )
23
- from oscura.config.loader import (
23
+ from oscura.core.config.loader import (
24
24
  get_config_value,
25
25
  load_config,
26
26
  load_config_file,
27
27
  save_config,
28
28
  )
29
- from oscura.config.memory import (
29
+ from oscura.core.config.memory import (
30
30
  MemoryConfiguration,
31
31
  configure_from_environment,
32
32
  enable_auto_degrade,
@@ -36,7 +36,7 @@ from oscura.config.memory import (
36
36
  set_memory_reserve,
37
37
  set_memory_thresholds,
38
38
  )
39
- from oscura.config.migration import (
39
+ from oscura.core.config.migration import (
40
40
  Migration,
41
41
  MigrationFunction,
42
42
  SchemaMigration,
@@ -46,7 +46,7 @@ from oscura.config.migration import (
46
46
  migrate_config,
47
47
  register_migration,
48
48
  )
49
- from oscura.config.pipeline import (
49
+ from oscura.core.config.pipeline import (
50
50
  Pipeline,
51
51
  PipelineDefinition,
52
52
  PipelineExecutionError,
@@ -57,7 +57,7 @@ from oscura.config.pipeline import (
57
57
  load_pipeline,
58
58
  resolve_includes,
59
59
  )
60
- from oscura.config.preferences import (
60
+ from oscura.core.config.preferences import (
61
61
  DefaultsPreferences,
62
62
  EditorPreferences,
63
63
  ExportPreferences,
@@ -69,7 +69,7 @@ from oscura.config.preferences import (
69
69
  get_preferences_manager,
70
70
  save_preferences,
71
71
  )
72
- from oscura.config.protocol import (
72
+ from oscura.core.config.protocol import (
73
73
  ProtocolCapabilities,
74
74
  ProtocolDefinition,
75
75
  ProtocolRegistry,
@@ -78,7 +78,7 @@ from oscura.config.protocol import (
78
78
  load_protocol,
79
79
  resolve_inheritance,
80
80
  )
81
- from oscura.config.schema import (
81
+ from oscura.core.config.schema import (
82
82
  ConfigSchema,
83
83
  SchemaRegistry,
84
84
  ValidationError,
@@ -86,7 +86,13 @@ from oscura.config.schema import (
86
86
  register_schema,
87
87
  validate_against_schema,
88
88
  )
89
- from oscura.config.settings import (
89
+
90
+ # Alias for backward compatibility with oscura.core.config module
91
+ validate_config = validate_against_schema
92
+
93
+ # Legacy imports for backward compatibility
94
+ from oscura.core.config.legacy import SmartDefaults, _deep_merge
95
+ from oscura.core.config.settings import (
90
96
  AnalysisSettings,
91
97
  CLIDefaults,
92
98
  OutputSettings,
@@ -97,7 +103,7 @@ from oscura.config.settings import (
97
103
  save_settings,
98
104
  set_settings,
99
105
  )
100
- from oscura.config.thresholds import (
106
+ from oscura.core.config.thresholds import (
101
107
  LogicFamily,
102
108
  ThresholdProfile,
103
109
  ThresholdRegistry,
@@ -144,11 +150,13 @@ __all__ = [
144
150
  "SchemaMigration",
145
151
  "SchemaRegistry",
146
152
  "Settings",
153
+ "SmartDefaults",
147
154
  "ThresholdProfile",
148
155
  "ThresholdRegistry",
149
156
  "UserPreferences",
150
157
  "ValidationError",
151
158
  "VisualizationPreferences",
159
+ "_deep_merge",
152
160
  "configure_from_environment",
153
161
  "enable_auto_degrade",
154
162
  "get_config_value",
@@ -188,4 +196,5 @@ __all__ = [
188
196
  "set_memory_thresholds",
189
197
  "set_settings",
190
198
  "validate_against_schema",
199
+ "validate_config",
191
200
  ]
@@ -5,7 +5,7 @@ for injecting defaults into user configurations.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.config.defaults import inject_defaults
8
+ >>> from oscura.core.config.defaults import inject_defaults
9
9
  >>> config = {"name": "test"}
10
10
  >>> full_config = inject_defaults(config, "protocol")
11
11
  """
@@ -5,7 +5,7 @@ with support for schema validation, default injection, and path resolution.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.config.loader import load_config_file
8
+ >>> from oscura.core.config.loader import load_config_file
9
9
  >>> config = load_config_file("pipeline.yaml", schema="pipeline")
10
10
  """
11
11
 
@@ -76,7 +76,7 @@ def load_config_file(
76
76
  try:
77
77
  config = _load_json(path)
78
78
  except ConfigurationError:
79
- raise ConfigurationError( # noqa: B904
79
+ raise ConfigurationError(
80
80
  f"Unsupported configuration format: {ext}",
81
81
  config_key=str(path),
82
82
  fix_hint="Use .yaml, .yml, or .json extension",
@@ -84,13 +84,13 @@ def load_config_file(
84
84
 
85
85
  # Validate against schema if requested
86
86
  if validate and schema is not None:
87
- from oscura.config.schema import validate_against_schema
87
+ from oscura.core.config.schema import validate_against_schema
88
88
 
89
89
  validate_against_schema(config, schema)
90
90
 
91
91
  # Inject defaults if requested
92
92
  if inject_defaults and schema is not None:
93
- from oscura.config.defaults import inject_defaults as do_inject
93
+ from oscura.core.config.defaults import inject_defaults as do_inject
94
94
 
95
95
  config = do_inject(config, schema)
96
96
 
@@ -215,7 +215,7 @@ def load_config(
215
215
  >>> config = load_config() # Auto-find config
216
216
  >>> config = load_config("~/.oscura/config.yaml")
217
217
  """
218
- from oscura.config.defaults import DEFAULT_CONFIG, deep_merge
218
+ from oscura.core.config.defaults import DEFAULT_CONFIG, deep_merge
219
219
 
220
220
  config: dict[str, Any] = {}
221
221
 
@@ -224,6 +224,8 @@ def load_config(
224
224
 
225
225
  config = copy.deepcopy(DEFAULT_CONFIG)
226
226
 
227
+ # Search for config files if no explicit path provided
228
+ # use_defaults flag only controls whether to merge with DEFAULT_CONFIG
227
229
  if config_path is None:
228
230
  # Search standard locations
229
231
  search_paths = [
@@ -4,7 +4,7 @@ This module provides global memory limit configuration and settings.
4
4
 
5
5
 
6
6
  Example:
7
- >>> from oscura.config.memory import set_memory_limit, get_memory_config
7
+ >>> from oscura.core.config.memory import set_memory_limit, get_memory_config
8
8
  >>> set_memory_limit("4GB")
9
9
  >>> config = get_memory_config()
10
10
  >>> print(f"Max memory: {config.max_memory / 1e9:.1f} GB")
@@ -90,7 +90,7 @@ def set_memory_limit(limit: int | str | None) -> None:
90
90
  Environment:
91
91
  Can also be set via TK_MAX_MEMORY environment variable.
92
92
  """
93
- global _global_config # noqa: PLW0602
93
+ global _global_config
94
94
 
95
95
  if limit is None:
96
96
  _global_config.max_memory = None
@@ -117,7 +117,7 @@ def set_memory_thresholds(
117
117
  >>> set_memory_thresholds(warn_threshold=0.7, critical_threshold=0.9)
118
118
  >>> set_memory_thresholds(critical_threshold=0.95) # Keep warn unchanged
119
119
  """
120
- global _global_config # noqa: PLW0602
120
+ global _global_config
121
121
 
122
122
  if warn_threshold is not None:
123
123
  _global_config.warn_threshold = warn_threshold
@@ -139,7 +139,7 @@ def enable_auto_degrade(enabled: bool = True) -> None:
139
139
  >>> enable_auto_degrade(True)
140
140
  >>> # Operations will now auto-downsample if memory insufficient
141
141
  """
142
- global _global_config # noqa: PLW0602
142
+ global _global_config
143
143
  _global_config.auto_degrade = enabled
144
144
 
145
145
 
@@ -154,7 +154,7 @@ def set_memory_reserve(reserve: int | str) -> None:
154
154
  >>> set_memory_reserve("1GB") # Reserve 1 GB for system
155
155
  >>> set_memory_reserve(512 * 1024**2) # 512 MB
156
156
  """
157
- global _global_config # noqa: PLW0602
157
+ global _global_config
158
158
 
159
159
  if isinstance(reserve, str):
160
160
  _global_config.memory_reserve = _parse_memory_string(reserve)
@@ -5,7 +5,7 @@ configuration files between schema versions while preserving user data.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.config.migration import migrate_config, register_migration
8
+ >>> from oscura.core.config.migration import migrate_config, register_migration
9
9
  >>> # Register a migration function
10
10
  >>> def migrate_1_0_to_1_1(config: dict) -> dict:
11
11
  ... config['new_field'] = 'default_value'
@@ -17,7 +17,7 @@ from typing import TYPE_CHECKING, Any
17
17
 
18
18
  import yaml
19
19
 
20
- from oscura.config.schema import validate_against_schema
20
+ from oscura.core.config.schema import validate_against_schema
21
21
  from oscura.core.exceptions import ConfigurationError
22
22
 
23
23
  if TYPE_CHECKING:
@@ -690,7 +690,7 @@ def _substitute_variables(content: str, variables: dict[str, Any], max_depth: in
690
690
  pattern = re.compile(r"\$\{(\w+)\}")
691
691
  depth = 0
692
692
 
693
- for depth in range(max_depth): # noqa: B007
693
+ for depth in range(max_depth):
694
694
  prev_content = content
695
695
  substitutions_made = False
696
696
 
@@ -768,33 +768,99 @@ def resolve_includes(
768
768
  if not pipeline.includes:
769
769
  return pipeline
770
770
 
771
- # Normalize source file path for comparison
772
- source_key = str(Path(pipeline.source_file).resolve()) if pipeline.source_file else None
771
+ source_key = _get_source_key(pipeline)
772
+ _validate_circular_dependency(source_key, _visited)
773
+ _validate_depth_limit(_depth, max_depth, _visited, source_key)
773
774
 
774
- # Cycle detection using DFS with visited set
775
- if source_key and source_key in _visited:
776
- cycle_list = [*list(_visited), source_key]
775
+ if source_key:
776
+ _visited.add(source_key)
777
+
778
+ merged_steps = _merge_included_pipelines(
779
+ pipeline, base_path, max_depth, namespace_isolation, _visited, _depth
780
+ )
781
+ merged_steps.extend(pipeline.steps)
782
+
783
+ return _build_resolved_pipeline(pipeline, merged_steps)
784
+
785
+
786
+ def _get_source_key(pipeline: PipelineDefinition) -> str | None:
787
+ """Get normalized source file path for pipeline.
788
+
789
+ Args:
790
+ pipeline: Pipeline definition.
791
+
792
+ Returns:
793
+ Normalized source path or None.
794
+ """
795
+ return str(Path(pipeline.source_file).resolve()) if pipeline.source_file else None
796
+
797
+
798
+ def _validate_circular_dependency(source_key: str | None, visited: set[str]) -> None:
799
+ """Validate no circular dependency.
800
+
801
+ Args:
802
+ source_key: Source file path.
803
+ visited: Set of visited paths.
804
+
805
+ Raises:
806
+ PipelineValidationError: If circular dependency detected.
807
+ """
808
+ if source_key and source_key in visited:
809
+ cycle_list = [*list(visited), source_key]
777
810
  cycle = " → ".join([Path(p).name for p in cycle_list])
778
811
  raise PipelineValidationError(
779
812
  f"Circular pipeline include detected: {cycle}",
780
813
  suggestion=f"Remove circular dependency from {Path(source_key).name}",
781
814
  )
782
815
 
783
- # Depth limit check
784
- if _depth >= max_depth:
816
+
817
+ def _validate_depth_limit(
818
+ depth: int, max_depth: int, visited: set[str], source_key: str | None
819
+ ) -> None:
820
+ """Validate include depth within limit.
821
+
822
+ Args:
823
+ depth: Current depth.
824
+ max_depth: Maximum allowed depth.
825
+ visited: Set of visited paths.
826
+ source_key: Current source key.
827
+
828
+ Raises:
829
+ PipelineValidationError: If depth exceeded.
830
+ """
831
+ if depth >= max_depth:
785
832
  chain = " → ".join(
786
- [Path(p).name for p in _visited] + [Path(source_key).name if source_key else "?"]
833
+ [Path(p).name for p in visited] + [Path(source_key).name if source_key else "?"]
787
834
  )
788
835
  raise PipelineValidationError(
789
836
  f"Pipeline include depth exceeded maximum of {max_depth}",
790
837
  suggestion=f"Reduce nesting. Current chain: {chain}",
791
838
  )
792
839
 
793
- if source_key:
794
- _visited.add(source_key)
795
840
 
796
- # Merge included pipelines
841
+ def _merge_included_pipelines(
842
+ pipeline: PipelineDefinition,
843
+ base_path: Path,
844
+ max_depth: int,
845
+ namespace_isolation: bool,
846
+ visited: set[str],
847
+ depth: int,
848
+ ) -> list[PipelineStep]:
849
+ """Merge all included pipelines.
850
+
851
+ Args:
852
+ pipeline: Main pipeline.
853
+ base_path: Base path for includes.
854
+ max_depth: Maximum depth.
855
+ namespace_isolation: Enable namespacing.
856
+ visited: Visited paths.
857
+ depth: Current depth.
858
+
859
+ Returns:
860
+ List of merged steps.
861
+ """
797
862
  merged_steps = []
863
+
798
864
  for include_path in pipeline.includes:
799
865
  include_full = base_path / include_path
800
866
 
@@ -803,22 +869,18 @@ def resolve_includes(
803
869
  continue
804
870
 
805
871
  try:
806
- # Load included pipeline
807
872
  included = load_pipeline(include_full, pipeline.variables)
808
-
809
- # Recursively resolve nested includes
810
873
  resolved = resolve_includes(
811
874
  included,
812
875
  include_full.parent,
813
876
  max_depth=max_depth,
814
877
  namespace_isolation=namespace_isolation,
815
- _visited=_visited.copy(), # Copy to avoid mutation across branches
816
- _depth=_depth + 1,
878
+ _visited=visited.copy(),
879
+ _depth=depth + 1,
817
880
  )
818
881
 
819
- # Apply namespace isolation
820
882
  if namespace_isolation:
821
- namespace = Path(include_path).stem # Use filename as namespace
883
+ namespace = Path(include_path).stem
822
884
  namespaced_steps = _apply_namespace(resolved.steps, namespace)
823
885
  merged_steps.extend(namespaced_steps)
824
886
  logger.debug(f"Included pipeline '{namespace}' with {len(namespaced_steps)} steps")
@@ -832,9 +894,21 @@ def resolve_includes(
832
894
  suggestion=f"Check file exists and is valid YAML: {e}",
833
895
  ) from e
834
896
 
835
- # Add main pipeline steps after includes
836
- merged_steps.extend(pipeline.steps)
897
+ return merged_steps
898
+
837
899
 
900
+ def _build_resolved_pipeline(
901
+ pipeline: PipelineDefinition, merged_steps: list[PipelineStep]
902
+ ) -> PipelineDefinition:
903
+ """Build resolved pipeline with merged steps.
904
+
905
+ Args:
906
+ pipeline: Original pipeline.
907
+ merged_steps: Merged steps list.
908
+
909
+ Returns:
910
+ New PipelineDefinition with resolved includes.
911
+ """
838
912
  return PipelineDefinition(
839
913
  name=pipeline.name,
840
914
  version=pipeline.version,
@@ -842,7 +916,7 @@ def resolve_includes(
842
916
  steps=merged_steps,
843
917
  parallel_groups=pipeline.parallel_groups,
844
918
  variables=pipeline.variables,
845
- includes=[], # Clear after resolution
919
+ includes=[],
846
920
  source_file=pipeline.source_file,
847
921
  )
848
922
 
@@ -865,6 +939,8 @@ def _apply_namespace(steps: list[PipelineStep], namespace: str) -> list[Pipeline
865
939
  namespaced = []
866
940
  for step in steps:
867
941
  # Create a copy with namespaced name
942
+ # NECESSARY COPIES: All .copy() calls create isolated params/conditions.
943
+ # This prevents parameter mutations from affecting original step definitions.
868
944
  namespaced_step = PipelineStep(
869
945
  name=f"{namespace}.{step.name}",
870
946
  type=step.type,
@@ -15,7 +15,7 @@ from typing import Any
15
15
 
16
16
  import yaml
17
17
 
18
- from oscura.config.schema import validate_against_schema
18
+ from oscura.core.config.schema import validate_against_schema
19
19
  from oscura.core.exceptions import ConfigurationError
20
20
 
21
21
  logger = logging.getLogger(__name__)
@@ -18,7 +18,7 @@ from typing import TYPE_CHECKING, Any
18
18
 
19
19
  import yaml
20
20
 
21
- from oscura.config.schema import validate_against_schema
21
+ from oscura.core.config.schema import validate_against_schema
22
22
  from oscura.core.exceptions import ConfigurationError
23
23
 
24
24
  if TYPE_CHECKING:
@@ -644,7 +644,7 @@ class ProtocolWatcher:
644
644
  continue
645
645
 
646
646
  try:
647
- mtime = os.path.getmtime(file_path) # noqa: PTH204
647
+ mtime = os.path.getmtime(file_path)
648
648
  except OSError:
649
649
  continue
650
650
 
@@ -674,7 +674,7 @@ class ProtocolWatcher:
674
674
  for file_path in self.directory.glob("**/*.yaml"):
675
675
  if file_path.is_file():
676
676
  with contextlib.suppress(OSError):
677
- self._file_mtimes[str(file_path)] = os.path.getmtime(file_path) # noqa: PTH204
677
+ self._file_mtimes[str(file_path)] = os.path.getmtime(file_path)
678
678
 
679
679
  def _notify(self, protocol: ProtocolDefinition) -> None:
680
680
  """Notify callbacks of protocol change."""