dsh-logicprobe 0.3.1 → 0.5.0
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.
- package/README.en-US.md +7 -1
- package/README.md +9 -2
- package/lib/concurrency-tool.js +34 -0
- package/lib/concurrency.js +76 -0
- package/lib/data-engine.js +930 -0
- package/lib/data-tool.js +61 -0
- package/lib/engine.js +1622 -1103
- package/lib/index.js +291 -277
- package/lib/tool.js +57 -45
- package/lib/types/concurrency-tool.d.ts +8 -0
- package/lib/types/concurrency.d.ts +20 -0
- package/lib/types/data-engine.d.ts +199 -0
- package/lib/types/data-tool.d.ts +10 -0
- package/lib/types/engine.d.ts +171 -133
- package/lib/types/tool.d.ts +4 -2
- package/package.json +79 -78
- package/skills/logicprobe/SKILL.md +285 -266
- package/skills/logicprobe/references/__pycache__/verification-harness.cpython-312.pyc +0 -0
- package/skills/logicprobe/references/concurrency-risk-guide.md +54 -0
- package/skills/logicprobe/references/dsh-model-schema.md +145 -104
- package/skills/logicprobe/references/logic-verification-guide.md +463 -413
- package/skills/logicprobe/references/verification-harness.py +806 -582
- package/skills/logicprobe-datamodel/SKILL.md +124 -0
- package/skills/logicprobe-datamodel/references/__pycache__/data-model-harness.cpython-312.pyc +0 -0
- package/skills/logicprobe-datamodel/references/data-model-guide.md +62 -0
- package/skills/logicprobe-datamodel/references/data-model-harness.py +528 -0
- package/skills/logicprobe-datamodel/references/data-model-schema.md +128 -0
- package/src/concurrency-tool.ts +37 -0
- package/src/concurrency.ts +102 -0
- package/src/data-engine.ts +1001 -0
- package/src/data-tool.ts +65 -0
- package/src/engine.ts +533 -0
- package/src/index.ts +315 -301
- package/src/tool.ts +60 -48
|
@@ -1,582 +1,806 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""State Machine Verification Harness — generic template for logicprobe Phase 2a/2b.
|
|
3
|
-
|
|
4
|
-
Fill in the MODEL section below with states, transitions, invariants extracted from the plan.
|
|
5
|
-
Run: python3 verification-harness.py
|
|
6
|
-
Output: structured verification report for Phase 3 gap analysis.
|
|
7
|
-
"""
|
|
8
|
-
import sys
|
|
9
|
-
from collections import deque
|
|
10
|
-
from itertools import permutations
|
|
11
|
-
|
|
12
|
-
# =============================================================================
|
|
13
|
-
# MODEL — fill in from plan extraction
|
|
14
|
-
# =============================================================================
|
|
15
|
-
|
|
16
|
-
STATES: dict[str, dict[str, str]] = {
|
|
17
|
-
# "STATE_NAME": {
|
|
18
|
-
# "event_or_condition": "NEXT_STATE",
|
|
19
|
-
# "another_event": "ANOTHER_STATE",
|
|
20
|
-
# },
|
|
21
|
-
# For guarded transitions, encode guard in event name:
|
|
22
|
-
# "timeout (retry_count==0)": "RETRY",
|
|
23
|
-
# "timeout (retry_count>=1)": "FATAL",
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
INIT: str = "INIT"
|
|
27
|
-
|
|
28
|
-
TERMINALS: set[str] = set() # States where machine intentionally stops
|
|
29
|
-
|
|
30
|
-
# Invariants: list of {"desc": "human description", "check": lambda states, reachable: bool}
|
|
31
|
-
INVARIANTS: list[dict] = []
|
|
32
|
-
|
|
33
|
-
# Event pairs that can arrive concurrently (for A2 race interleaving)
|
|
34
|
-
CONCURRENT_PAIRS: list[tuple[str, str]] = []
|
|
35
|
-
|
|
36
|
-
# Counter/timer variables mentioned in guards (for A5 boundary blast)
|
|
37
|
-
# Format: {"name": "variable_name", "max_valid": max_value, "type": "counter|timestamp"}
|
|
38
|
-
BOUNDARY_VARS: list[dict] = []
|
|
39
|
-
|
|
40
|
-
# Paired operations (for A4 pair symmetry)
|
|
41
|
-
# Format: ("acquire_event_name", "release_event_name")
|
|
42
|
-
PAIRS: list[tuple[str, str]] = [
|
|
43
|
-
# ("lock", "unlock"),
|
|
44
|
-
# ("alloc", "free"),
|
|
45
|
-
# ("start", "stop"),
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
base =
|
|
169
|
-
if
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def
|
|
268
|
-
"""
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
"
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
"
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
("
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""State Machine Verification Harness — generic template for logicprobe Phase 2a/2b.
|
|
3
|
+
|
|
4
|
+
Fill in the MODEL section below with states, transitions, invariants extracted from the plan.
|
|
5
|
+
Run: python3 verification-harness.py
|
|
6
|
+
Output: structured verification report for Phase 3 gap analysis.
|
|
7
|
+
"""
|
|
8
|
+
import sys
|
|
9
|
+
from collections import deque
|
|
10
|
+
from itertools import permutations
|
|
11
|
+
|
|
12
|
+
# =============================================================================
|
|
13
|
+
# MODEL — fill in from plan extraction
|
|
14
|
+
# =============================================================================
|
|
15
|
+
|
|
16
|
+
STATES: dict[str, dict[str, str]] = {
|
|
17
|
+
# "STATE_NAME": {
|
|
18
|
+
# "event_or_condition": "NEXT_STATE",
|
|
19
|
+
# "another_event": "ANOTHER_STATE",
|
|
20
|
+
# },
|
|
21
|
+
# For guarded transitions, encode guard in event name:
|
|
22
|
+
# "timeout (retry_count==0)": "RETRY",
|
|
23
|
+
# "timeout (retry_count>=1)": "FATAL",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
INIT: str = "INIT"
|
|
27
|
+
|
|
28
|
+
TERMINALS: set[str] = set() # States where machine intentionally stops
|
|
29
|
+
|
|
30
|
+
# Invariants: list of {"desc": "human description", "check": lambda states, reachable: bool}
|
|
31
|
+
INVARIANTS: list[dict] = []
|
|
32
|
+
|
|
33
|
+
# Event pairs that can arrive concurrently (for A2 race interleaving)
|
|
34
|
+
CONCURRENT_PAIRS: list[tuple[str, str]] = []
|
|
35
|
+
|
|
36
|
+
# Counter/timer variables mentioned in guards (for A5 boundary blast)
|
|
37
|
+
# Format: {"name": "variable_name", "max_valid": max_value, "type": "counter|timestamp"}
|
|
38
|
+
BOUNDARY_VARS: list[dict] = []
|
|
39
|
+
|
|
40
|
+
# Paired operations (for A4 pair symmetry)
|
|
41
|
+
# Format: ("acquire_event_name", "release_event_name")
|
|
42
|
+
PAIRS: list[tuple[str, str]] = [
|
|
43
|
+
# ("lock", "unlock"),
|
|
44
|
+
# ("alloc", "free"),
|
|
45
|
+
# ("start", "stop"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# Monotonic variables (for S8)
|
|
49
|
+
# Format: {"name": "counter", "direction": "inc|dec", "increase_events": [...], "decrease_events": [...]}
|
|
50
|
+
MONOTONIC_VARS: list[dict] = []
|
|
51
|
+
|
|
52
|
+
# Events that must be idempotent (for A8)
|
|
53
|
+
IDEMPOTENT_EVENTS: set[str] = set()
|
|
54
|
+
|
|
55
|
+
# Leads-to requirements (for A9): list of (from_state, to_state)
|
|
56
|
+
LEADS_TO: list[tuple[str, str]] = []
|
|
57
|
+
|
|
58
|
+
# Sequence requirements (for A10): list of event lists that must occur in order
|
|
59
|
+
SEQUENCES: list[list[str]] = []
|
|
60
|
+
|
|
61
|
+
# Atomic groups (for A11): {"events": [...], "commit": "...", "rollback": "..."}
|
|
62
|
+
ATOMIC_GROUPS: list[dict] = []
|
|
63
|
+
|
|
64
|
+
# Before model for D1-D4 comparison (optional)
|
|
65
|
+
BEFORE_STATES: dict[str, dict[str, str]] = {}
|
|
66
|
+
BEFORE_INIT: str = "INIT"
|
|
67
|
+
BEFORE_TERMINALS: set[str] = set()
|
|
68
|
+
STATE_MAPPING: dict[str, str] = {}
|
|
69
|
+
|
|
70
|
+
# =============================================================================
|
|
71
|
+
# PHASE 2a: STRUCTURAL PRIMITIVES
|
|
72
|
+
# =============================================================================
|
|
73
|
+
|
|
74
|
+
def all_states():
|
|
75
|
+
return set(STATES.keys())
|
|
76
|
+
|
|
77
|
+
def all_events():
|
|
78
|
+
events = set()
|
|
79
|
+
for trans in STATES.values():
|
|
80
|
+
events.update(trans.keys())
|
|
81
|
+
return events
|
|
82
|
+
|
|
83
|
+
def S1_reachability():
|
|
84
|
+
"""BFS from INIT — find unreachable states."""
|
|
85
|
+
visited = set()
|
|
86
|
+
queue = deque([INIT])
|
|
87
|
+
while queue:
|
|
88
|
+
s = queue.popleft()
|
|
89
|
+
if s in visited:
|
|
90
|
+
continue
|
|
91
|
+
visited.add(s)
|
|
92
|
+
for nxt in STATES.get(s, {}).values():
|
|
93
|
+
if nxt not in visited:
|
|
94
|
+
queue.append(nxt)
|
|
95
|
+
unreachable = all_states() - visited
|
|
96
|
+
return {
|
|
97
|
+
"pass": len(unreachable) == 0,
|
|
98
|
+
"reachable": sorted(visited),
|
|
99
|
+
"unreachable": sorted(unreachable),
|
|
100
|
+
"detail": f"{len(unreachable)} unreachable: {sorted(unreachable)}" if unreachable else "All states reachable",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def S2_deadlock():
|
|
104
|
+
"""Any non-terminal state with zero outgoing transitions?"""
|
|
105
|
+
deadlocks = []
|
|
106
|
+
for s in sorted(all_states()):
|
|
107
|
+
if s not in TERMINALS and len(STATES.get(s, {})) == 0:
|
|
108
|
+
deadlocks.append(s)
|
|
109
|
+
return {
|
|
110
|
+
"pass": len(deadlocks) == 0,
|
|
111
|
+
"deadlocks": deadlocks,
|
|
112
|
+
"detail": f"Deadlocks: {deadlocks}" if deadlocks else "No deadlocks",
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
def S3_liveness():
|
|
116
|
+
"""Detect absorbing cycles that exclude expected terminal/recovery states."""
|
|
117
|
+
# Build reverse graph to find cycles
|
|
118
|
+
cycles = []
|
|
119
|
+
for start in all_states():
|
|
120
|
+
# DFS from each state to find cycles
|
|
121
|
+
def find_cycle(s, path, visited_cycle):
|
|
122
|
+
if s in visited_cycle:
|
|
123
|
+
idx = path.index(s)
|
|
124
|
+
return path[idx:]
|
|
125
|
+
visited_cycle.add(s)
|
|
126
|
+
for nxt in STATES.get(s, {}).values():
|
|
127
|
+
result = find_cycle(nxt, path + [s], visited_cycle.copy())
|
|
128
|
+
if result:
|
|
129
|
+
return result
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
cycle = find_cycle(start, [], set())
|
|
133
|
+
if cycle and cycle not in cycles:
|
|
134
|
+
cycles.append(cycle)
|
|
135
|
+
|
|
136
|
+
# An absorbing cycle is one where ALL transitions from cycle states stay in the cycle
|
|
137
|
+
absorbing = []
|
|
138
|
+
for cycle in cycles:
|
|
139
|
+
cycle_set = set(cycle)
|
|
140
|
+
is_absorbing = True
|
|
141
|
+
for s in cycle:
|
|
142
|
+
for nxt in STATES.get(s, {}).values():
|
|
143
|
+
if nxt not in cycle_set:
|
|
144
|
+
is_absorbing = False
|
|
145
|
+
break
|
|
146
|
+
if not is_absorbing:
|
|
147
|
+
break
|
|
148
|
+
if is_absorbing:
|
|
149
|
+
# Check if cycle excludes terminal/recovery states
|
|
150
|
+
if not (cycle_set & TERMINALS):
|
|
151
|
+
absorbing.append(cycle)
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
"pass": len(absorbing) == 0,
|
|
155
|
+
"absorbing_cycles": absorbing,
|
|
156
|
+
"all_cycles": cycles,
|
|
157
|
+
"detail": f"Absorbing cycles (no exit, no terminal): {absorbing}" if absorbing else "No harmful absorbing cycles",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def S4_determinism():
|
|
161
|
+
"""Same (state, event) → multiple different targets?"""
|
|
162
|
+
# The dict-of-dicts structure is inherently deterministic per event key.
|
|
163
|
+
# This check verifies: for each state, event names are unambiguous (no duplicates).
|
|
164
|
+
ambiguous = []
|
|
165
|
+
for s in sorted(all_states()):
|
|
166
|
+
seen = {}
|
|
167
|
+
for event, target in STATES.get(s, {}).items():
|
|
168
|
+
base = event.split("(")[0].strip() # Strip guard suffix for comparison
|
|
169
|
+
if base in seen and seen[base] != target:
|
|
170
|
+
ambiguous.append((s, base, seen[base], target))
|
|
171
|
+
seen[base] = target
|
|
172
|
+
return {
|
|
173
|
+
"pass": len(ambiguous) == 0,
|
|
174
|
+
"ambiguous": ambiguous,
|
|
175
|
+
"detail": f"Ambiguous transitions: {ambiguous}" if ambiguous else "Deterministic",
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
def S5_event_completeness():
|
|
179
|
+
"""States missing handlers for events that other states handle."""
|
|
180
|
+
events = all_events()
|
|
181
|
+
warnings = []
|
|
182
|
+
for s in sorted(all_states()):
|
|
183
|
+
if s in TERMINALS:
|
|
184
|
+
continue
|
|
185
|
+
handled = set(STATES.get(s, {}).keys())
|
|
186
|
+
# Only flag if a state is missing events that are relevant (handled elsewhere)
|
|
187
|
+
relevant = set()
|
|
188
|
+
for e in events - handled:
|
|
189
|
+
# Check if this event type appears in guard variants
|
|
190
|
+
base = e.split("(")[0].strip()
|
|
191
|
+
if any(base in h for h in handled):
|
|
192
|
+
pass # Already handled via guard variant
|
|
193
|
+
else:
|
|
194
|
+
relevant.add(e)
|
|
195
|
+
if relevant:
|
|
196
|
+
warnings.append((s, sorted(relevant)))
|
|
197
|
+
return {
|
|
198
|
+
"pass": len(warnings) == 0,
|
|
199
|
+
"warnings": warnings,
|
|
200
|
+
"detail": f"Missing event handlers: {warnings}" if warnings else "All states handle all relevant events",
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
def S6_guard_completeness():
|
|
204
|
+
"""For each transition with a guard condition, are ALL branch outcomes defined?"""
|
|
205
|
+
# Group transitions by (state, base_event)
|
|
206
|
+
guard_groups = {}
|
|
207
|
+
for s in sorted(all_states()):
|
|
208
|
+
for event in STATES.get(s, {}):
|
|
209
|
+
base = event.split("(")[0].strip()
|
|
210
|
+
key = (s, base)
|
|
211
|
+
if key not in guard_groups:
|
|
212
|
+
guard_groups[key] = []
|
|
213
|
+
guard_groups[key].append(event)
|
|
214
|
+
|
|
215
|
+
incomplete = []
|
|
216
|
+
for (s, base), variants in guard_groups.items():
|
|
217
|
+
if len(variants) > 1:
|
|
218
|
+
# Guard exists — check if there's a default/else path
|
|
219
|
+
has_default = any("else" in v.lower() or "default" in v.lower() for v in variants)
|
|
220
|
+
has_explicit = len(variants) >= 2 # At minimum, two guard branches
|
|
221
|
+
# Heuristic: if we have guard variants but no explicit "else", flag it
|
|
222
|
+
if not has_default:
|
|
223
|
+
incomplete.append({
|
|
224
|
+
"state": s,
|
|
225
|
+
"event": base,
|
|
226
|
+
"variants": variants,
|
|
227
|
+
"missing": "else/default branch",
|
|
228
|
+
})
|
|
229
|
+
return {
|
|
230
|
+
"pass": len(incomplete) == 0,
|
|
231
|
+
"incomplete_guards": incomplete,
|
|
232
|
+
"detail": f"Incomplete guards: {incomplete}" if incomplete else "All guard branches defined or single-path",
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
def S7_invariants():
|
|
236
|
+
"""Verify each claimed invariant against all reachable states."""
|
|
237
|
+
# First compute reachable set
|
|
238
|
+
visited = set()
|
|
239
|
+
queue = deque([INIT])
|
|
240
|
+
while queue:
|
|
241
|
+
s = queue.popleft()
|
|
242
|
+
if s in visited:
|
|
243
|
+
continue
|
|
244
|
+
visited.add(s)
|
|
245
|
+
for nxt in STATES.get(s, {}).values():
|
|
246
|
+
if nxt not in visited:
|
|
247
|
+
queue.append(nxt)
|
|
248
|
+
|
|
249
|
+
violations = []
|
|
250
|
+
for inv in INVARIANTS:
|
|
251
|
+
try:
|
|
252
|
+
if not inv["check"](STATES, visited):
|
|
253
|
+
violations.append(inv["desc"])
|
|
254
|
+
except Exception as e:
|
|
255
|
+
violations.append(f"{inv['desc']} — ERROR: {e}")
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
"pass": len(violations) == 0,
|
|
259
|
+
"violations": violations,
|
|
260
|
+
"detail": f"Invariant violations: {violations}" if violations else "All invariants hold",
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# =============================================================================
|
|
264
|
+
# PHASE 2b: ADVERSARIAL PROBES
|
|
265
|
+
# =============================================================================
|
|
266
|
+
|
|
267
|
+
def step(current, events):
|
|
268
|
+
"""Simulate a sequence of events from current state. Returns final state."""
|
|
269
|
+
s = current
|
|
270
|
+
for e in events:
|
|
271
|
+
if s in TERMINALS:
|
|
272
|
+
break
|
|
273
|
+
trans = STATES.get(s, {})
|
|
274
|
+
# Exact match first
|
|
275
|
+
if e in trans:
|
|
276
|
+
s = trans[e]
|
|
277
|
+
else:
|
|
278
|
+
# Try matching guard variants — pick the first matching base event
|
|
279
|
+
base_match = None
|
|
280
|
+
for evt, target in trans.items():
|
|
281
|
+
if evt.startswith(e) or e.startswith(evt.split("(")[0].strip()):
|
|
282
|
+
base_match = target
|
|
283
|
+
break
|
|
284
|
+
if base_match:
|
|
285
|
+
s = base_match
|
|
286
|
+
# else: event ignored (unhandled) — stay in current state
|
|
287
|
+
return s
|
|
288
|
+
|
|
289
|
+
def A1_unexpected_event():
|
|
290
|
+
"""Inject every event into every state that doesn't handle it."""
|
|
291
|
+
events = all_events()
|
|
292
|
+
findings = []
|
|
293
|
+
for s in sorted(all_states()):
|
|
294
|
+
if s in TERMINALS:
|
|
295
|
+
continue
|
|
296
|
+
handled = set(STATES.get(s, {}).keys())
|
|
297
|
+
unhandled = events - handled
|
|
298
|
+
if unhandled:
|
|
299
|
+
findings.append({
|
|
300
|
+
"state": s,
|
|
301
|
+
"unhandled": sorted(unhandled),
|
|
302
|
+
"risk": "Event silently ignored — may represent undefined behavior",
|
|
303
|
+
})
|
|
304
|
+
return {
|
|
305
|
+
"pass": len(findings) == 0,
|
|
306
|
+
"findings": findings,
|
|
307
|
+
"detail": f"{len(findings)} states with unhandled events" if findings else "All event/state combinations defined",
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
def A2_race_interleaving():
|
|
311
|
+
"""For each concurrent pair, test both arrival orders."""
|
|
312
|
+
if not CONCURRENT_PAIRS:
|
|
313
|
+
return {"pass": True, "findings": [], "detail": "No concurrent pairs defined — skipped"}
|
|
314
|
+
|
|
315
|
+
findings = []
|
|
316
|
+
for e1, e2 in CONCURRENT_PAIRS:
|
|
317
|
+
# Test from each state where both events are possible
|
|
318
|
+
for s in sorted(all_states()):
|
|
319
|
+
trans = STATES.get(s, {})
|
|
320
|
+
if e1 not in trans and e2 not in trans:
|
|
321
|
+
continue
|
|
322
|
+
final_e1e2 = step(s, [e1, e2])
|
|
323
|
+
final_e2e1 = step(s, [e2, e1])
|
|
324
|
+
if final_e1e2 != final_e2e1:
|
|
325
|
+
findings.append({
|
|
326
|
+
"state": s,
|
|
327
|
+
"pair": (e1, e2),
|
|
328
|
+
"final_e1_then_e2": final_e1e2,
|
|
329
|
+
"final_e2_then_e1": final_e2e1,
|
|
330
|
+
"risk": "Order-dependent outcome",
|
|
331
|
+
})
|
|
332
|
+
return {
|
|
333
|
+
"pass": len(findings) == 0,
|
|
334
|
+
"findings": findings,
|
|
335
|
+
"detail": f"{len(findings)} order-dependent race conditions" if findings else "No race conditions detected",
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
def A3_order_permutation():
|
|
339
|
+
"""Test if different event orderings produce different terminal states."""
|
|
340
|
+
events = sorted(all_events())
|
|
341
|
+
if len(events) > 5:
|
|
342
|
+
# Too many permutations — sample subset
|
|
343
|
+
events = events[:5]
|
|
344
|
+
|
|
345
|
+
# Find event sequences that reach different terminals
|
|
346
|
+
terminal_sets = []
|
|
347
|
+
for perm in permutations(events):
|
|
348
|
+
final = step(INIT, list(perm))
|
|
349
|
+
terminal_sets.append((list(perm), final))
|
|
350
|
+
|
|
351
|
+
unique_terminals = set(t[1] for t in terminal_sets)
|
|
352
|
+
|
|
353
|
+
findings = []
|
|
354
|
+
if len(unique_terminals) > 1:
|
|
355
|
+
# Find the sequences producing each terminal
|
|
356
|
+
by_terminal = {}
|
|
357
|
+
for seq, term in terminal_sets:
|
|
358
|
+
by_terminal.setdefault(term, []).append(seq)
|
|
359
|
+
findings.append({
|
|
360
|
+
"terminal_states": sorted(unique_terminals),
|
|
361
|
+
"sequences": {t: seqs[0] for t, seqs in by_terminal.items()},
|
|
362
|
+
"risk": f"Same events produce {len(unique_terminals)} different outcomes",
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
"pass": len(findings) == 0,
|
|
367
|
+
"findings": findings,
|
|
368
|
+
"detail": f"Order-dependent: {len(unique_terminals)} different outcomes" if findings else "Order-independent",
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
def A4_pair_symmetry():
|
|
372
|
+
"""Check lock/unlock, alloc/free, start/stop symmetry."""
|
|
373
|
+
if not PAIRS:
|
|
374
|
+
return {"pass": True, "findings": [], "detail": "No paired operations defined — skipped"}
|
|
375
|
+
|
|
376
|
+
events = all_events()
|
|
377
|
+
findings = []
|
|
378
|
+
|
|
379
|
+
for acquire, release in PAIRS:
|
|
380
|
+
# Check if this pair type is even used in the model
|
|
381
|
+
acquire_events = [e for e in events if acquire in e.lower()]
|
|
382
|
+
release_events = [e for e in events if release in e.lower()]
|
|
383
|
+
|
|
384
|
+
if not acquire_events and not release_events:
|
|
385
|
+
continue
|
|
386
|
+
|
|
387
|
+
# Simple check: for each acquire event, is there a corresponding release?
|
|
388
|
+
# More sophisticated: every path that contains acquire must contain release
|
|
389
|
+
# before reaching a terminal state or another acquire.
|
|
390
|
+
|
|
391
|
+
# Quick heuristic: count occurrences in transition targets
|
|
392
|
+
acquire_targets = set()
|
|
393
|
+
release_sources = set()
|
|
394
|
+
for s, trans in STATES.items():
|
|
395
|
+
for e, t in trans.items():
|
|
396
|
+
if any(ae in e.lower() for ae in acquire_events):
|
|
397
|
+
acquire_targets.add(t)
|
|
398
|
+
if any(re in e.lower() for re in release_events):
|
|
399
|
+
release_sources.add(s)
|
|
400
|
+
|
|
401
|
+
if acquire_targets and not release_sources:
|
|
402
|
+
findings.append({
|
|
403
|
+
"pair": (acquire, release),
|
|
404
|
+
"risk": f"'{acquire}' used but no '{release}' found — resource leak likely",
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
"pass": len(findings) == 0,
|
|
409
|
+
"findings": findings,
|
|
410
|
+
"detail": f"Asymmetric pairs: {findings}" if findings else "All pairs balanced",
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
def A5_boundary_blast():
|
|
414
|
+
"""Probe counter/timer boundary values."""
|
|
415
|
+
if not BOUNDARY_VARS:
|
|
416
|
+
return {"pass": True, "findings": [], "detail": "No boundary variables defined — skipped"}
|
|
417
|
+
|
|
418
|
+
findings = []
|
|
419
|
+
for var in BOUNDARY_VARS:
|
|
420
|
+
name = var["name"]
|
|
421
|
+
max_val = var.get("max_valid", 255)
|
|
422
|
+
vtype = var.get("type", "counter")
|
|
423
|
+
|
|
424
|
+
test_values = [0, 1, max_val - 1, max_val, max_val + 1]
|
|
425
|
+
if vtype == "counter":
|
|
426
|
+
test_values += [2**8 - 1, 2**16 - 1, 2**32 - 1]
|
|
427
|
+
|
|
428
|
+
for tv in test_values:
|
|
429
|
+
if tv < 0 or tv > max_val:
|
|
430
|
+
findings.append({
|
|
431
|
+
"variable": name,
|
|
432
|
+
"tested_value": tv,
|
|
433
|
+
"max_valid": max_val,
|
|
434
|
+
"risk": f"Value {tv} exceeds max valid {max_val} — overflow possible",
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
if vtype == "timestamp":
|
|
438
|
+
findings.append({
|
|
439
|
+
"variable": name,
|
|
440
|
+
"risk": "Timestamp wraparound — verify elapsed_ms() / elapsed_ticks() handle wraparound correctly",
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
"pass": len(findings) == 0,
|
|
445
|
+
"findings": findings,
|
|
446
|
+
"detail": f"Boundary issues: {len(findings)}" if findings else "Boundary checks passed",
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
def A6_resource_injection():
|
|
450
|
+
"""Simulate resource failures at each state."""
|
|
451
|
+
# Heuristic: identify states that likely allocate resources
|
|
452
|
+
findings = []
|
|
453
|
+
for s in sorted(all_states()):
|
|
454
|
+
if s in TERMINALS:
|
|
455
|
+
continue
|
|
456
|
+
state_lower = s.lower()
|
|
457
|
+
events_lower = [e.lower() for e in STATES.get(s, {}).keys()]
|
|
458
|
+
|
|
459
|
+
# Does this state look like it allocates resources?
|
|
460
|
+
alloc_keywords = ["alloc", "create", "init", "start", "open", "connect", "begin"]
|
|
461
|
+
has_alloc = any(kw in state_lower or any(kw in e for e in events_lower) for kw in alloc_keywords)
|
|
462
|
+
|
|
463
|
+
if not has_alloc:
|
|
464
|
+
continue
|
|
465
|
+
|
|
466
|
+
# Does it have an error recovery path?
|
|
467
|
+
error_keywords = ["error", "fail", "retry", "timeout", "recover", "fatal"]
|
|
468
|
+
has_recovery = any(any(kw in e for e in events_lower) for kw in error_keywords)
|
|
469
|
+
|
|
470
|
+
if not has_recovery:
|
|
471
|
+
findings.append({
|
|
472
|
+
"state": s,
|
|
473
|
+
"risk": f"State '{s}' may allocate resources but has no visible error recovery path",
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
"pass": len(findings) == 0,
|
|
478
|
+
"findings": findings,
|
|
479
|
+
"detail": f"Resource vulnerability: {len(findings)} states" if findings else "No resource vulnerabilities detected",
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
def A7_shortest_violation(invariant_results):
|
|
483
|
+
"""Find shortest violating path for each failed invariant (requires re-running with path tracking)."""
|
|
484
|
+
if not INVARIANTS:
|
|
485
|
+
return {"pass": True, "findings": [], "detail": "No invariants defined — skipped"}
|
|
486
|
+
|
|
487
|
+
findings = []
|
|
488
|
+
for inv in INVARIANTS:
|
|
489
|
+
# BFS to find shortest path to violation
|
|
490
|
+
queue = deque([(INIT, [])])
|
|
491
|
+
visited = set()
|
|
492
|
+
found = None
|
|
493
|
+
|
|
494
|
+
while queue and not found:
|
|
495
|
+
s, path = queue.popleft()
|
|
496
|
+
if s in visited:
|
|
497
|
+
continue
|
|
498
|
+
visited.add(s)
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
if not inv["check"](STATES, {s}):
|
|
502
|
+
found = path
|
|
503
|
+
break
|
|
504
|
+
except Exception:
|
|
505
|
+
found = path
|
|
506
|
+
break
|
|
507
|
+
|
|
508
|
+
for event, nxt in STATES.get(s, {}).items():
|
|
509
|
+
if nxt not in visited:
|
|
510
|
+
queue.append((nxt, path + [(s, event, nxt)]))
|
|
511
|
+
|
|
512
|
+
if found:
|
|
513
|
+
findings.append({
|
|
514
|
+
"invariant": inv["desc"],
|
|
515
|
+
"violating_path": found,
|
|
516
|
+
"path_length": len(found),
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
"pass": len(findings) == 0,
|
|
521
|
+
"findings": findings,
|
|
522
|
+
"detail": f"Violated invariants: {len(findings)}" if findings else "All invariants hold for all reachable paths",
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
# =============================================================================
|
|
526
|
+
# S8 / A8-A11 / D1-D4
|
|
527
|
+
# =============================================================================
|
|
528
|
+
|
|
529
|
+
def S8_monotonic_variables():
|
|
530
|
+
findings = []
|
|
531
|
+
for var in MONOTONIC_VARS:
|
|
532
|
+
direction = var.get('direction')
|
|
533
|
+
name = var.get('name', '?')
|
|
534
|
+
if direction == 'inc':
|
|
535
|
+
for event in var.get('decrease_events', []):
|
|
536
|
+
findings.append({'code': 'S8_MONOTONIC_DECREASE', 'severity': 'error', 'message': f'Monotonic inc variable {name} decreased by {event}', 'evidence': {'variable': name, 'event': event}})
|
|
537
|
+
elif direction == 'dec':
|
|
538
|
+
for event in var.get('increase_events', []):
|
|
539
|
+
findings.append({'code': 'S8_MONOTONIC_INCREASE', 'severity': 'error', 'message': f'Monotonic dec variable {name} increased by {event}', 'evidence': {'variable': name, 'event': event}})
|
|
540
|
+
return {'pass': len(findings) == 0, 'findings': findings, 'detail': 'Monotonic variables respected' if not findings else f'{len(findings)} monotonic violations'}
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def A8_idempotent_replay():
|
|
544
|
+
findings = []
|
|
545
|
+
for event in IDEMPOTENT_EVENTS:
|
|
546
|
+
for state in sorted(all_states()):
|
|
547
|
+
if event not in STATES.get(state, {}):
|
|
548
|
+
continue
|
|
549
|
+
once = STATES[state][event]
|
|
550
|
+
twice = STATES.get(once, {}).get(event)
|
|
551
|
+
if twice is None:
|
|
552
|
+
findings.append({'code': 'A8_NOT_REPLAYABLE', 'severity': 'warning', 'message': f'Idempotent event {event} not replayable from {state}', 'evidence': {'state': state, 'event': event}})
|
|
553
|
+
elif twice != once:
|
|
554
|
+
findings.append({'code': 'A8_NOT_IDEMPOTENT', 'severity': 'error', 'message': f'Idempotent event {event} changes state from {state}', 'evidence': {'state': state, 'event': event, 'once': once, 'twice': twice}})
|
|
555
|
+
return {'pass': len(findings) == 0, 'findings': findings, 'detail': 'Idempotent events replay-safe' if not findings else f'{len(findings)} idempotent findings'}
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def A9_leads_to():
|
|
559
|
+
findings = []
|
|
560
|
+
for from_state, to_state in LEADS_TO:
|
|
561
|
+
visited = set()
|
|
562
|
+
queue = deque([from_state])
|
|
563
|
+
bad = False
|
|
564
|
+
while queue:
|
|
565
|
+
state = queue.popleft()
|
|
566
|
+
if state == to_state:
|
|
567
|
+
continue
|
|
568
|
+
if state in visited:
|
|
569
|
+
bad = True
|
|
570
|
+
break
|
|
571
|
+
visited.add(state)
|
|
572
|
+
nexts = list(STATES.get(state, {}).values())
|
|
573
|
+
if not nexts:
|
|
574
|
+
bad = True
|
|
575
|
+
break
|
|
576
|
+
for nxt in nexts:
|
|
577
|
+
queue.append(nxt)
|
|
578
|
+
if bad:
|
|
579
|
+
findings.append({'code': 'A9_LEADS_TO_VIOLATION', 'severity': 'error', 'message': f'State {from_state} does not always lead to {to_state}', 'evidence': {'from': from_state, 'to': to_state}})
|
|
580
|
+
return {'pass': len(findings) == 0, 'findings': findings, 'detail': 'Leads-to invariants hold' if not findings else f'{len(findings)} leads-to violations'}
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def A10_sequence_order():
|
|
584
|
+
findings = []
|
|
585
|
+
for seq in SEQUENCES:
|
|
586
|
+
visited = set()
|
|
587
|
+
queue = deque([(INIT, 0, [])])
|
|
588
|
+
violation = None
|
|
589
|
+
while queue:
|
|
590
|
+
state, progress, path = queue.popleft()
|
|
591
|
+
key = (state, progress)
|
|
592
|
+
if key in visited:
|
|
593
|
+
continue
|
|
594
|
+
visited.add(key)
|
|
595
|
+
for event, nxt in STATES.get(state, {}).items():
|
|
596
|
+
p = progress
|
|
597
|
+
bad = False
|
|
598
|
+
if p < len(seq) and event == seq[p]:
|
|
599
|
+
p += 1
|
|
600
|
+
else:
|
|
601
|
+
idx = seq.index(event) if event in seq else -1
|
|
602
|
+
if idx > p:
|
|
603
|
+
bad = True
|
|
604
|
+
new_path = path + [event]
|
|
605
|
+
if bad:
|
|
606
|
+
violation = new_path
|
|
607
|
+
break
|
|
608
|
+
queue.append((nxt, p, new_path))
|
|
609
|
+
if violation:
|
|
610
|
+
break
|
|
611
|
+
if violation:
|
|
612
|
+
findings.append({'code': 'A10_SEQUENCE_VIOLATION', 'severity': 'error', 'message': f'Sequence {seq} violated by path {violation}', 'evidence': {'sequence': seq, 'path': violation}})
|
|
613
|
+
return {'pass': len(findings) == 0, 'findings': findings, 'detail': 'Sequence orders hold' if not findings else f'{len(findings)} sequence violations'}
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def A11_atomicity():
|
|
617
|
+
findings = []
|
|
618
|
+
for group in ATOMIC_GROUPS:
|
|
619
|
+
atomic = set(group.get('events', []))
|
|
620
|
+
commit = group.get('commit')
|
|
621
|
+
rollback = group.get('rollback')
|
|
622
|
+
visited = set()
|
|
623
|
+
queue = deque([(INIT, False, False, [])])
|
|
624
|
+
violation = None
|
|
625
|
+
while queue:
|
|
626
|
+
state, started, closed, path = queue.popleft()
|
|
627
|
+
key = (state, started, closed)
|
|
628
|
+
if key in visited:
|
|
629
|
+
continue
|
|
630
|
+
visited.add(key)
|
|
631
|
+
for event, nxt in STATES.get(state, {}).items():
|
|
632
|
+
ns = started or event in atomic
|
|
633
|
+
nc = closed or event == commit or (rollback is not None and event == rollback)
|
|
634
|
+
new_path = path + [event]
|
|
635
|
+
if started and not closed and event not in atomic and event != commit and event != rollback:
|
|
636
|
+
violation = (new_path, 'left atomic scope')
|
|
637
|
+
break
|
|
638
|
+
if ns and not nc and nxt in TERMINALS:
|
|
639
|
+
violation = (new_path, 'terminal with incomplete atomic group')
|
|
640
|
+
break
|
|
641
|
+
queue.append((nxt, ns, nc, new_path))
|
|
642
|
+
if violation:
|
|
643
|
+
break
|
|
644
|
+
if violation:
|
|
645
|
+
findings.append({'code': 'A11_ATOMICITY_VIOLATION', 'severity': 'error', 'message': f'Atomicity violated: {violation[1]}', 'evidence': {'group': group, 'path': violation[0]}})
|
|
646
|
+
return {'pass': len(findings) == 0, 'findings': findings, 'detail': 'Atomicity invariants hold' if not findings else f'{len(findings)} atomicity violations'}
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def D1_behavioral_preservation():
|
|
650
|
+
findings = []
|
|
651
|
+
if not BEFORE_STATES:
|
|
652
|
+
return {'pass': True, 'findings': [], 'detail': 'No before model — skipped'}
|
|
653
|
+
for state, trans in BEFORE_STATES.items():
|
|
654
|
+
mapped = STATE_MAPPING.get(state, state)
|
|
655
|
+
for event in trans:
|
|
656
|
+
if event not in STATES.get(mapped, {}):
|
|
657
|
+
findings.append({'code': 'D1_EVENT_DISABLED', 'severity': 'error', 'message': f'BEFORE event {event} disabled in AFTER state {mapped}', 'evidence': {'beforeState': state, 'mappedState': mapped, 'event': event}})
|
|
658
|
+
return {'pass': len(findings) == 0, 'findings': findings, 'detail': 'Behavior preserved' if not findings else f'{len(findings)} behavior regressions'}
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def D2_invariant_continuity():
|
|
662
|
+
if not BEFORE_STATES:
|
|
663
|
+
return {'pass': True, 'findings': [], 'detail': 'No before model — skipped'}
|
|
664
|
+
return {'pass': True, 'findings': [], 'detail': 'Invariant continuity requires structured invariants; use DSH or data-model harness for full check'}
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def D3_delta_summary():
|
|
668
|
+
findings = []
|
|
669
|
+
if not BEFORE_STATES:
|
|
670
|
+
return {'pass': True, 'findings': [], 'detail': 'No before model — skipped'}
|
|
671
|
+
before_events = {e for t in BEFORE_STATES.values() for e in t}
|
|
672
|
+
after_events = all_events()
|
|
673
|
+
before_states = set(BEFORE_STATES)
|
|
674
|
+
after_states = all_states()
|
|
675
|
+
removed_states = sorted(before_states - after_states)
|
|
676
|
+
added_states = sorted(after_states - before_states)
|
|
677
|
+
removed_events = sorted(before_events - after_events)
|
|
678
|
+
added_events = sorted(after_events - before_events)
|
|
679
|
+
for state in removed_states:
|
|
680
|
+
findings.append({'code': 'D3_REMOVED_STATE', 'severity': 'warning', 'message': f'BEFORE state {state} removed', 'evidence': {'state': state}})
|
|
681
|
+
for event in removed_events:
|
|
682
|
+
findings.append({'code': 'D3_REMOVED_EVENT', 'severity': 'warning', 'message': f'BEFORE event {event} removed', 'evidence': {'event': event}})
|
|
683
|
+
detail = f'Delta: +{len(added_states)} states, -{len(removed_states)} states, +{len(added_events)} events, -{len(removed_events)} events'
|
|
684
|
+
return {'pass': True, 'findings': findings, 'detail': detail}
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def D4_deadlock_liveness_regression():
|
|
688
|
+
findings = []
|
|
689
|
+
if not BEFORE_STATES:
|
|
690
|
+
return {'pass': True, 'findings': [], 'detail': 'No before model — skipped'}
|
|
691
|
+
def deadlock_set(states, terminals):
|
|
692
|
+
return {s for s in states if s not in terminals and len(states.get(s, {})) == 0}
|
|
693
|
+
before_dead = deadlock_set(BEFORE_STATES, BEFORE_TERMINALS)
|
|
694
|
+
after_dead = deadlock_set(STATES, TERMINALS)
|
|
695
|
+
mapped_before_dead = {STATE_MAPPING.get(s, s) for s in before_dead}
|
|
696
|
+
for state in sorted(after_dead):
|
|
697
|
+
if state not in mapped_before_dead:
|
|
698
|
+
findings.append({'code': 'D4_DEADLOCK_REGRESSION', 'severity': 'error', 'message': f'AFTER introduces deadlock in {state}', 'evidence': {'state': state}})
|
|
699
|
+
return {'pass': len(findings) == 0, 'findings': findings, 'detail': 'No deadlock regressions' if not findings else f'{len(findings)} deadlock regressions'}
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
# =============================================================================
|
|
703
|
+
# MAIN
|
|
704
|
+
# =============================================================================
|
|
705
|
+
|
|
706
|
+
def run_all():
|
|
707
|
+
results = {}
|
|
708
|
+
errors = 0
|
|
709
|
+
warnings = 0
|
|
710
|
+
|
|
711
|
+
print("=" * 60)
|
|
712
|
+
print("PHASE 2a: STRUCTURAL PRIMITIVES")
|
|
713
|
+
print("=" * 60)
|
|
714
|
+
|
|
715
|
+
checks_2a = [
|
|
716
|
+
("S1 Reachability", S1_reachability),
|
|
717
|
+
("S2 Deadlock", S2_deadlock),
|
|
718
|
+
("S3 Liveness", S3_liveness),
|
|
719
|
+
("S4 Determinism", S4_determinism),
|
|
720
|
+
("S5 Event Completeness", S5_event_completeness),
|
|
721
|
+
("S6 Guard Completeness", S6_guard_completeness),
|
|
722
|
+
("S7 Invariants", S7_invariants),
|
|
723
|
+
("S8 Monotonic Variables", S8_monotonic_variables),
|
|
724
|
+
]
|
|
725
|
+
|
|
726
|
+
for name, check_fn in checks_2a:
|
|
727
|
+
result = check_fn()
|
|
728
|
+
results[name] = result
|
|
729
|
+
status = "PASS" if result["pass"] else "FAIL"
|
|
730
|
+
prefix = " " if result["pass"] else " [!] "
|
|
731
|
+
print(f"{prefix}[{status}] {name}: {result['detail']}")
|
|
732
|
+
if not result["pass"]:
|
|
733
|
+
if "Warning" in str(type(check_fn)):
|
|
734
|
+
warnings += 1
|
|
735
|
+
else:
|
|
736
|
+
errors += 1
|
|
737
|
+
|
|
738
|
+
print()
|
|
739
|
+
print("=" * 60)
|
|
740
|
+
print("PHASE 2b: ADVERSARIAL PROBES")
|
|
741
|
+
print("=" * 60)
|
|
742
|
+
|
|
743
|
+
probes_2b = [
|
|
744
|
+
("A1 Unexpected Event", A1_unexpected_event),
|
|
745
|
+
("A2 Race Interleaving", A2_race_interleaving),
|
|
746
|
+
("A3 Order Permutation", A3_order_permutation),
|
|
747
|
+
("A4 Pair Symmetry", A4_pair_symmetry),
|
|
748
|
+
("A5 Boundary Blast", A5_boundary_blast),
|
|
749
|
+
("A6 Resource Injection", A6_resource_injection),
|
|
750
|
+
("A7 Shortest Violation", lambda: A7_shortest_violation(results.get("S7 Invariants", {}))),
|
|
751
|
+
("A8 Idempotent Replay", A8_idempotent_replay),
|
|
752
|
+
("A9 Leads-To", A9_leads_to),
|
|
753
|
+
("A10 Sequence Order", A10_sequence_order),
|
|
754
|
+
("A11 Atomicity", A11_atomicity),
|
|
755
|
+
]
|
|
756
|
+
|
|
757
|
+
for name, probe_fn in probes_2b:
|
|
758
|
+
result = probe_fn()
|
|
759
|
+
results[name] = result
|
|
760
|
+
status = "PASS" if result["pass"] else "FAIL"
|
|
761
|
+
prefix = " " if result["pass"] else " [!] "
|
|
762
|
+
print(f"{prefix}[{status}] {name}: {result['detail']}")
|
|
763
|
+
if not result["pass"]:
|
|
764
|
+
warnings += 1 # Probe failures are warnings by default (may be false positives)
|
|
765
|
+
|
|
766
|
+
if BEFORE_STATES:
|
|
767
|
+
print()
|
|
768
|
+
print("=" * 60)
|
|
769
|
+
print("PHASE 2c: BEFORE/AFTER REGRESSION")
|
|
770
|
+
print("=" * 60)
|
|
771
|
+
diff_checks = [
|
|
772
|
+
("D1 Behavioral Preservation", D1_behavioral_preservation),
|
|
773
|
+
("D2 Invariant Continuity", D2_invariant_continuity),
|
|
774
|
+
("D3 Delta Summary", D3_delta_summary),
|
|
775
|
+
("D4 Deadlock/Liveness Regression", D4_deadlock_liveness_regression),
|
|
776
|
+
]
|
|
777
|
+
for name, check_fn in diff_checks:
|
|
778
|
+
result = check_fn()
|
|
779
|
+
results[name] = result
|
|
780
|
+
status = "PASS" if result["pass"] else "FAIL"
|
|
781
|
+
prefix = " " if result["pass"] else " [!] "
|
|
782
|
+
print(f"{prefix}[{status}] {name}: {result['detail']}")
|
|
783
|
+
if not result["pass"]:
|
|
784
|
+
errors += 1
|
|
785
|
+
|
|
786
|
+
print()
|
|
787
|
+
print("=" * 60)
|
|
788
|
+
print(f"SUMMARY: {errors} structural errors, {warnings} probe/other warnings")
|
|
789
|
+
print("=" * 60)
|
|
790
|
+
|
|
791
|
+
if errors > 0:
|
|
792
|
+
print()
|
|
793
|
+
print("ACTION: Fix structural errors before proceeding to Phase 3.")
|
|
794
|
+
print("Structural errors indicate the plan's logic is incomplete or inconsistent.")
|
|
795
|
+
|
|
796
|
+
if warnings > 0 and errors == 0:
|
|
797
|
+
print()
|
|
798
|
+
print("ACTION: Review probe warnings — may be false positives or acceptable risks.")
|
|
799
|
+
print("Escalate confirmed findings to Phase 3 gap analysis.")
|
|
800
|
+
|
|
801
|
+
return errors, warnings, results
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
if __name__ == "__main__":
|
|
805
|
+
errors, warnings, results = run_all()
|
|
806
|
+
sys.exit(1 if errors > 0 else 0)
|