dsh-logicprobe 0.8.1 → 0.9.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/lib/concurrency-tool.js +1 -1
- package/lib/engine.js +230 -39
- package/lib/index.js +12 -3
- package/lib/tool.js +1 -1
- package/lib/types/engine.d.ts +1 -1
- package/package.json +1 -1
- package/skills/logicprobe/SKILL.md +16 -3
- package/skills/logicprobe/references/dsh-model-schema.md +6 -5
- package/skills/logicprobe/references/logic-verification-guide.md +7 -3
- package/skills/logicprobe/references/logicprobe-engine.py +201 -38
- package/skills/logicprobe-datamodel/SKILL.md +1 -1
- package/src/concurrency-tool.ts +2 -2
- package/src/engine.ts +215 -34
- package/src/index.ts +12 -3
- package/src/tool.ts +2 -2
package/src/engine.ts
CHANGED
|
@@ -154,7 +154,7 @@ export type InvariantSpec =
|
|
|
154
154
|
when?: InvariantWhen
|
|
155
155
|
}
|
|
156
156
|
| { id: string; description: string; kind: 'event-before-state'; event: string; state: string }
|
|
157
|
-
| { id: string; description: string; kind: 'leads-to'; from: string; to: string }
|
|
157
|
+
| { id: string; description: string; kind: 'leads-to'; from: string; to: string | string[] }
|
|
158
158
|
| { id: string; description: string; kind: 'sequence'; events: string[] }
|
|
159
159
|
| { id: string; description: string; kind: 'atomicity'; events: string[]; commit: string; rollback?: string }
|
|
160
160
|
| { id: string; description: string; kind: 'budget'; budget: number }
|
|
@@ -469,8 +469,27 @@ export function validateModel(input: unknown): { ok: true; model: LogicModelV1 }
|
|
|
469
469
|
} else if (invariant.kind === 'leads-to') {
|
|
470
470
|
if (typeof invariant.from !== 'string' || invariant.from.length === 0) bad(path + '.from', 'must be a non-empty string')
|
|
471
471
|
else if (!invariantStateIds.has(invariant.from)) bad(path + '.from', 'references unknown state ' + invariant.from)
|
|
472
|
-
|
|
473
|
-
|
|
472
|
+
// `to` is one state id or a target set. A set with a duplicate member is a
|
|
473
|
+
// model-authoring slip that would otherwise give one meaning two hashes, so it
|
|
474
|
+
// is rejected rather than silently deduplicated.
|
|
475
|
+
if (typeof invariant.to === 'string') {
|
|
476
|
+
if (invariant.to.length === 0) bad(path + '.to', 'must be a non-empty string')
|
|
477
|
+
else if (!invariantStateIds.has(invariant.to)) bad(path + '.to', 'references unknown state ' + invariant.to)
|
|
478
|
+
} else if (Array.isArray(invariant.to)) {
|
|
479
|
+
if (invariant.to.length === 0) bad(path + '.to', 'must be a non-empty array of state ids')
|
|
480
|
+
else {
|
|
481
|
+
const targets = new Set<string>()
|
|
482
|
+
invariant.to.forEach((target, targetIndex) => {
|
|
483
|
+
const at = path + '.to[' + targetIndex + ']'
|
|
484
|
+
if (typeof target !== 'string' || target.length === 0) bad(at, 'must be a non-empty string')
|
|
485
|
+
else if (!invariantStateIds.has(target)) bad(at, 'references unknown state ' + target)
|
|
486
|
+
else if (targets.has(target)) bad(at, 'duplicates state ' + target + ' in the target set')
|
|
487
|
+
else targets.add(target)
|
|
488
|
+
})
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
bad(path + '.to', 'must be a state id or a non-empty array of state ids')
|
|
492
|
+
}
|
|
474
493
|
} else if (invariant.kind === 'sequence') {
|
|
475
494
|
if (!Array.isArray(invariant.events) || invariant.events.length === 0) bad(path + '.events', 'must be a non-empty array')
|
|
476
495
|
else invariant.events.forEach((event, eventIndex) => {
|
|
@@ -1108,6 +1127,96 @@ function shortestViolationForInvariant(model: LogicModelV1, options: NormalizedO
|
|
|
1108
1127
|
return undefined
|
|
1109
1128
|
}
|
|
1110
1129
|
|
|
1130
|
+
/**
|
|
1131
|
+
* Whether an invariant asserts a property of whole runs rather than of a single
|
|
1132
|
+
* runtime state. These kinds cannot be decided by a state predicate, so they are not
|
|
1133
|
+
* part of `shortestViolationForInvariant` (and therefore not of S7), which owns the
|
|
1134
|
+
* states; A9-A13 own the verdicts, and `pathPropertyViolation` below makes the same
|
|
1135
|
+
* verdicts available to A7 and D2.
|
|
1136
|
+
*/
|
|
1137
|
+
function isPathPropertyInvariant(invariant: InvariantSpec): boolean {
|
|
1138
|
+
return invariant.kind === 'leads-to'
|
|
1139
|
+
|| invariant.kind === 'sequence'
|
|
1140
|
+
|| invariant.kind === 'atomicity'
|
|
1141
|
+
|| invariant.kind === 'budget'
|
|
1142
|
+
|| invariant.kind === 'probability'
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/** Whether a probability invariant's bound fails for the computed hit probability. */
|
|
1146
|
+
function probabilityViolated(invariant: Extract<InvariantSpec, { kind: 'probability' }>, probability: number): boolean {
|
|
1147
|
+
const eps = 1e-9
|
|
1148
|
+
if (invariant.op === '>=') return probability < invariant.p - eps
|
|
1149
|
+
if (invariant.op === '>') return probability <= invariant.p + eps
|
|
1150
|
+
if (invariant.op === '<=') return probability > invariant.p + eps
|
|
1151
|
+
return probability >= invariant.p - eps
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/** Every runtime state reachable from init, capped like the other searches. */
|
|
1155
|
+
function reachableRuntimeStates(model: LogicModelV1, options: NormalizedOptions): RuntimeState[] {
|
|
1156
|
+
const init = initialState(model)
|
|
1157
|
+
const visited = new Set<string>([runtimeKey(init)])
|
|
1158
|
+
const reached: RuntimeState[] = [init]
|
|
1159
|
+
const queue: RuntimeState[] = [init]
|
|
1160
|
+
let steps = 0
|
|
1161
|
+
while (queue.length > 0) {
|
|
1162
|
+
if (++steps > options.maxStates) break
|
|
1163
|
+
const current = queue.shift()!
|
|
1164
|
+
for (const event of allEvents(model)) {
|
|
1165
|
+
for (const next of stepRuntime(model, current, event)) {
|
|
1166
|
+
const key = runtimeKey(next)
|
|
1167
|
+
if (visited.has(key)) continue
|
|
1168
|
+
visited.add(key)
|
|
1169
|
+
reached.push(next)
|
|
1170
|
+
queue.push(next)
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return reached
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Violation of a path-property invariant, or undefined when it holds. Each kind
|
|
1179
|
+
* delegates to the search its dedicated probe uses (A9 leads-to, A10 sequence, A11
|
|
1180
|
+
* atomicity, A12 budget, A13 probability), so a verdict reported here can never
|
|
1181
|
+
* disagree with the probe that owns the kind.
|
|
1182
|
+
*/
|
|
1183
|
+
function pathPropertyViolation(model: LogicModelV1, options: NormalizedOptions, invariant: InvariantSpec): InvariantViolation | undefined {
|
|
1184
|
+
if (invariant.kind === 'leads-to') {
|
|
1185
|
+
const targets = new Set(leadsToTargets(invariant))
|
|
1186
|
+
const label = leadsToTargetLabel(invariant)
|
|
1187
|
+
for (const runtime of reachableRuntimeStates(model, options)) {
|
|
1188
|
+
if (runtime.state !== invariant.from) continue
|
|
1189
|
+
const bad = findLeadsToBadPath(model, runtime, targets, label)
|
|
1190
|
+
if (bad !== undefined) return { invariant, path: bad.path, reason: bad.reason }
|
|
1191
|
+
}
|
|
1192
|
+
return undefined
|
|
1193
|
+
}
|
|
1194
|
+
if (invariant.kind === 'sequence') return findSequenceViolation(model, options, invariant)
|
|
1195
|
+
if (invariant.kind === 'atomicity') return findAtomicityViolation(model, options, invariant)
|
|
1196
|
+
if (invariant.kind === 'budget') {
|
|
1197
|
+
const violation = findBudgetViolation(model, options, invariant)
|
|
1198
|
+
if (violation === undefined) return undefined
|
|
1199
|
+
return {
|
|
1200
|
+
invariant,
|
|
1201
|
+
path: violation.path,
|
|
1202
|
+
reason: violation.unbounded
|
|
1203
|
+
? 'a reachable cycle keeps accumulating cost, so no finite budget holds'
|
|
1204
|
+
: 'a run accumulates cost ' + String(violation.cost) + ', above the declared budget ' + String(invariant.budget),
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (invariant.kind === 'probability') {
|
|
1208
|
+
const { probability, converged } = computeHitProbability(model, options, invariant.target)
|
|
1209
|
+
if (!probabilityViolated(invariant, probability)) return undefined
|
|
1210
|
+
return {
|
|
1211
|
+
invariant,
|
|
1212
|
+
path: [],
|
|
1213
|
+
reason: 'P(hit ' + invariant.target + ') = ' + probability.toFixed(6) + ' does not satisfy ' + invariant.op + ' ' + String(invariant.p)
|
|
1214
|
+
+ (converged ? '' : ' (value iteration did not converge)'),
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return undefined
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1111
1220
|
function shortestEventBeforeStateViolation(model: LogicModelV1, options: NormalizedOptions, invariant: Extract<InvariantSpec, { kind: 'event-before-state' }>): InvariantViolation | undefined {
|
|
1112
1221
|
const init = initialState(model)
|
|
1113
1222
|
const seenInitially = false
|
|
@@ -1138,10 +1247,12 @@ function shortestEventBeforeStateViolation(model: LogicModelV1, options: Normali
|
|
|
1138
1247
|
return undefined
|
|
1139
1248
|
}
|
|
1140
1249
|
|
|
1141
|
-
function runInvariants(model: LogicModelV1, options: NormalizedOptions): InvariantViolation[] {
|
|
1250
|
+
function runInvariants(model: LogicModelV1, options: NormalizedOptions, includePathProperties = false): InvariantViolation[] {
|
|
1142
1251
|
const violations: InvariantViolation[] = []
|
|
1143
1252
|
for (const invariant of model.invariants ?? []) {
|
|
1144
|
-
const violation =
|
|
1253
|
+
const violation = includePathProperties && isPathPropertyInvariant(invariant)
|
|
1254
|
+
? pathPropertyViolation(model, options, invariant)
|
|
1255
|
+
: shortestViolationForInvariant(model, options, invariant)
|
|
1145
1256
|
if (violation !== undefined) violations.push(violation)
|
|
1146
1257
|
}
|
|
1147
1258
|
return violations
|
|
@@ -1507,14 +1618,27 @@ function A6_resourceInjection(model: LogicModelV1): CheckResult {
|
|
|
1507
1618
|
}
|
|
1508
1619
|
|
|
1509
1620
|
function A7_shortestViolations(model: LogicModelV1, options: NormalizedOptions): CheckResult {
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1621
|
+
// Path-property invariants are included: "for any invariant that fails" is this
|
|
1622
|
+
// check's contract, and a check that silently skips five of the eight kinds cannot
|
|
1623
|
+
// honour it. The searches for the state kinds, for sequence, atomicity and budget
|
|
1624
|
+
// are breadth-first, so their first witness is the shortest one; the leads-to walk
|
|
1625
|
+
// and the probability bound have no such guarantee, and their message says what it
|
|
1626
|
+
// found instead of claiming minimality.
|
|
1627
|
+
const violations = runInvariants(model, options, true)
|
|
1628
|
+
const findings: Finding[] = violations.map((violation) => {
|
|
1629
|
+
const pathLength = violation.path.length
|
|
1630
|
+
const message = isPathPropertyInvariant(violation.invariant)
|
|
1631
|
+
? 'Invariant "' + violation.invariant.id + '" violated: ' + violation.reason
|
|
1632
|
+
+ (pathLength === 0 ? '' : ' (witness path length: ' + pathLength + ')')
|
|
1633
|
+
: 'Invariant "' + violation.invariant.id + '" shortest violating path length: ' + pathLength + (pathLength === 0 ? ' (initial state)' : '')
|
|
1634
|
+
return {
|
|
1635
|
+
code: 'A7_SHORTEST_COUNTEREXAMPLE',
|
|
1636
|
+
severity: 'warning' as const,
|
|
1637
|
+
message,
|
|
1638
|
+
path: violation.path,
|
|
1639
|
+
evidence: { invariant: violation.invariant.id },
|
|
1640
|
+
}
|
|
1641
|
+
})
|
|
1518
1642
|
return checkResult('A7', 'Minimal Counter-Example', findings, violations.length === 0 ? 'All invariants hold for all reachable paths' : 'Violated invariants: ' + findings.length)
|
|
1519
1643
|
}
|
|
1520
1644
|
|
|
@@ -1595,6 +1719,25 @@ function mapInvariantForComparison(invariant: InvariantSpec, mapping: Record<str
|
|
|
1595
1719
|
}
|
|
1596
1720
|
return mapped
|
|
1597
1721
|
}
|
|
1722
|
+
if (invariant.kind === 'leads-to') {
|
|
1723
|
+
// A target set maps member by member; a rename that drops a state is reported by
|
|
1724
|
+
// D2's own mapped-state check rather than silently changing the property.
|
|
1725
|
+
return {
|
|
1726
|
+
...invariant,
|
|
1727
|
+
id: invariant.id + ':before',
|
|
1728
|
+
description: invariant.description + ' (from BEFORE)',
|
|
1729
|
+
from: mapStateId(mapping, invariant.from),
|
|
1730
|
+
to: typeof invariant.to === 'string' ? mapStateId(mapping, invariant.to) : invariant.to.map((state) => mapStateId(mapping, state)),
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
if (invariant.kind === 'probability') {
|
|
1734
|
+
return {
|
|
1735
|
+
...invariant,
|
|
1736
|
+
id: invariant.id + ':before',
|
|
1737
|
+
description: invariant.description + ' (from BEFORE)',
|
|
1738
|
+
target: mapStateId(mapping, invariant.target),
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1598
1741
|
return { ...invariant, id: invariant.id + ':before', description: invariant.description + ' (from BEFORE)' }
|
|
1599
1742
|
}
|
|
1600
1743
|
|
|
@@ -1634,8 +1777,36 @@ function D2_invariantContinuity(before: LogicModelV1, after: LogicModelV1, optio
|
|
|
1634
1777
|
})
|
|
1635
1778
|
continue
|
|
1636
1779
|
}
|
|
1780
|
+
} else if (mapped.kind === 'leads-to') {
|
|
1781
|
+
// The state references of a path-property invariant must follow stateMapping too,
|
|
1782
|
+
// otherwise a pure rename searches for a state AFTER does not declare.
|
|
1783
|
+
for (const state of [mapped.from, ...leadsToTargets(mapped)]) {
|
|
1784
|
+
if (!afterStates.has(state)) {
|
|
1785
|
+
findings.push({
|
|
1786
|
+
code: 'D2_MAPPED_STATE_MISSING',
|
|
1787
|
+
severity: 'warning',
|
|
1788
|
+
message: 'BEFORE invariant "' + invariant.id + '" maps to state ' + state + ', which is not declared in AFTER.',
|
|
1789
|
+
evidence: { invariant: invariant.id, state },
|
|
1790
|
+
})
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
} else if (mapped.kind === 'probability') {
|
|
1794
|
+
if (!afterStates.has(mapped.target)) {
|
|
1795
|
+
findings.push({
|
|
1796
|
+
code: 'D2_MAPPED_STATE_MISSING',
|
|
1797
|
+
severity: 'warning',
|
|
1798
|
+
message: 'BEFORE invariant "' + invariant.id + '" maps to state ' + mapped.target + ', which is not declared in AFTER.',
|
|
1799
|
+
evidence: { invariant: invariant.id, state: mapped.target },
|
|
1800
|
+
})
|
|
1801
|
+
}
|
|
1637
1802
|
}
|
|
1638
|
-
|
|
1803
|
+
// Every invariant the engine can decide is re-decided against AFTER. Path-property
|
|
1804
|
+
// kinds go through the same searches their dedicated probes use, so D2 can no
|
|
1805
|
+
// longer report "all BEFORE invariants continue to hold" while A9-A13 fail on the
|
|
1806
|
+
// very same model.
|
|
1807
|
+
const violation = isPathPropertyInvariant(mapped)
|
|
1808
|
+
? pathPropertyViolation(after, options, mapped)
|
|
1809
|
+
: shortestViolationForInvariant(after, options, mapped)
|
|
1639
1810
|
if (violation !== undefined) {
|
|
1640
1811
|
findings.push({
|
|
1641
1812
|
code: 'D2_INVARIANT_REGRESSION',
|
|
@@ -1825,15 +1996,29 @@ function S8_monotonicVariables(model: LogicModelV1): CheckResult {
|
|
|
1825
1996
|
}
|
|
1826
1997
|
|
|
1827
1998
|
/**
|
|
1828
|
-
*
|
|
1829
|
-
*
|
|
1830
|
-
|
|
1999
|
+
* Target states of a `leads-to` invariant. The schema accepts one state id or a
|
|
2000
|
+
* non-empty array of them, so "reach any of these" needs no separate invariant kind.
|
|
2001
|
+
*/
|
|
2002
|
+
function leadsToTargets(invariant: Extract<InvariantSpec, { kind: 'leads-to' }>): string[] {
|
|
2003
|
+
return typeof invariant.to === 'string' ? [invariant.to] : invariant.to
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
/** Target list as findings print it. A single target renders as its bare id, so existing messages stay byte-identical. */
|
|
2007
|
+
function leadsToTargetLabel(invariant: Extract<InvariantSpec, { kind: 'leads-to' }>): string {
|
|
2008
|
+
return leadsToTargets(invariant).join(', ')
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
/**
|
|
2012
|
+
* Find a run from `start` that reaches none of `targets`, or return undefined when
|
|
2013
|
+
* every run reaches at least one of them. The property is universal (see the A9 row
|
|
2014
|
+
* in `SKILL.md`): one branch that loops forever, or that stops before any target,
|
|
2015
|
+
* refutes it.
|
|
1831
2016
|
*
|
|
1832
|
-
* The search is a depth-first walk of the run graph with three colours, and
|
|
1833
|
-
*
|
|
2017
|
+
* The search is a depth-first walk of the run graph with three colours, and runs at a
|
|
2018
|
+
* target are success leaves that are never expanded:
|
|
1834
2019
|
*
|
|
1835
2020
|
* - a node reached while it is on the current walk (GRAY) closes a cycle that avoids
|
|
1836
|
-
*
|
|
2021
|
+
* every target, so some run never reaches one;
|
|
1837
2022
|
* - a node with no outgoing step at all stops the machine where it stands, which is
|
|
1838
2023
|
* the same violation for a different reason;
|
|
1839
2024
|
* - a node whose walk completed without a violation is BLACK, and reaching it again
|
|
@@ -1844,8 +2029,8 @@ function S8_monotonicVariables(model: LogicModelV1): CheckResult {
|
|
|
1844
2029
|
* A run is identified by its state plus its variable values, so the same state id
|
|
1845
2030
|
* with different values is a different node.
|
|
1846
2031
|
*/
|
|
1847
|
-
function findLeadsToBadPath(model: LogicModelV1, start: RuntimeState,
|
|
1848
|
-
if (start.state
|
|
2032
|
+
function findLeadsToBadPath(model: LogicModelV1, start: RuntimeState, targets: Set<string>, label: string): { path: PathStep[]; reason: string } | undefined {
|
|
2033
|
+
if (targets.has(start.state)) return undefined
|
|
1849
2034
|
const GRAY = 1
|
|
1850
2035
|
const BLACK = 2
|
|
1851
2036
|
interface LeadFrame {
|
|
@@ -1866,7 +2051,7 @@ function findLeadsToBadPath(model: LogicModelV1, start: RuntimeState, target: st
|
|
|
1866
2051
|
for (const next of stepRuntime(model, frame.runtime, event)) nexts.push({ next, event })
|
|
1867
2052
|
}
|
|
1868
2053
|
frame.nexts = nexts
|
|
1869
|
-
if (nexts.length === 0) return { path: frame.path, reason: 'Dead end before target ' +
|
|
2054
|
+
if (nexts.length === 0) return { path: frame.path, reason: 'Dead end before target ' + label }
|
|
1870
2055
|
}
|
|
1871
2056
|
if (frame.index >= frame.nexts.length) {
|
|
1872
2057
|
color.set(runtimeKey(frame.runtime), BLACK)
|
|
@@ -1876,15 +2061,15 @@ function findLeadsToBadPath(model: LogicModelV1, start: RuntimeState, target: st
|
|
|
1876
2061
|
const { next, event } = frame.nexts[frame.index]
|
|
1877
2062
|
frame.index += 1
|
|
1878
2063
|
const step: PathStep = { from: frame.runtime.state, event, to: next.state }
|
|
1879
|
-
if (next.state
|
|
2064
|
+
if (targets.has(next.state)) continue
|
|
1880
2065
|
const key = runtimeKey(next)
|
|
1881
2066
|
const seen = color.get(key)
|
|
1882
|
-
if (seen === GRAY) return { path: [...frame.path, step], reason: 'Cycle avoids target ' +
|
|
2067
|
+
if (seen === GRAY) return { path: [...frame.path, step], reason: 'Cycle avoids target ' + label }
|
|
1883
2068
|
if (seen === BLACK) continue
|
|
1884
2069
|
color.set(key, GRAY)
|
|
1885
2070
|
stack.push({ runtime: next, path: [...frame.path, step], nexts: undefined, index: 0 })
|
|
1886
2071
|
}
|
|
1887
|
-
// Every branch either reached
|
|
2072
|
+
// Every branch either reached a target or rejoined a branch that did.
|
|
1888
2073
|
return undefined
|
|
1889
2074
|
}
|
|
1890
2075
|
|
|
@@ -1892,9 +2077,11 @@ function A9_leadsTo(model: LogicModelV1, exploration: Exploration): CheckResult
|
|
|
1892
2077
|
const findings: Finding[] = []
|
|
1893
2078
|
for (const invariant of model.invariants ?? []) {
|
|
1894
2079
|
if (invariant.kind !== 'leads-to') continue
|
|
2080
|
+
const targets = new Set(leadsToTargets(invariant))
|
|
2081
|
+
const label = leadsToTargetLabel(invariant)
|
|
1895
2082
|
for (const runtime of exploration.reachable) {
|
|
1896
2083
|
if (runtime.state !== invariant.from) continue
|
|
1897
|
-
const bad = findLeadsToBadPath(model, runtime,
|
|
2084
|
+
const bad = findLeadsToBadPath(model, runtime, targets, label)
|
|
1898
2085
|
if (bad !== undefined) {
|
|
1899
2086
|
findings.push({
|
|
1900
2087
|
code: 'A9_LEADS_TO_VIOLATION',
|
|
@@ -2318,16 +2505,10 @@ function computeHitProbability(model: LogicModelV1, options: NormalizedOptions,
|
|
|
2318
2505
|
|
|
2319
2506
|
function A13_probability(model: LogicModelV1, options: NormalizedOptions): CheckResult {
|
|
2320
2507
|
const findings: Finding[] = []
|
|
2321
|
-
const eps = 1e-9
|
|
2322
2508
|
for (const invariant of model.invariants ?? []) {
|
|
2323
2509
|
if (invariant.kind !== 'probability') continue
|
|
2324
2510
|
const { probability, converged } = computeHitProbability(model, options, invariant.target)
|
|
2325
|
-
|
|
2326
|
-
if (invariant.op === '>=') violated = probability < invariant.p - eps
|
|
2327
|
-
else if (invariant.op === '>') violated = probability <= invariant.p + eps
|
|
2328
|
-
else if (invariant.op === '<=') violated = probability > invariant.p + eps
|
|
2329
|
-
else violated = probability >= invariant.p - eps
|
|
2330
|
-
if (violated) {
|
|
2511
|
+
if (probabilityViolated(invariant, probability)) {
|
|
2331
2512
|
findings.push({
|
|
2332
2513
|
code: 'A13_PROBABILITY_VIOLATION',
|
|
2333
2514
|
severity: 'error',
|
package/src/index.ts
CHANGED
|
@@ -75,7 +75,7 @@ export type InteractionMode = 'ask' | 'auto' | 'follow-approval'
|
|
|
75
75
|
const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
|
|
76
76
|
Plugin logicprobe is active. Documents are not truth — code is. Verify every verifiable claim before accepting or acting on any design.
|
|
77
77
|
|
|
78
|
-
**1% Rule**: If there is even a 1% chance the logicprobe skill applies — reviewing design documents, architecture specs, technical proposals, or refactoring plans that make claims about API names, file locations, enum values, mechanism feasibility, state machines, protocol logic, data models, schema migrations, data invariants,
|
|
78
|
+
**1% Rule**: If there is even a 1% chance the logicprobe skill applies — reviewing design documents, architecture specs, technical proposals, or refactoring plans that make claims about API names, file locations, enum values, mechanism feasibility, state machines, protocol logic, data models, schema migrations, data invariants, behavioral guarantees ("always"/"never"/"guaranteed"), worst-case path cost or declared budgets, probabilistic reachability ("≥90% of runs reach SAFE"), deadlines ("must leave within 2 ticks"), cross-machine handshakes, or concurrency guarantees ("thread-safe"/"lock-free"/"ISR-safe") — load it with the skill tool before responding. The cost of loading is trivial compared to the cost of a false claim.
|
|
79
79
|
|
|
80
80
|
**Red Flags** — if you think any of these, STOP. You are rationalizing:
|
|
81
81
|
|
|
@@ -86,7 +86,16 @@ Plugin logicprobe is active. Documents are not truth — code is. Verify every v
|
|
|
86
86
|
| "I'll verify while implementing" | Verification happens before implementation, not during. |
|
|
87
87
|
| "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
|
|
88
88
|
|
|
89
|
-
**Native verification path
|
|
89
|
+
**Native verification path** (dsh) — six model-invocable tools:
|
|
90
|
+
|
|
91
|
+
- \`logicprobe_verify\` — state machines: S1-S8 structural + A1-A14 adversarial, including A12 worst-case path cost against a declared \`budget\`, A13 probabilistic reachability over a DTMC, and A14 discrete-tick deadlines. Pass \`beforeModel\` + \`stateMapping\` to add the D1-D4 before/after regression.
|
|
92
|
+
- \`logicprobe_datamodel_verify\` — data models and schema migrations: DS/DA checks, migration coverage, copy consistency, rollback symmetry, DD1-DD4 data regression.
|
|
93
|
+
- \`logicprobe_compose_verify\` — two or more machines checked together (rendezvous handshake semantics): C1 composition deadlock, C2 rendezvous that never fires.
|
|
94
|
+
- \`logicprobe_concurrency_scan\` — mine concurrency claims (thread-safe, lock-free, race condition, mutex, ISR-safe) and route them to dedicated verification; logicprobe does not prove concurrency safety.
|
|
95
|
+
- \`logicprobe_export\` — emit external-checker input from a verified model: UPPAAL, TLA+, PRISM, SPIN.
|
|
96
|
+
- \`logicprobe_uml\` — model a code flow as UML (render), read a diagram back into a model (parse), or audit the modelling (review: UML001-UML019 structural defects, documentation gaps, diagram-versus-model round-trip fidelity).
|
|
97
|
+
|
|
98
|
+
Python harnesses in the skill references remain the fallback for non-dsh hosts.
|
|
90
99
|
|
|
91
100
|
**Proactive suggestion**: When a user asks code-level behavioral questions — "could this state machine deadlock", "is this retry limit safe", "check this timing sequence for bugs", "is this migration non-breaking", "does this copy cover all required fields" — suggest logicprobe as an optional verification pass (do not auto-escalate).
|
|
92
101
|
</EXTREMELY_IMPORTANT>`
|
|
@@ -211,7 +220,7 @@ function resolveInteraction(config: Config, session: Session): 'ask' | 'auto' {
|
|
|
211
220
|
function modeContextText(config: Config, session: Session): string {
|
|
212
221
|
const interaction = resolveInteraction(config, session)
|
|
213
222
|
const lines = [
|
|
214
|
-
'logicprobe:
|
|
223
|
+
'logicprobe: native tools — `logicprobe_verify` (state machines: S1-S8 + A1-A14, incl. A12 budget/worst-case path cost, A13 probabilistic reachability, A14 deadlines; `beforeModel` adds D1-D4 regression), `logicprobe_datamodel_verify` (data models and schema migrations: DS/DA, migration coverage, copy consistency, rollback symmetry, DD1-DD4), `logicprobe_compose_verify` (two or more machines: C1 composition deadlock, C2 rendezvous never fires), `logicprobe_concurrency_scan` (mine concurrency claims — it does not prove concurrency safety), `logicprobe_export` (UPPAAL, TLA+, PRISM, SPIN input from a verified model).',
|
|
215
224
|
'logicprobe: use `logicprobe_uml` to model a code flow as UML (render), to read a UML diagram back into a model (parse), or to audit the modelling (review: structural defects, documentation gaps, diagram-vs-model round-trip fidelity).',
|
|
216
225
|
interaction === 'auto'
|
|
217
226
|
? 'logicprobe interaction=auto: do NOT call ask_user_question for model confirmation; run round-trip validation of the extracted transition table and mark the result UNCONFIRMED.'
|
package/src/tool.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
2
|
import type { JsonValue } from './json-value.js'
|
|
3
3
|
import { runVerification } from './engine.js'
|
|
4
4
|
|
|
@@ -16,7 +16,7 @@ export const LOGICPROBE_VERIFY_TOOL_NAME = 'logicprobe_verify'
|
|
|
16
16
|
export const logicProbeVerifyTool = defineTool({
|
|
17
17
|
name: LOGICPROBE_VERIFY_TOOL_NAME,
|
|
18
18
|
description:
|
|
19
|
-
'Run executable state-machine verification (logicprobe). Takes a LogicModelV1 object with schemaVersion=1, init, states ({id, terminal?}), transitions ({from, event, to, guard?, updates?, cost?}), variables?, invariants?, concurrentPairs?, boundaryChecks?, resourcePairs?, idempotentEvents?, narrative?. Guards are structured ({variable, op, value} | {all} | {any} | {not}); invariants support never-states, var-in-range ({variable, min?, max?, when?} — when scopes the range to a state ({state}) or a guard node, is evaluated on the post-state of every transition, leaves the initial state checked unconditionally, and is rejected on any other invariant kind), event-before-state, leads-to, sequence, atomicity, budget (transition cost defaults to 1; A12 checks every reachable path stays within the declared budget and reports the shortest over-budget counterexample), and probability (transition weight, default 1, makes the model a DTMC; A13 checks P(hit target) against the bound by value iteration). The schema is closed: an undeclared key on any model part (model, state, transition, update, variable, invariant, boundaryCheck, resourcePair, narrative, scenario) is a validation error, never ignored. Variables support monotonic inc/dec. The optional narrative block carries natural-language descriptions of states (narrative.states), events (narrative.events), and (state, event) scenarios (narrative.scenarios: [{from, event, scenario}]); when present it must fully cover the model and is echoed in the report. Returns a report with S1-S8 structural checks and A1-
|
|
19
|
+
'Run executable state-machine verification (logicprobe). Takes a LogicModelV1 object with schemaVersion=1, init, states ({id, terminal?}), transitions ({from, event, to, guard?, updates?, cost?}), variables?, invariants?, concurrentPairs?, boundaryChecks?, resourcePairs?, idempotentEvents?, narrative?. Guards are structured ({variable, op, value} | {all} | {any} | {not}); invariants support never-states, var-in-range ({variable, min?, max?, when?} — when scopes the range to a state ({state}) or a guard node, is evaluated on the post-state of every transition, leaves the initial state checked unconditionally, and is rejected on any other invariant kind), event-before-state, leads-to (`to` is one state id or a non-empty array of them: every path must reach at least one member), sequence, atomicity, budget (transition cost defaults to 1; A12 checks every reachable path stays within the declared budget and reports the shortest over-budget counterexample), and probability (transition weight, default 1, makes the model a DTMC; A13 checks P(hit target) against the bound by value iteration), and deadline (declaring maxTicks on a state is how many tick steps it may stay resident and the top-level tickEvents list names those steps; A14 reports the over-residency path, and A14_NO_TICK_EVENTS when maxTicks is declared without tickEvents). The schema is closed: an undeclared key on any model part (model, state, transition, update, variable, invariant, boundaryCheck, resourcePair, narrative, scenario) is a validation error, never ignored. Variables support monotonic inc/dec. The optional narrative block carries natural-language descriptions of states (narrative.states), events (narrative.events), and (state, event) scenarios (narrative.scenarios: [{from, event, scenario}]); when present it must fully cover the model and is echoed in the report. Returns a report with S1-S8 structural checks and A1-A14 adversarial, cost, probability and deadline probes, including counterexample paths (shortest where the search is breadth-first). If beforeModel is provided, also runs D1-D4 before/after regression checks. See skills/logicprobe/references/dsh-model-schema.md.',
|
|
20
20
|
parameters: {
|
|
21
21
|
model: {
|
|
22
22
|
type: 'json',
|