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.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/__init__.py +1 -22
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +2763 -0
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/README.md +7 -7
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +171 -63
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/reporting/templates/index.md +13 -13
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/autodetect.py +1 -5
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +11 -3
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/export/wavedrom.py +0 -430
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -338
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
"""Protocol grammar validator for detecting specification errors and inconsistencies.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive validation of protocol specifications to detect
|
|
4
|
+
grammar errors, field inconsistencies, dependency issues, and state machine problems.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from oscura.validation import ProtocolGrammarValidator
|
|
8
|
+
>>> from oscura.sessions import ProtocolSpec, FieldHypothesis
|
|
9
|
+
>>>
|
|
10
|
+
>>> # Define protocol spec
|
|
11
|
+
>>> spec = ProtocolSpec(
|
|
12
|
+
... name="MyProtocol",
|
|
13
|
+
... fields=[
|
|
14
|
+
... FieldHypothesis("header", 0, 1, "constant", 0.99, {"value": 0xAA}),
|
|
15
|
+
... FieldHypothesis("length", 1, 1, "data", 0.90),
|
|
16
|
+
... FieldHypothesis("payload", 2, 4, "data", 0.85),
|
|
17
|
+
... FieldHypothesis("checksum", 6, 1, "checksum", 0.95, {"range": (0, 6)}),
|
|
18
|
+
... ]
|
|
19
|
+
... )
|
|
20
|
+
>>>
|
|
21
|
+
>>> # Validate protocol specification
|
|
22
|
+
>>> validator = ProtocolGrammarValidator()
|
|
23
|
+
>>> report = validator.validate(spec)
|
|
24
|
+
>>>
|
|
25
|
+
>>> if report.has_errors():
|
|
26
|
+
... print("Validation failed:")
|
|
27
|
+
... for error in report.errors:
|
|
28
|
+
... print(f" {error.severity}: {error.message}")
|
|
29
|
+
... else:
|
|
30
|
+
... print("Protocol specification is valid!")
|
|
31
|
+
>>>
|
|
32
|
+
>>> # Export report
|
|
33
|
+
>>> report.export_json(Path("validation_report.json"))
|
|
34
|
+
|
|
35
|
+
References:
|
|
36
|
+
V0.6.0_COMPLETE_COMPREHENSIVE_PLAN.md: Feature 36 (Conformance Testing)
|
|
37
|
+
Formal Language Theory: Grammar validation and consistency checking
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
from dataclasses import dataclass, field
|
|
43
|
+
from enum import Enum
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
from typing import TYPE_CHECKING, Any
|
|
46
|
+
|
|
47
|
+
if TYPE_CHECKING:
|
|
48
|
+
from oscura.sessions.blackbox import FieldHypothesis, ProtocolSpec
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ErrorSeverity(Enum):
|
|
52
|
+
"""Severity level for validation errors.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
ERROR: Must fix - specification is invalid.
|
|
56
|
+
WARNING: Should fix - potential issues or ambiguities.
|
|
57
|
+
INFO: Advisory - suggestions for improvement.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
ERROR = "ERROR"
|
|
61
|
+
WARNING = "WARNING"
|
|
62
|
+
INFO = "INFO"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ErrorType(Enum):
|
|
66
|
+
"""Type of validation error.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
FIELD_OVERLAP: Fields occupy overlapping byte ranges.
|
|
70
|
+
FIELD_GAP: Gap between consecutive fields.
|
|
71
|
+
INVALID_OFFSET: Field offset is negative or invalid.
|
|
72
|
+
INVALID_LENGTH: Field length is zero or negative.
|
|
73
|
+
LENGTH_MISMATCH: Length field value doesn't match referenced field.
|
|
74
|
+
CHECKSUM_RANGE: Checksum coverage range is invalid.
|
|
75
|
+
DUPLICATE_FIELD: Multiple fields with same name.
|
|
76
|
+
UNREACHABLE_STATE: State machine has unreachable states.
|
|
77
|
+
MISSING_TRANSITION: State machine has incomplete transitions.
|
|
78
|
+
AMBIGUOUS_GRAMMAR: Protocol grammar has ambiguities.
|
|
79
|
+
INVALID_DEPENDENCY: Field dependency cannot be resolved.
|
|
80
|
+
ALIGNMENT_WARNING: Field not aligned to expected boundary.
|
|
81
|
+
ENUM_DUPLICATE: Enum has duplicate values.
|
|
82
|
+
ENUM_GAP: Enum has missing values in range.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
FIELD_OVERLAP = "FIELD_OVERLAP"
|
|
86
|
+
FIELD_GAP = "FIELD_GAP"
|
|
87
|
+
INVALID_OFFSET = "INVALID_OFFSET"
|
|
88
|
+
INVALID_LENGTH = "INVALID_LENGTH"
|
|
89
|
+
LENGTH_MISMATCH = "LENGTH_MISMATCH"
|
|
90
|
+
CHECKSUM_RANGE = "CHECKSUM_RANGE"
|
|
91
|
+
DUPLICATE_FIELD = "DUPLICATE_FIELD"
|
|
92
|
+
UNREACHABLE_STATE = "UNREACHABLE_STATE"
|
|
93
|
+
MISSING_TRANSITION = "MISSING_TRANSITION"
|
|
94
|
+
AMBIGUOUS_GRAMMAR = "AMBIGUOUS_GRAMMAR"
|
|
95
|
+
INVALID_DEPENDENCY = "INVALID_DEPENDENCY"
|
|
96
|
+
ALIGNMENT_WARNING = "ALIGNMENT_WARNING"
|
|
97
|
+
ENUM_DUPLICATE = "ENUM_DUPLICATE"
|
|
98
|
+
ENUM_GAP = "ENUM_GAP"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class ValidationError:
|
|
103
|
+
"""Single validation error with context and suggestions.
|
|
104
|
+
|
|
105
|
+
Attributes:
|
|
106
|
+
error_type: Type of error encountered.
|
|
107
|
+
severity: Severity level (ERROR/WARNING/INFO).
|
|
108
|
+
field_name: Name of field causing error (if applicable).
|
|
109
|
+
message: Human-readable error message.
|
|
110
|
+
suggestion: Suggested fix for the error.
|
|
111
|
+
line_number: Line number in specification (if applicable).
|
|
112
|
+
context: Additional context information.
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
>>> error = ValidationError(
|
|
116
|
+
... error_type=ErrorType.FIELD_OVERLAP,
|
|
117
|
+
... severity=ErrorSeverity.ERROR,
|
|
118
|
+
... field_name="checksum",
|
|
119
|
+
... message="Field 'checksum' overlaps with 'payload' at byte 5",
|
|
120
|
+
... suggestion="Move checksum field to byte 6 or reduce payload length"
|
|
121
|
+
... )
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
error_type: ErrorType
|
|
125
|
+
severity: ErrorSeverity
|
|
126
|
+
field_name: str | None
|
|
127
|
+
message: str
|
|
128
|
+
suggestion: str | None = None
|
|
129
|
+
line_number: int | None = None
|
|
130
|
+
context: dict[str, Any] = field(default_factory=dict)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class ValidationReport:
|
|
135
|
+
"""Comprehensive validation report with errors, warnings, and info.
|
|
136
|
+
|
|
137
|
+
Attributes:
|
|
138
|
+
errors: List of ERROR severity issues (must fix).
|
|
139
|
+
warnings: List of WARNING severity issues (should fix).
|
|
140
|
+
info: List of INFO severity messages (advisory).
|
|
141
|
+
protocol_name: Name of validated protocol.
|
|
142
|
+
total_fields: Total number of fields validated.
|
|
143
|
+
metadata: Additional validation metadata.
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> report = ValidationReport(
|
|
147
|
+
... errors=[],
|
|
148
|
+
... warnings=[warning1, warning2],
|
|
149
|
+
... info=[info1],
|
|
150
|
+
... protocol_name="MyProtocol",
|
|
151
|
+
... total_fields=4
|
|
152
|
+
... )
|
|
153
|
+
>>> print(f"Valid: {not report.has_errors()}")
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
errors: list[ValidationError] = field(default_factory=list)
|
|
157
|
+
warnings: list[ValidationError] = field(default_factory=list)
|
|
158
|
+
info: list[ValidationError] = field(default_factory=list)
|
|
159
|
+
protocol_name: str = ""
|
|
160
|
+
total_fields: int = 0
|
|
161
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
162
|
+
|
|
163
|
+
def has_errors(self) -> bool:
|
|
164
|
+
"""Check if report contains any errors.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
True if errors exist, False otherwise.
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
>>> report = ValidationReport(errors=[error1])
|
|
171
|
+
>>> report.has_errors()
|
|
172
|
+
True
|
|
173
|
+
"""
|
|
174
|
+
return len(self.errors) > 0
|
|
175
|
+
|
|
176
|
+
def has_warnings(self) -> bool:
|
|
177
|
+
"""Check if report contains any warnings.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
True if warnings exist, False otherwise.
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
>>> report = ValidationReport(warnings=[warning1])
|
|
184
|
+
>>> report.has_warnings()
|
|
185
|
+
True
|
|
186
|
+
"""
|
|
187
|
+
return len(self.warnings) > 0
|
|
188
|
+
|
|
189
|
+
def all_issues(self) -> list[ValidationError]:
|
|
190
|
+
"""Get all issues (errors + warnings + info).
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Combined list of all validation issues.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
>>> issues = report.all_issues()
|
|
197
|
+
>>> print(f"Total issues: {len(issues)}")
|
|
198
|
+
"""
|
|
199
|
+
return self.errors + self.warnings + self.info
|
|
200
|
+
|
|
201
|
+
def export_json(self, output: Path) -> None:
|
|
202
|
+
"""Export validation report as JSON.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
output: Output JSON file path.
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
>>> report.export_json(Path("validation.json"))
|
|
209
|
+
"""
|
|
210
|
+
import json
|
|
211
|
+
|
|
212
|
+
data = {
|
|
213
|
+
"protocol_name": self.protocol_name,
|
|
214
|
+
"total_fields": self.total_fields,
|
|
215
|
+
"summary": {
|
|
216
|
+
"errors": len(self.errors),
|
|
217
|
+
"warnings": len(self.warnings),
|
|
218
|
+
"info": len(self.info),
|
|
219
|
+
"is_valid": not self.has_errors(),
|
|
220
|
+
},
|
|
221
|
+
"errors": [
|
|
222
|
+
{
|
|
223
|
+
"type": err.error_type.value,
|
|
224
|
+
"severity": err.severity.value,
|
|
225
|
+
"field": err.field_name,
|
|
226
|
+
"message": err.message,
|
|
227
|
+
"suggestion": err.suggestion,
|
|
228
|
+
"line": err.line_number,
|
|
229
|
+
"context": err.context,
|
|
230
|
+
}
|
|
231
|
+
for err in self.errors
|
|
232
|
+
],
|
|
233
|
+
"warnings": [
|
|
234
|
+
{
|
|
235
|
+
"type": warn.error_type.value,
|
|
236
|
+
"severity": warn.severity.value,
|
|
237
|
+
"field": warn.field_name,
|
|
238
|
+
"message": warn.message,
|
|
239
|
+
"suggestion": warn.suggestion,
|
|
240
|
+
"line": warn.line_number,
|
|
241
|
+
"context": warn.context,
|
|
242
|
+
}
|
|
243
|
+
for warn in self.warnings
|
|
244
|
+
],
|
|
245
|
+
"info": [
|
|
246
|
+
{
|
|
247
|
+
"type": i.error_type.value,
|
|
248
|
+
"severity": i.severity.value,
|
|
249
|
+
"field": i.field_name,
|
|
250
|
+
"message": i.message,
|
|
251
|
+
"suggestion": i.suggestion,
|
|
252
|
+
"line": i.line_number,
|
|
253
|
+
"context": i.context,
|
|
254
|
+
}
|
|
255
|
+
for i in self.info
|
|
256
|
+
],
|
|
257
|
+
"metadata": self.metadata,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
output.write_text(json.dumps(data, indent=2))
|
|
261
|
+
|
|
262
|
+
def export_text(self, output: Path) -> None:
|
|
263
|
+
"""Export validation report as human-readable text.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
output: Output text file path.
|
|
267
|
+
|
|
268
|
+
Example:
|
|
269
|
+
>>> report.export_text(Path("validation.txt"))
|
|
270
|
+
"""
|
|
271
|
+
lines = [
|
|
272
|
+
f"Validation Report: {self.protocol_name}",
|
|
273
|
+
"=" * 80,
|
|
274
|
+
f"Total Fields: {self.total_fields}",
|
|
275
|
+
f"Errors: {len(self.errors)}",
|
|
276
|
+
f"Warnings: {len(self.warnings)}",
|
|
277
|
+
f"Info: {len(self.info)}",
|
|
278
|
+
f"Status: {'INVALID' if self.has_errors() else 'VALID'}",
|
|
279
|
+
"",
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
if self.errors:
|
|
283
|
+
lines.extend(["", "ERRORS:", "-" * 80])
|
|
284
|
+
for err in self.errors:
|
|
285
|
+
lines.append(f"[{err.error_type.value}] {err.message}")
|
|
286
|
+
if err.field_name:
|
|
287
|
+
lines.append(f" Field: {err.field_name}")
|
|
288
|
+
if err.suggestion:
|
|
289
|
+
lines.append(f" Suggestion: {err.suggestion}")
|
|
290
|
+
lines.append("")
|
|
291
|
+
|
|
292
|
+
if self.warnings:
|
|
293
|
+
lines.extend(["", "WARNINGS:", "-" * 80])
|
|
294
|
+
for warn in self.warnings:
|
|
295
|
+
lines.append(f"[{warn.error_type.value}] {warn.message}")
|
|
296
|
+
if warn.field_name:
|
|
297
|
+
lines.append(f" Field: {warn.field_name}")
|
|
298
|
+
if warn.suggestion:
|
|
299
|
+
lines.append(f" Suggestion: {warn.suggestion}")
|
|
300
|
+
lines.append("")
|
|
301
|
+
|
|
302
|
+
if self.info:
|
|
303
|
+
lines.extend(["", "INFO:", "-" * 80])
|
|
304
|
+
for i in self.info:
|
|
305
|
+
lines.append(f"[{i.error_type.value}] {i.message}")
|
|
306
|
+
if i.field_name:
|
|
307
|
+
lines.append(f" Field: {i.field_name}")
|
|
308
|
+
if i.suggestion:
|
|
309
|
+
lines.append(f" Suggestion: {i.suggestion}")
|
|
310
|
+
lines.append("")
|
|
311
|
+
|
|
312
|
+
output.write_text("\n".join(lines))
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class ProtocolGrammarValidator:
|
|
316
|
+
"""Validates protocol specifications for consistency and correctness.
|
|
317
|
+
|
|
318
|
+
Performs comprehensive validation including:
|
|
319
|
+
- Field definition checking (overlaps, gaps, valid ranges)
|
|
320
|
+
- Length field consistency
|
|
321
|
+
- Checksum coverage validation
|
|
322
|
+
- Enum value checking
|
|
323
|
+
- Conditional field dependencies
|
|
324
|
+
- State machine completeness
|
|
325
|
+
- Grammar ambiguity detection
|
|
326
|
+
|
|
327
|
+
Example:
|
|
328
|
+
>>> validator = ProtocolGrammarValidator()
|
|
329
|
+
>>> report = validator.validate(protocol_spec)
|
|
330
|
+
>>> if report.has_errors():
|
|
331
|
+
... print("Invalid specification")
|
|
332
|
+
>>> report.export_json(Path("report.json"))
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
def __init__(
|
|
336
|
+
self,
|
|
337
|
+
check_alignment: bool = True,
|
|
338
|
+
check_gaps: bool = True,
|
|
339
|
+
check_state_machine: bool = True,
|
|
340
|
+
) -> None:
|
|
341
|
+
"""Initialize grammar validator.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
check_alignment: Enable byte alignment warnings (default: True).
|
|
345
|
+
check_gaps: Enable gap detection between fields (default: True).
|
|
346
|
+
check_state_machine: Enable state machine validation (default: True).
|
|
347
|
+
|
|
348
|
+
Example:
|
|
349
|
+
>>> validator = ProtocolGrammarValidator(check_gaps=False)
|
|
350
|
+
"""
|
|
351
|
+
self.check_alignment = check_alignment
|
|
352
|
+
self.check_gaps = check_gaps
|
|
353
|
+
self.check_state_machine = check_state_machine
|
|
354
|
+
|
|
355
|
+
def validate(self, spec: ProtocolSpec) -> ValidationReport:
|
|
356
|
+
"""Validate complete protocol specification.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
spec: Protocol specification to validate.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Validation report with errors, warnings, and info.
|
|
363
|
+
|
|
364
|
+
Example:
|
|
365
|
+
>>> report = validator.validate(spec)
|
|
366
|
+
>>> print(f"Errors: {len(report.errors)}")
|
|
367
|
+
"""
|
|
368
|
+
report = ValidationReport(
|
|
369
|
+
protocol_name=spec.name,
|
|
370
|
+
total_fields=len(spec.fields),
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Validate field definitions
|
|
374
|
+
self._validate_field_definitions(spec, report)
|
|
375
|
+
|
|
376
|
+
# Validate field dependencies
|
|
377
|
+
self._validate_dependencies(spec, report)
|
|
378
|
+
|
|
379
|
+
# Validate checksums
|
|
380
|
+
self._validate_checksums(spec, report)
|
|
381
|
+
|
|
382
|
+
# Validate enums (from evidence)
|
|
383
|
+
self._validate_enums(spec, report)
|
|
384
|
+
|
|
385
|
+
# Validate state machine
|
|
386
|
+
if self.check_state_machine and spec.state_machine is not None:
|
|
387
|
+
self._validate_state_machine(spec, report)
|
|
388
|
+
|
|
389
|
+
# Add metadata
|
|
390
|
+
report.metadata["validator_config"] = {
|
|
391
|
+
"check_alignment": self.check_alignment,
|
|
392
|
+
"check_gaps": self.check_gaps,
|
|
393
|
+
"check_state_machine": self.check_state_machine,
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return report
|
|
397
|
+
|
|
398
|
+
def _check_duplicate_names(self, spec: ProtocolSpec, report: ValidationReport) -> None:
|
|
399
|
+
"""Check for duplicate field names.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
spec: Protocol specification.
|
|
403
|
+
report: Validation report to populate.
|
|
404
|
+
"""
|
|
405
|
+
field_names = [f.name for f in spec.fields]
|
|
406
|
+
seen_names: set[str] = set()
|
|
407
|
+
for name in field_names:
|
|
408
|
+
if name in seen_names:
|
|
409
|
+
report.errors.append(
|
|
410
|
+
ValidationError(
|
|
411
|
+
error_type=ErrorType.DUPLICATE_FIELD,
|
|
412
|
+
severity=ErrorSeverity.ERROR,
|
|
413
|
+
field_name=name,
|
|
414
|
+
message=f"Duplicate field name: '{name}'",
|
|
415
|
+
suggestion="Rename one of the duplicate fields to be unique",
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
seen_names.add(name)
|
|
419
|
+
|
|
420
|
+
def _check_field_basic_validity(
|
|
421
|
+
self, field_def: FieldHypothesis, report: ValidationReport
|
|
422
|
+
) -> None:
|
|
423
|
+
"""Check basic field validity (offset and length).
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
field_def: Field definition to check.
|
|
427
|
+
report: Validation report to populate.
|
|
428
|
+
"""
|
|
429
|
+
if field_def.offset < 0:
|
|
430
|
+
report.errors.append(
|
|
431
|
+
ValidationError(
|
|
432
|
+
error_type=ErrorType.INVALID_OFFSET,
|
|
433
|
+
severity=ErrorSeverity.ERROR,
|
|
434
|
+
field_name=field_def.name,
|
|
435
|
+
message=f"Field '{field_def.name}' has invalid offset: {field_def.offset}",
|
|
436
|
+
suggestion="Offset must be >= 0",
|
|
437
|
+
context={"offset": field_def.offset},
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if field_def.length <= 0:
|
|
442
|
+
report.errors.append(
|
|
443
|
+
ValidationError(
|
|
444
|
+
error_type=ErrorType.INVALID_LENGTH,
|
|
445
|
+
severity=ErrorSeverity.ERROR,
|
|
446
|
+
field_name=field_def.name,
|
|
447
|
+
message=f"Field '{field_def.name}' has invalid length: {field_def.length}",
|
|
448
|
+
suggestion="Length must be > 0",
|
|
449
|
+
context={"length": field_def.length},
|
|
450
|
+
)
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
def _check_field_overlap_and_gap(
|
|
454
|
+
self, field_def: FieldHypothesis, next_field: FieldHypothesis, report: ValidationReport
|
|
455
|
+
) -> None:
|
|
456
|
+
"""Check for overlaps and gaps between consecutive fields.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
field_def: Current field.
|
|
460
|
+
next_field: Next field.
|
|
461
|
+
report: Validation report to populate.
|
|
462
|
+
"""
|
|
463
|
+
current_end = field_def.offset + field_def.length
|
|
464
|
+
next_start = next_field.offset
|
|
465
|
+
|
|
466
|
+
if current_end > next_start:
|
|
467
|
+
report.errors.append(
|
|
468
|
+
ValidationError(
|
|
469
|
+
error_type=ErrorType.FIELD_OVERLAP,
|
|
470
|
+
severity=ErrorSeverity.ERROR,
|
|
471
|
+
field_name=field_def.name,
|
|
472
|
+
message=(
|
|
473
|
+
f"Field '{field_def.name}' (bytes {field_def.offset}-"
|
|
474
|
+
f"{current_end - 1}) overlaps with '{next_field.name}' "
|
|
475
|
+
f"(starts at byte {next_start})"
|
|
476
|
+
),
|
|
477
|
+
suggestion=(
|
|
478
|
+
f"Move '{next_field.name}' to byte {current_end} or "
|
|
479
|
+
f"reduce '{field_def.name}' length"
|
|
480
|
+
),
|
|
481
|
+
context={
|
|
482
|
+
"current_field": field_def.name,
|
|
483
|
+
"current_range": (field_def.offset, current_end - 1),
|
|
484
|
+
"next_field": next_field.name,
|
|
485
|
+
"next_offset": next_start,
|
|
486
|
+
},
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
elif self.check_gaps and current_end < next_start:
|
|
490
|
+
gap_size = next_start - current_end
|
|
491
|
+
report.warnings.append(
|
|
492
|
+
ValidationError(
|
|
493
|
+
error_type=ErrorType.FIELD_GAP,
|
|
494
|
+
severity=ErrorSeverity.WARNING,
|
|
495
|
+
field_name=field_def.name,
|
|
496
|
+
message=(
|
|
497
|
+
f"Gap of {gap_size} byte(s) between '{field_def.name}' "
|
|
498
|
+
f"and '{next_field.name}'"
|
|
499
|
+
),
|
|
500
|
+
suggestion=("Add padding field or adjust offsets to eliminate gap"),
|
|
501
|
+
context={
|
|
502
|
+
"gap_start": current_end,
|
|
503
|
+
"gap_end": next_start - 1,
|
|
504
|
+
"gap_size": gap_size,
|
|
505
|
+
},
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
def _check_field_alignment(self, field_def: FieldHypothesis, report: ValidationReport) -> None:
|
|
510
|
+
"""Check field alignment.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
field_def: Field definition.
|
|
514
|
+
report: Validation report to populate.
|
|
515
|
+
"""
|
|
516
|
+
if self.check_alignment and field_def.length in {2, 4, 8}:
|
|
517
|
+
if field_def.offset % field_def.length != 0:
|
|
518
|
+
report.info.append(
|
|
519
|
+
ValidationError(
|
|
520
|
+
error_type=ErrorType.ALIGNMENT_WARNING,
|
|
521
|
+
severity=ErrorSeverity.INFO,
|
|
522
|
+
field_name=field_def.name,
|
|
523
|
+
message=(
|
|
524
|
+
f"Field '{field_def.name}' ({field_def.length}-byte) is not "
|
|
525
|
+
f"aligned to {field_def.length}-byte boundary (offset: "
|
|
526
|
+
f"{field_def.offset})"
|
|
527
|
+
),
|
|
528
|
+
suggestion=(
|
|
529
|
+
f"Consider aligning to offset "
|
|
530
|
+
f"{(field_def.offset // field_def.length + 1) * field_def.length}"
|
|
531
|
+
),
|
|
532
|
+
context={
|
|
533
|
+
"offset": field_def.offset,
|
|
534
|
+
"alignment": field_def.length,
|
|
535
|
+
},
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
def _validate_field_definitions(self, spec: ProtocolSpec, report: ValidationReport) -> None:
|
|
540
|
+
"""Validate field definitions for overlaps, gaps, and invalid ranges.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
spec: Protocol specification.
|
|
544
|
+
report: Validation report to populate.
|
|
545
|
+
"""
|
|
546
|
+
# Check for duplicate field names
|
|
547
|
+
self._check_duplicate_names(spec, report)
|
|
548
|
+
|
|
549
|
+
# Sort fields by offset for sequential validation
|
|
550
|
+
sorted_fields = sorted(spec.fields, key=lambda f: f.offset)
|
|
551
|
+
|
|
552
|
+
for i, field_def in enumerate(sorted_fields):
|
|
553
|
+
# Check basic validity
|
|
554
|
+
self._check_field_basic_validity(field_def, report)
|
|
555
|
+
|
|
556
|
+
# Check overlap and gap with next field
|
|
557
|
+
if i < len(sorted_fields) - 1:
|
|
558
|
+
self._check_field_overlap_and_gap(field_def, sorted_fields[i + 1], report)
|
|
559
|
+
|
|
560
|
+
# Check alignment
|
|
561
|
+
self._check_field_alignment(field_def, report)
|
|
562
|
+
|
|
563
|
+
def _validate_dependencies(self, spec: ProtocolSpec, report: ValidationReport) -> None:
|
|
564
|
+
"""Validate dependencies between fields (length fields, etc.).
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
spec: Protocol specification.
|
|
568
|
+
report: Validation report to populate.
|
|
569
|
+
"""
|
|
570
|
+
# Build field name -> field mapping
|
|
571
|
+
field_map = {f.name: f for f in spec.fields}
|
|
572
|
+
|
|
573
|
+
for field_def in spec.fields:
|
|
574
|
+
# Check if field has dependency in evidence
|
|
575
|
+
if "depends_on" in field_def.evidence:
|
|
576
|
+
dep_name = field_def.evidence["depends_on"]
|
|
577
|
+
if dep_name not in field_map:
|
|
578
|
+
report.errors.append(
|
|
579
|
+
ValidationError(
|
|
580
|
+
error_type=ErrorType.INVALID_DEPENDENCY,
|
|
581
|
+
severity=ErrorSeverity.ERROR,
|
|
582
|
+
field_name=field_def.name,
|
|
583
|
+
message=(
|
|
584
|
+
f"Field '{field_def.name}' depends on non-existent field "
|
|
585
|
+
f"'{dep_name}'"
|
|
586
|
+
),
|
|
587
|
+
suggestion=f"Add field '{dep_name}' or remove dependency",
|
|
588
|
+
context={"dependency": dep_name},
|
|
589
|
+
)
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# Check length field references
|
|
593
|
+
if "references" in field_def.evidence:
|
|
594
|
+
ref_name = field_def.evidence["references"]
|
|
595
|
+
if ref_name not in field_map:
|
|
596
|
+
report.errors.append(
|
|
597
|
+
ValidationError(
|
|
598
|
+
error_type=ErrorType.INVALID_DEPENDENCY,
|
|
599
|
+
severity=ErrorSeverity.ERROR,
|
|
600
|
+
field_name=field_def.name,
|
|
601
|
+
message=(
|
|
602
|
+
f"Length field '{field_def.name}' references non-existent "
|
|
603
|
+
f"field '{ref_name}'"
|
|
604
|
+
),
|
|
605
|
+
suggestion=f"Add field '{ref_name}' or remove reference",
|
|
606
|
+
context={"reference": ref_name},
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
def _validate_checksums(self, spec: ProtocolSpec, report: ValidationReport) -> None:
|
|
611
|
+
"""Validate checksum field coverage ranges.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
spec: Protocol specification.
|
|
615
|
+
report: Validation report to populate.
|
|
616
|
+
"""
|
|
617
|
+
for field_def in spec.fields:
|
|
618
|
+
if field_def.field_type == "checksum":
|
|
619
|
+
# Check if range is specified in evidence
|
|
620
|
+
if "range" in field_def.evidence:
|
|
621
|
+
range_tuple = field_def.evidence["range"]
|
|
622
|
+
if not isinstance(range_tuple, (list, tuple)) or len(range_tuple) != 2:
|
|
623
|
+
report.errors.append(
|
|
624
|
+
ValidationError(
|
|
625
|
+
error_type=ErrorType.CHECKSUM_RANGE,
|
|
626
|
+
severity=ErrorSeverity.ERROR,
|
|
627
|
+
field_name=field_def.name,
|
|
628
|
+
message=(
|
|
629
|
+
f"Checksum '{field_def.name}' has invalid range format: "
|
|
630
|
+
f"{range_tuple}"
|
|
631
|
+
),
|
|
632
|
+
suggestion="Range must be (start_byte, end_byte) tuple",
|
|
633
|
+
context={"range": range_tuple},
|
|
634
|
+
)
|
|
635
|
+
)
|
|
636
|
+
continue
|
|
637
|
+
|
|
638
|
+
start_byte, end_byte = range_tuple
|
|
639
|
+
|
|
640
|
+
# Validate range bounds
|
|
641
|
+
if start_byte < 0 or end_byte < start_byte:
|
|
642
|
+
report.errors.append(
|
|
643
|
+
ValidationError(
|
|
644
|
+
error_type=ErrorType.CHECKSUM_RANGE,
|
|
645
|
+
severity=ErrorSeverity.ERROR,
|
|
646
|
+
field_name=field_def.name,
|
|
647
|
+
message=(
|
|
648
|
+
f"Checksum '{field_def.name}' has invalid range: "
|
|
649
|
+
f"({start_byte}, {end_byte})"
|
|
650
|
+
),
|
|
651
|
+
suggestion="Range must be (start >= 0, end >= start)",
|
|
652
|
+
context={"range": (start_byte, end_byte)},
|
|
653
|
+
)
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# Check if checksum covers itself (should not)
|
|
657
|
+
if start_byte <= field_def.offset < end_byte:
|
|
658
|
+
report.warnings.append(
|
|
659
|
+
ValidationError(
|
|
660
|
+
error_type=ErrorType.CHECKSUM_RANGE,
|
|
661
|
+
severity=ErrorSeverity.WARNING,
|
|
662
|
+
field_name=field_def.name,
|
|
663
|
+
message=(
|
|
664
|
+
f"Checksum '{field_def.name}' at byte {field_def.offset} "
|
|
665
|
+
f"is within its own coverage range ({start_byte}, {end_byte})"
|
|
666
|
+
),
|
|
667
|
+
suggestion=("Checksum should typically not cover its own location"),
|
|
668
|
+
context={
|
|
669
|
+
"checksum_offset": field_def.offset,
|
|
670
|
+
"coverage_range": (start_byte, end_byte),
|
|
671
|
+
},
|
|
672
|
+
)
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def _check_enum_duplicates(
|
|
676
|
+
self,
|
|
677
|
+
field_def: FieldHypothesis,
|
|
678
|
+
enum_values: dict[str, Any],
|
|
679
|
+
report: ValidationReport,
|
|
680
|
+
) -> None:
|
|
681
|
+
"""Check for duplicate enum values.
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
field_def: Field definition
|
|
685
|
+
enum_values: Enum values dict
|
|
686
|
+
report: Validation report to populate
|
|
687
|
+
"""
|
|
688
|
+
value_to_names: dict[Any, list[str]] = {}
|
|
689
|
+
for name, value in enum_values.items():
|
|
690
|
+
if value not in value_to_names:
|
|
691
|
+
value_to_names[value] = []
|
|
692
|
+
value_to_names[value].append(name)
|
|
693
|
+
|
|
694
|
+
for value, names in value_to_names.items():
|
|
695
|
+
if len(names) > 1:
|
|
696
|
+
report.warnings.append(
|
|
697
|
+
ValidationError(
|
|
698
|
+
error_type=ErrorType.ENUM_DUPLICATE,
|
|
699
|
+
severity=ErrorSeverity.WARNING,
|
|
700
|
+
field_name=field_def.name,
|
|
701
|
+
message=(
|
|
702
|
+
f"Enum in '{field_def.name}' has duplicate value {value} "
|
|
703
|
+
f"for names: {', '.join(names)}"
|
|
704
|
+
),
|
|
705
|
+
suggestion="Ensure each enum value is unique",
|
|
706
|
+
context={"value": value, "names": names},
|
|
707
|
+
)
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
def _check_enum_gaps(
|
|
711
|
+
self,
|
|
712
|
+
field_def: FieldHypothesis,
|
|
713
|
+
enum_values: dict[str, Any],
|
|
714
|
+
report: ValidationReport,
|
|
715
|
+
) -> None:
|
|
716
|
+
"""Check for gaps in sequential enums.
|
|
717
|
+
|
|
718
|
+
Args:
|
|
719
|
+
field_def: Field definition
|
|
720
|
+
enum_values: Enum values dict
|
|
721
|
+
report: Validation report to populate
|
|
722
|
+
"""
|
|
723
|
+
if not all(isinstance(v, int) for v in enum_values.values()):
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
int_values = sorted([v for v in enum_values.values() if isinstance(v, int)])
|
|
727
|
+
if not int_values:
|
|
728
|
+
return
|
|
729
|
+
|
|
730
|
+
min_val, max_val = int_values[0], int_values[-1]
|
|
731
|
+
expected_range = set(range(min_val, max_val + 1))
|
|
732
|
+
actual_set = set(int_values)
|
|
733
|
+
missing = expected_range - actual_set
|
|
734
|
+
|
|
735
|
+
if missing and len(missing) <= 5: # Only report small gaps
|
|
736
|
+
report.info.append(
|
|
737
|
+
ValidationError(
|
|
738
|
+
error_type=ErrorType.ENUM_GAP,
|
|
739
|
+
severity=ErrorSeverity.INFO,
|
|
740
|
+
field_name=field_def.name,
|
|
741
|
+
message=(
|
|
742
|
+
f"Enum in '{field_def.name}' has gaps in value range: "
|
|
743
|
+
f"missing {sorted(missing)}"
|
|
744
|
+
),
|
|
745
|
+
suggestion="Add missing enum values or verify range",
|
|
746
|
+
context={"missing_values": sorted(missing)},
|
|
747
|
+
)
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
def _validate_enums(self, spec: ProtocolSpec, report: ValidationReport) -> None:
|
|
751
|
+
"""Validate enum values from field evidence.
|
|
752
|
+
|
|
753
|
+
Args:
|
|
754
|
+
spec: Protocol specification.
|
|
755
|
+
report: Validation report to populate.
|
|
756
|
+
"""
|
|
757
|
+
for field_def in spec.fields:
|
|
758
|
+
if "enum_values" not in field_def.evidence:
|
|
759
|
+
continue
|
|
760
|
+
|
|
761
|
+
enum_values = field_def.evidence["enum_values"]
|
|
762
|
+
if not isinstance(enum_values, dict):
|
|
763
|
+
continue
|
|
764
|
+
|
|
765
|
+
# Validate enum properties
|
|
766
|
+
self._check_enum_duplicates(field_def, enum_values, report)
|
|
767
|
+
self._check_enum_gaps(field_def, enum_values, report)
|
|
768
|
+
|
|
769
|
+
def _validate_state_machine(self, spec: ProtocolSpec, report: ValidationReport) -> None:
|
|
770
|
+
"""Validate state machine completeness and reachability.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
spec: Protocol specification.
|
|
774
|
+
report: Validation report to populate.
|
|
775
|
+
"""
|
|
776
|
+
sm = spec.state_machine
|
|
777
|
+
if sm is None:
|
|
778
|
+
return
|
|
779
|
+
|
|
780
|
+
# Verify state machine structure
|
|
781
|
+
if not self._check_state_machine_format(sm, report):
|
|
782
|
+
return
|
|
783
|
+
|
|
784
|
+
states = sm.states
|
|
785
|
+
transitions = sm.transitions
|
|
786
|
+
|
|
787
|
+
# Check state reachability
|
|
788
|
+
self._check_unreachable_states(sm, states, transitions, report)
|
|
789
|
+
|
|
790
|
+
# Check for dead-end states
|
|
791
|
+
self._check_dead_end_states(sm, states, transitions, report)
|
|
792
|
+
|
|
793
|
+
def _check_state_machine_format(self, sm: Any, report: ValidationReport) -> bool:
|
|
794
|
+
"""Check if state machine has required attributes.
|
|
795
|
+
|
|
796
|
+
Args:
|
|
797
|
+
sm: State machine object.
|
|
798
|
+
report: Validation report to populate with errors.
|
|
799
|
+
|
|
800
|
+
Returns:
|
|
801
|
+
True if format is valid, False otherwise.
|
|
802
|
+
"""
|
|
803
|
+
if not hasattr(sm, "states") or not hasattr(sm, "transitions"):
|
|
804
|
+
report.warnings.append(
|
|
805
|
+
ValidationError(
|
|
806
|
+
error_type=ErrorType.AMBIGUOUS_GRAMMAR,
|
|
807
|
+
severity=ErrorSeverity.WARNING,
|
|
808
|
+
field_name=None,
|
|
809
|
+
message="State machine format not recognized, skipping validation",
|
|
810
|
+
suggestion="Ensure state machine has 'states' and 'transitions' attributes",
|
|
811
|
+
)
|
|
812
|
+
)
|
|
813
|
+
return False
|
|
814
|
+
return True
|
|
815
|
+
|
|
816
|
+
def _check_unreachable_states(
|
|
817
|
+
self, sm: Any, states: Any, transitions: Any, report: ValidationReport
|
|
818
|
+
) -> None:
|
|
819
|
+
"""Find and report unreachable states.
|
|
820
|
+
|
|
821
|
+
Args:
|
|
822
|
+
sm: State machine with initial_state attribute.
|
|
823
|
+
states: Collection of all states.
|
|
824
|
+
transitions: Collection of transitions.
|
|
825
|
+
report: Validation report to populate.
|
|
826
|
+
"""
|
|
827
|
+
if not hasattr(sm, "initial_state"):
|
|
828
|
+
return
|
|
829
|
+
|
|
830
|
+
# Build reachability set using BFS
|
|
831
|
+
reachable = self._find_reachable_states(sm.initial_state, transitions)
|
|
832
|
+
|
|
833
|
+
# Report unreachable states
|
|
834
|
+
unreachable = set(states) - reachable
|
|
835
|
+
for state in unreachable:
|
|
836
|
+
report.warnings.append(
|
|
837
|
+
ValidationError(
|
|
838
|
+
error_type=ErrorType.UNREACHABLE_STATE,
|
|
839
|
+
severity=ErrorSeverity.WARNING,
|
|
840
|
+
field_name=None,
|
|
841
|
+
message=f"State '{state}' is unreachable from initial state",
|
|
842
|
+
suggestion="Add transition to this state or remove it",
|
|
843
|
+
context={"state": state, "initial_state": sm.initial_state},
|
|
844
|
+
)
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
def _find_reachable_states(self, initial_state: Any, transitions: Any) -> set[Any]:
|
|
848
|
+
"""Find all states reachable from initial state using BFS.
|
|
849
|
+
|
|
850
|
+
Args:
|
|
851
|
+
initial_state: Starting state.
|
|
852
|
+
transitions: Collection of transitions with source/target attributes.
|
|
853
|
+
|
|
854
|
+
Returns:
|
|
855
|
+
Set of reachable states.
|
|
856
|
+
"""
|
|
857
|
+
reachable = {initial_state}
|
|
858
|
+
queue = [initial_state]
|
|
859
|
+
|
|
860
|
+
while queue:
|
|
861
|
+
current = queue.pop(0)
|
|
862
|
+
for trans in transitions:
|
|
863
|
+
if hasattr(trans, "source") and trans.source == current:
|
|
864
|
+
target = trans.target if hasattr(trans, "target") else None
|
|
865
|
+
if target and target not in reachable:
|
|
866
|
+
reachable.add(target)
|
|
867
|
+
queue.append(target)
|
|
868
|
+
|
|
869
|
+
return reachable
|
|
870
|
+
|
|
871
|
+
def _check_dead_end_states(
|
|
872
|
+
self, sm: Any, states: Any, transitions: Any, report: ValidationReport
|
|
873
|
+
) -> None:
|
|
874
|
+
"""Find and report states with no outgoing transitions.
|
|
875
|
+
|
|
876
|
+
Args:
|
|
877
|
+
sm: State machine with optional final_states attribute.
|
|
878
|
+
states: Collection of all states.
|
|
879
|
+
transitions: Collection of transitions.
|
|
880
|
+
report: Validation report to populate.
|
|
881
|
+
"""
|
|
882
|
+
# Identify states with outgoing transitions
|
|
883
|
+
states_with_transitions = {
|
|
884
|
+
trans.source for trans in transitions if hasattr(trans, "source")
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
# Get designated final states
|
|
888
|
+
final_states = set()
|
|
889
|
+
if hasattr(sm, "final_states"):
|
|
890
|
+
final_states = set(sm.final_states)
|
|
891
|
+
|
|
892
|
+
# Report dead ends (states without transitions and not marked as final)
|
|
893
|
+
for state in states:
|
|
894
|
+
if state not in states_with_transitions and state not in final_states:
|
|
895
|
+
report.warnings.append(
|
|
896
|
+
ValidationError(
|
|
897
|
+
error_type=ErrorType.MISSING_TRANSITION,
|
|
898
|
+
severity=ErrorSeverity.WARNING,
|
|
899
|
+
field_name=None,
|
|
900
|
+
message=f"State '{state}' has no outgoing transitions (dead end)",
|
|
901
|
+
suggestion="Add transitions from this state or mark as final state",
|
|
902
|
+
context={"state": state},
|
|
903
|
+
)
|
|
904
|
+
)
|