dsh-logicprobe 0.4.0 → 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 +7 -1
- 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 -1364
- package/lib/index.js +291 -277
- package/lib/tool.js +57 -57
- 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 -151
- package/package.json +79 -78
- package/skills/logicprobe/SKILL.md +285 -268
- 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 -129
- 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 +234 -0
- package/src/index.ts +315 -301
- package/src/tool.ts +60 -60
package/src/data-tool.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { defineTool, type JsonValue } from '@deepseek-ai/dsh-tools'
|
|
2
|
+
import { runDataVerification, DATA_ENGINE_SCHEMA_VERSION } from './data-engine.js'
|
|
3
|
+
|
|
4
|
+
export const LOGICPROBE_DATAMODEL_VERIFY_TOOL_NAME = 'logicprobe_datamodel_verify'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Model-visible DSH tool wrapping the bundled data-model verification engine.
|
|
8
|
+
* The model passes a DataModelV1 object; the engine validates it and returns
|
|
9
|
+
* DS/DA/DD checks. Optional beforeModel/fieldMapping/copyPairs/migrationMappings
|
|
10
|
+
* enable before/after data-model regression and migration coverage checks.
|
|
11
|
+
*/
|
|
12
|
+
export const logicProbeDataModelVerifyTool = defineTool({
|
|
13
|
+
name: LOGICPROBE_DATAMODEL_VERIFY_TOOL_NAME,
|
|
14
|
+
description:
|
|
15
|
+
'Run executable data-model verification (logicprobe-datamodel). Takes a DataModelV1 object with schemaVersion=1, entities ({name, fields, primaryKey?, uniqueKeys?, indexes?}), relationships?, invariants?, boundaryChecks?. Optional beforeModel/fieldMapping/copyPairs/migrationMappings/backupPairs enable migration coverage, copy consistency, rollback symmetry, and before/after data regression (DD1-DD4). Returns a report with DS/DA/DD checks. See skills/logicprobe-datamodel/references/data-model-schema.md.',
|
|
16
|
+
parameters: {
|
|
17
|
+
model: {
|
|
18
|
+
type: 'json',
|
|
19
|
+
required: true,
|
|
20
|
+
description: 'DataModelV1 data model to verify.',
|
|
21
|
+
},
|
|
22
|
+
beforeModel: {
|
|
23
|
+
type: 'json',
|
|
24
|
+
description: 'Optional BEFORE DataModelV1 data model for regression comparison.',
|
|
25
|
+
},
|
|
26
|
+
fieldMapping: {
|
|
27
|
+
type: 'json',
|
|
28
|
+
description: 'Optional object mapping BEFORE "Entity.field" paths to AFTER "Entity.field" paths.',
|
|
29
|
+
},
|
|
30
|
+
copyPairs: {
|
|
31
|
+
type: 'json',
|
|
32
|
+
description: 'Optional copy pairs: { id, sourceEntity, targetEntity, mapping: {sourceField: targetField} }.',
|
|
33
|
+
},
|
|
34
|
+
migrationMappings: {
|
|
35
|
+
type: 'json',
|
|
36
|
+
description: 'Optional migration mappings: { from: "Entity.field", to: "Entity.field", transform?, note? }.',
|
|
37
|
+
},
|
|
38
|
+
backupPairs: {
|
|
39
|
+
type: 'json',
|
|
40
|
+
description: 'Optional backup/restore pairs for DA7 rollback symmetry.',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
output: {
|
|
44
|
+
schema: {
|
|
45
|
+
type: 'json',
|
|
46
|
+
description: 'logicprobe-datamodel verification report with summary and per-check findings.',
|
|
47
|
+
},
|
|
48
|
+
render(_args, value) {
|
|
49
|
+
return [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }]
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
timeoutMs: 10_000,
|
|
53
|
+
isConcurrencySafe: () => true,
|
|
54
|
+
async execute(args) {
|
|
55
|
+
return runDataVerification(args.model, {
|
|
56
|
+
beforeModel: args.beforeModel,
|
|
57
|
+
fieldMapping: args.fieldMapping as Record<string, string> | undefined,
|
|
58
|
+
copyPairs: args.copyPairs as Array<{ id: string; sourceEntity: string; targetEntity: string; mapping: Record<string, string> }> | undefined,
|
|
59
|
+
migrationMappings: args.migrationMappings as Array<{ from: string; to: string; transform?: 'copy' | 'rename' | 'split' | 'merge' | 'drop' | 'manual'; note?: string }> | undefined,
|
|
60
|
+
backupPairs: args.backupPairs as Array<{ id: string; sourceEntity: string; targetEntity: string; mapping: Record<string, string> }> | undefined,
|
|
61
|
+
}) as unknown as JsonValue
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export { DATA_ENGINE_SCHEMA_VERSION }
|
package/src/engine.ts
CHANGED
|
@@ -52,12 +52,16 @@ export interface VariableSpec {
|
|
|
52
52
|
init: number | boolean
|
|
53
53
|
min?: number
|
|
54
54
|
max?: number
|
|
55
|
+
monotonic?: 'inc' | 'dec'
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export type InvariantSpec =
|
|
58
59
|
| { id: string; description: string; kind: 'never-states'; states: string[] }
|
|
59
60
|
| { id: string; description: string; kind: 'var-in-range'; variable: string; min?: number; max?: number }
|
|
60
61
|
| { id: string; description: string; kind: 'event-before-state'; event: string; state: string }
|
|
62
|
+
| { id: string; description: string; kind: 'leads-to'; from: string; to: string }
|
|
63
|
+
| { id: string; description: string; kind: 'sequence'; events: string[] }
|
|
64
|
+
| { id: string; description: string; kind: 'atomicity'; events: string[]; commit: string; rollback?: string }
|
|
61
65
|
|
|
62
66
|
export interface ResourcePairSpec {
|
|
63
67
|
resource: string
|
|
@@ -81,6 +85,7 @@ export interface LogicModelV1 {
|
|
|
81
85
|
concurrentPairs?: [string, string][]
|
|
82
86
|
boundaryChecks?: BoundaryCheckSpec[]
|
|
83
87
|
resourcePairs?: ResourcePairSpec[]
|
|
88
|
+
idempotentEvents?: string[]
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
export interface VerificationOptions {
|
|
@@ -245,6 +250,7 @@ export function validateModel(input: unknown): { ok: true; model: LogicModelV1 }
|
|
|
245
250
|
if (variable.min !== undefined && typeof variable.min !== 'number') bad(path + '.min', 'must be a number')
|
|
246
251
|
if (variable.max !== undefined && typeof variable.max !== 'number') bad(path + '.max', 'must be a number')
|
|
247
252
|
if (typeof variable.min === 'number' && typeof variable.max === 'number' && variable.min > variable.max) bad(path + '.max', 'must be >= min')
|
|
253
|
+
if (variable.monotonic !== undefined && variable.monotonic !== 'inc' && variable.monotonic !== 'dec') bad(path + '.monotonic', "must be 'inc' or 'dec'")
|
|
248
254
|
if (variable.kind === 'integer' && typeof variable.init === 'number') {
|
|
249
255
|
if (typeof variable.min === 'number' && variable.init < variable.min) bad(path + '.init', 'must be >= min')
|
|
250
256
|
if (typeof variable.max === 'number' && variable.init > variable.max) bad(path + '.init', 'must be <= max')
|
|
@@ -278,6 +284,21 @@ export function validateModel(input: unknown): { ok: true; model: LogicModelV1 }
|
|
|
278
284
|
} else if (invariant.kind === 'event-before-state') {
|
|
279
285
|
if (typeof invariant.event !== 'string' || invariant.event.length === 0) bad(path + '.event', 'must be a non-empty string')
|
|
280
286
|
if (typeof invariant.state !== 'string' || invariant.state.length === 0) bad(path + '.state', 'must be a non-empty string')
|
|
287
|
+
} else if (invariant.kind === 'leads-to') {
|
|
288
|
+
if (typeof invariant.from !== 'string' || invariant.from.length === 0) bad(path + '.from', 'must be a non-empty string')
|
|
289
|
+
if (typeof invariant.to !== 'string' || invariant.to.length === 0) bad(path + '.to', 'must be a non-empty string')
|
|
290
|
+
} else if (invariant.kind === 'sequence') {
|
|
291
|
+
if (!Array.isArray(invariant.events) || invariant.events.length === 0) bad(path + '.events', 'must be a non-empty array')
|
|
292
|
+
else invariant.events.forEach((event, eventIndex) => {
|
|
293
|
+
if (typeof event !== 'string' || event.length === 0) bad(path + '.events[' + eventIndex + ']', 'must be a non-empty string')
|
|
294
|
+
})
|
|
295
|
+
} else if (invariant.kind === 'atomicity') {
|
|
296
|
+
if (!Array.isArray(invariant.events) || invariant.events.length === 0) bad(path + '.events', 'must be a non-empty array')
|
|
297
|
+
else invariant.events.forEach((event, eventIndex) => {
|
|
298
|
+
if (typeof event !== 'string' || event.length === 0) bad(path + '.events[' + eventIndex + ']', 'must be a non-empty string')
|
|
299
|
+
})
|
|
300
|
+
if (typeof invariant.commit !== 'string' || invariant.commit.length === 0) bad(path + '.commit', 'must be a non-empty string')
|
|
301
|
+
if (invariant.rollback !== undefined && typeof invariant.rollback !== 'string') bad(path + '.rollback', 'must be a string')
|
|
281
302
|
} else {
|
|
282
303
|
bad(path + '.kind', 'unknown invariant kind')
|
|
283
304
|
}
|
|
@@ -305,6 +326,12 @@ export function validateModel(input: unknown): { ok: true; model: LogicModelV1 }
|
|
|
305
326
|
if (!Array.isArray(check.values) || check.values.some((value) => typeof value !== 'number')) bad(path + '.values', 'must be an array of numbers')
|
|
306
327
|
})
|
|
307
328
|
}
|
|
329
|
+
if (root.idempotentEvents !== undefined) {
|
|
330
|
+
if (!Array.isArray(root.idempotentEvents)) bad('idempotentEvents', 'must be an array')
|
|
331
|
+
else root.idempotentEvents.forEach((entry, index) => {
|
|
332
|
+
if (typeof entry !== 'string' || entry.length === 0) bad('idempotentEvents[' + index + ']', 'must be a non-empty string')
|
|
333
|
+
})
|
|
334
|
+
}
|
|
308
335
|
if (root.resourcePairs !== undefined) {
|
|
309
336
|
if (!Array.isArray(root.resourcePairs)) bad('resourcePairs', 'must be an array')
|
|
310
337
|
else root.resourcePairs.forEach((entry, index) => {
|
|
@@ -1362,6 +1389,208 @@ function buildComparisonSummary(before: LogicModelV1, after: LogicModelV1, mappi
|
|
|
1362
1389
|
removedTransitions,
|
|
1363
1390
|
}
|
|
1364
1391
|
}
|
|
1392
|
+
function A8_idempotentReplay(model: LogicModelV1, exploration: Exploration): CheckResult {
|
|
1393
|
+
const events = model.idempotentEvents ?? []
|
|
1394
|
+
const findings: Finding[] = []
|
|
1395
|
+
for (const event of events) {
|
|
1396
|
+
for (const runtime of exploration.reachable) {
|
|
1397
|
+
const onceOptions = stepRuntime(model, runtime, event)
|
|
1398
|
+
if (onceOptions.length === 0) continue
|
|
1399
|
+
for (const once of onceOptions) {
|
|
1400
|
+
const twiceOptions = stepRuntime(model, once, event)
|
|
1401
|
+
if (twiceOptions.length === 0) {
|
|
1402
|
+
findings.push({
|
|
1403
|
+
code: 'A8_NOT_REPLAYABLE',
|
|
1404
|
+
severity: 'warning',
|
|
1405
|
+
message: 'Idempotent event ' + event + ' is not replayable after first application from ' + runtime.state + '.',
|
|
1406
|
+
path: [{ from: runtime.state, event, to: once.state }],
|
|
1407
|
+
evidence: { state: runtime.state, event },
|
|
1408
|
+
})
|
|
1409
|
+
continue
|
|
1410
|
+
}
|
|
1411
|
+
for (const twice of twiceOptions) {
|
|
1412
|
+
if (runtimeKey(twice) !== runtimeKey(once)) {
|
|
1413
|
+
findings.push({
|
|
1414
|
+
code: 'A8_NOT_IDEMPOTENT',
|
|
1415
|
+
severity: 'error',
|
|
1416
|
+
message: 'Idempotent event ' + event + ' changes state when applied twice from ' + runtime.state + '.',
|
|
1417
|
+
path: [{ from: runtime.state, event, to: once.state }, { from: once.state, event, to: twice.state }],
|
|
1418
|
+
evidence: { state: runtime.state, event, afterOnce: once, afterTwice: twice },
|
|
1419
|
+
})
|
|
1420
|
+
break
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
return checkResult('A8', 'Idempotent Replay', findings, findings.length === 0 ? 'Idempotent events are replay-safe' : 'Idempotent replay findings: ' + findings.length)
|
|
1427
|
+
}
|
|
1428
|
+
function S8_monotonicVariables(model: LogicModelV1): CheckResult {
|
|
1429
|
+
const findings: Finding[] = []
|
|
1430
|
+
for (const variable of model.variables ?? []) {
|
|
1431
|
+
if (variable.monotonic === undefined) continue
|
|
1432
|
+
for (const transition of model.transitions) {
|
|
1433
|
+
for (const update of transition.updates ?? []) {
|
|
1434
|
+
if (update.variable !== variable.name) continue
|
|
1435
|
+
if (variable.monotonic === 'inc' && update.op === 'dec') {
|
|
1436
|
+
findings.push({ code: 'S8_MONOTONIC_DECREASE', severity: 'error', message: 'Monotonic (inc) variable ' + variable.name + ' is decreased by ' + transition.event + '.', evidence: { variable: variable.name, transition } })
|
|
1437
|
+
} else if (variable.monotonic === 'dec' && update.op === 'inc') {
|
|
1438
|
+
findings.push({ code: 'S8_MONOTONIC_INCREASE', severity: 'error', message: 'Monotonic (dec) variable ' + variable.name + ' is increased by ' + transition.event + '.', evidence: { variable: variable.name, transition } })
|
|
1439
|
+
} else if (update.op === 'set') {
|
|
1440
|
+
findings.push({ code: 'S8_MONOTONIC_SET_REVIEW', severity: 'warning', message: 'Monotonic variable ' + variable.name + ' uses set in ' + transition.event + '; verify it cannot move backwards.', evidence: { variable: variable.name, transition } })
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return checkResult('S8', 'Monotonic Variables', findings, findings.length === 0 ? 'Monotonic variables are respected' : 'Monotonic findings: ' + findings.length)
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function findLeadsToBadPath(model: LogicModelV1, start: RuntimeState, target: string): { path: PathStep[]; reason: string } | undefined {
|
|
1449
|
+
if (start.state === target) return undefined
|
|
1450
|
+
const visited = new Set<string>()
|
|
1451
|
+
const queue: Array<{ runtime: RuntimeState; path: PathStep[] }> = [{ runtime: start, path: [] }]
|
|
1452
|
+
while (queue.length > 0) {
|
|
1453
|
+
const entry = queue.shift()!
|
|
1454
|
+
const key = runtimeKey(entry.runtime)
|
|
1455
|
+
if (entry.runtime.state === target) continue
|
|
1456
|
+
if (visited.has(key)) return { path: entry.path, reason: 'Cycle avoids target ' + target }
|
|
1457
|
+
visited.add(key)
|
|
1458
|
+
const nexts: Array<{ next: RuntimeState; event: string }> = []
|
|
1459
|
+
for (const event of allEvents(model)) {
|
|
1460
|
+
for (const next of stepRuntime(model, entry.runtime, event)) nexts.push({ next, event })
|
|
1461
|
+
}
|
|
1462
|
+
if (nexts.length === 0) return { path: entry.path, reason: 'Dead end before target ' + target }
|
|
1463
|
+
for (const { next, event } of nexts) {
|
|
1464
|
+
queue.push({ runtime: next, path: [...entry.path, { from: entry.runtime.state, event, to: next.state }] })
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return { path: [], reason: 'No path reaches target ' + target }
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function A9_leadsTo(model: LogicModelV1, exploration: Exploration): CheckResult {
|
|
1471
|
+
const findings: Finding[] = []
|
|
1472
|
+
for (const invariant of model.invariants ?? []) {
|
|
1473
|
+
if (invariant.kind !== 'leads-to') continue
|
|
1474
|
+
for (const runtime of exploration.reachable) {
|
|
1475
|
+
if (runtime.state !== invariant.from) continue
|
|
1476
|
+
const bad = findLeadsToBadPath(model, runtime, invariant.to)
|
|
1477
|
+
if (bad !== undefined) {
|
|
1478
|
+
findings.push({
|
|
1479
|
+
code: 'A9_LEADS_TO_VIOLATION',
|
|
1480
|
+
severity: 'error',
|
|
1481
|
+
message: 'Leads-to invariant "' + invariant.id + '" violated from ' + invariant.from + ': ' + bad.reason,
|
|
1482
|
+
path: bad.path,
|
|
1483
|
+
evidence: { invariant },
|
|
1484
|
+
})
|
|
1485
|
+
break
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return checkResult('A9', 'Leads-To', findings, findings.length === 0 ? 'All leads-to invariants hold' : 'Leads-to findings: ' + findings.length)
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function findSequenceViolation(model: LogicModelV1, options: NormalizedOptions, invariant: Extract<InvariantSpec, { kind: 'sequence' }>): InvariantViolation | undefined {
|
|
1493
|
+
const events = invariant.events
|
|
1494
|
+
const init = initialState(model)
|
|
1495
|
+
const key = (runtime: RuntimeState, progress: number): string => runtimeKey(runtime) + '|' + progress
|
|
1496
|
+
const visited = new Set<string>([key(init, 0)])
|
|
1497
|
+
const queue: Array<{ runtime: RuntimeState; progress: number; path: PathStep[] }> = [{ runtime: init, progress: 0, path: [] }]
|
|
1498
|
+
let steps = 0
|
|
1499
|
+
while (queue.length > 0) {
|
|
1500
|
+
const entry = queue.shift()!
|
|
1501
|
+
if (++steps > options.maxStates) break
|
|
1502
|
+
for (const event of allEvents(model)) {
|
|
1503
|
+
for (const next of stepRuntime(model, entry.runtime, event)) {
|
|
1504
|
+
let progress = entry.progress
|
|
1505
|
+
let violation = false
|
|
1506
|
+
if (progress < events.length && event === events[progress]) {
|
|
1507
|
+
progress += 1
|
|
1508
|
+
} else {
|
|
1509
|
+
const index = events.indexOf(event)
|
|
1510
|
+
if (index > progress) violation = true
|
|
1511
|
+
}
|
|
1512
|
+
const path = [...entry.path, { from: entry.runtime.state, event, to: next.state }]
|
|
1513
|
+
if (violation) {
|
|
1514
|
+
return { invariant, path, reason: 'Event ' + event + ' occurred before ' + events[progress] }
|
|
1515
|
+
}
|
|
1516
|
+
const nextKey = key(next, progress)
|
|
1517
|
+
if (!visited.has(nextKey)) {
|
|
1518
|
+
visited.add(nextKey)
|
|
1519
|
+
queue.push({ runtime: next, progress, path })
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
return undefined
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function A10_sequenceOrder(model: LogicModelV1, options: NormalizedOptions): CheckResult {
|
|
1528
|
+
const findings: Finding[] = []
|
|
1529
|
+
for (const invariant of model.invariants ?? []) {
|
|
1530
|
+
if (invariant.kind !== 'sequence') continue
|
|
1531
|
+
const violation = findSequenceViolation(model, options, invariant)
|
|
1532
|
+
if (violation !== undefined) {
|
|
1533
|
+
findings.push({
|
|
1534
|
+
code: 'A10_SEQUENCE_VIOLATION',
|
|
1535
|
+
severity: 'error',
|
|
1536
|
+
message: 'Sequence invariant "' + invariant.id + '" violated: ' + violation.reason,
|
|
1537
|
+
path: violation.path,
|
|
1538
|
+
evidence: { invariant },
|
|
1539
|
+
})
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return checkResult('A10', 'Sequence Order', findings, findings.length === 0 ? 'All sequence invariants hold' : 'Sequence findings: ' + findings.length)
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
function findAtomicityViolation(model: LogicModelV1, options: NormalizedOptions, invariant: Extract<InvariantSpec, { kind: 'atomicity' }>): InvariantViolation | undefined {
|
|
1546
|
+
const atomic = new Set(invariant.events)
|
|
1547
|
+
const init = initialState(model)
|
|
1548
|
+
const key = (runtime: RuntimeState, started: boolean, closed: boolean): string => runtimeKey(runtime) + '|' + (started ? '1' : '0') + '|' + (closed ? '1' : '0')
|
|
1549
|
+
const visited = new Set<string>([key(init, false, false)])
|
|
1550
|
+
const queue: Array<{ runtime: RuntimeState; started: boolean; closed: boolean; path: PathStep[] }> = [{ runtime: init, started: false, closed: false, path: [] }]
|
|
1551
|
+
let steps = 0
|
|
1552
|
+
while (queue.length > 0) {
|
|
1553
|
+
const entry = queue.shift()!
|
|
1554
|
+
if (++steps > options.maxStates) break
|
|
1555
|
+
for (const event of allEvents(model)) {
|
|
1556
|
+
for (const next of stepRuntime(model, entry.runtime, event)) {
|
|
1557
|
+
const started = entry.started || atomic.has(event)
|
|
1558
|
+
const closed = entry.closed || event === invariant.commit || (invariant.rollback !== undefined && event === invariant.rollback)
|
|
1559
|
+
const path = [...entry.path, { from: entry.runtime.state, event, to: next.state }]
|
|
1560
|
+
if (entry.started && !entry.closed && !atomic.has(event) && event !== invariant.commit && event !== invariant.rollback) {
|
|
1561
|
+
return { invariant, path, reason: 'Left atomic scope via ' + event + ' without commit/rollback' }
|
|
1562
|
+
}
|
|
1563
|
+
if (started && !closed && isTerminal(model, next.state)) {
|
|
1564
|
+
return { invariant, path, reason: 'Terminal state reached with incomplete atomic group' }
|
|
1565
|
+
}
|
|
1566
|
+
const nextKey = key(next, started, closed)
|
|
1567
|
+
if (!visited.has(nextKey)) {
|
|
1568
|
+
visited.add(nextKey)
|
|
1569
|
+
queue.push({ runtime: next, started, closed, path })
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return undefined
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function A11_atomicity(model: LogicModelV1, options: NormalizedOptions): CheckResult {
|
|
1578
|
+
const findings: Finding[] = []
|
|
1579
|
+
for (const invariant of model.invariants ?? []) {
|
|
1580
|
+
if (invariant.kind !== 'atomicity') continue
|
|
1581
|
+
const violation = findAtomicityViolation(model, options, invariant)
|
|
1582
|
+
if (violation !== undefined) {
|
|
1583
|
+
findings.push({
|
|
1584
|
+
code: 'A11_ATOMICITY_VIOLATION',
|
|
1585
|
+
severity: 'error',
|
|
1586
|
+
message: 'Atomicity invariant "' + invariant.id + '" violated: ' + violation.reason,
|
|
1587
|
+
path: violation.path,
|
|
1588
|
+
evidence: { invariant },
|
|
1589
|
+
})
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return checkResult('A11', 'Atomicity', findings, findings.length === 0 ? 'All atomicity invariants hold' : 'Atomicity findings: ' + findings.length)
|
|
1593
|
+
}
|
|
1365
1594
|
// ---------------------------------------------------------------------------
|
|
1366
1595
|
// main entry
|
|
1367
1596
|
// ---------------------------------------------------------------------------
|
|
@@ -1397,6 +1626,7 @@ export function runVerification(input: unknown, options: VerificationOptions = {
|
|
|
1397
1626
|
S5_eventCompleteness(model),
|
|
1398
1627
|
S6_guardCompleteness(model),
|
|
1399
1628
|
S7_invariants(model, normalized),
|
|
1629
|
+
S8_monotonicVariables(model),
|
|
1400
1630
|
A1_unexpectedEvents(model),
|
|
1401
1631
|
A2_raceInterleaving(model, exploration),
|
|
1402
1632
|
A3_orderPermutation(model, normalized),
|
|
@@ -1404,6 +1634,10 @@ export function runVerification(input: unknown, options: VerificationOptions = {
|
|
|
1404
1634
|
A5_boundaryBlast(model),
|
|
1405
1635
|
A6_resourceInjection(model),
|
|
1406
1636
|
A7_shortestViolations(model, normalized),
|
|
1637
|
+
A8_idempotentReplay(model, exploration),
|
|
1638
|
+
A9_leadsTo(model, exploration),
|
|
1639
|
+
A10_sequenceOrder(model, normalized),
|
|
1640
|
+
A11_atomicity(model, normalized),
|
|
1407
1641
|
]
|
|
1408
1642
|
let comparison: ComparisonSummary | undefined
|
|
1409
1643
|
if (options.beforeModel !== undefined) {
|