dsh-logicprobe 0.5.5 → 0.6.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 +6 -3
- package/README.md +5 -2
- package/lib/compose-tool.js +43 -0
- package/lib/concurrency.js +11 -1
- package/lib/engine.js +2449 -1699
- package/lib/export-tool.js +45 -0
- package/lib/exporters.js +389 -0
- package/lib/index.js +48 -25
- package/lib/tool.js +58 -57
- package/lib/types/compose-tool.d.ts +6 -0
- package/lib/types/concurrency.d.ts +2 -0
- package/lib/types/engine.d.ts +255 -189
- package/lib/types/export-tool.d.ts +7 -0
- package/lib/types/exporters.d.ts +17 -0
- package/lib/types/tool.d.ts +5 -4
- package/package.json +106 -102
- package/skills/logicprobe/SKILL.md +14 -8
- package/skills/logicprobe/references/concurrency-risk-guide.md +2 -0
- package/skills/logicprobe/references/dsh-model-schema.md +266 -204
- package/skills/logicprobe/references/gap-routing-guide.md +36 -0
- package/skills/logicprobe/references/logic-verification-guide.md +56 -4
- package/skills/logicprobe/references/logicprobe-engine.py +3028 -0
- package/skills/logicprobe/references/verification-harness.py +153 -0
- package/src/compose-tool.ts +46 -0
- package/src/concurrency.ts +13 -1
- package/src/engine.ts +2568 -1778
- package/src/export-tool.ts +47 -0
- package/src/exporters.ts +347 -0
- package/src/index.ts +349 -315
- package/src/tool.ts +61 -60
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { exportModel } from './exporters.js';
|
|
3
|
+
export const LOGICPROBE_EXPORT_TOOL_NAME = 'logicprobe_export';
|
|
4
|
+
/**
|
|
5
|
+
* DSH tool wrapping the external-tool exporters: turn a LogicModelV1 machine into
|
|
6
|
+
* native input for the tool logicprobe routes to (UPPAAL / TLA+ / PRISM / SPIN).
|
|
7
|
+
* v1 translates the core machine; unrepresentable invariants become warnings.
|
|
8
|
+
*/
|
|
9
|
+
export const logicProbeExportTool = defineTool({
|
|
10
|
+
name: LOGICPROBE_EXPORT_TOOL_NAME,
|
|
11
|
+
description: 'Export a LogicModelV1 state machine into native input for an external verification tool (logicprobe routing). format is one of uppaal (.xta + queries), tla (TLC spec + safety property), prism (.pm + .pctl), spin (Promela + ltl). Booleans are exported as 0/1 integers; invariants the target cannot express are returned as warnings, never silently dropped. Returns { ok, format, primary, extras?, warnings? } or { ok: false, error } when the model is invalid or the PRISM enumeration is too large.',
|
|
12
|
+
parameters: {
|
|
13
|
+
model: {
|
|
14
|
+
type: 'json',
|
|
15
|
+
required: true,
|
|
16
|
+
description: 'LogicModelV1 state-machine model to export.',
|
|
17
|
+
},
|
|
18
|
+
format: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
required: true,
|
|
21
|
+
enum: ['uppaal', 'tla', 'prism', 'spin'],
|
|
22
|
+
description: 'Target tool format.',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
output: {
|
|
26
|
+
schema: {
|
|
27
|
+
type: 'json',
|
|
28
|
+
description: 'Export result with generated file content.',
|
|
29
|
+
},
|
|
30
|
+
render(_args, value) {
|
|
31
|
+
return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
timeoutMs: 10_000,
|
|
35
|
+
isConcurrencySafe: () => true,
|
|
36
|
+
async execute(args) {
|
|
37
|
+
try {
|
|
38
|
+
const result = exportModel(args.model, args.format);
|
|
39
|
+
return { ok: true, format: result.format, primary: result.primary, ...(result.extras === undefined ? {} : { extras: result.extras }), warnings: result.warnings };
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
});
|
package/lib/exporters.js
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// External-tool exporters: turn a validated LogicModelV1 into native input for the
|
|
2
|
+
// tools logicprobe routes to (see skills/logicprobe/references/gap-routing-guide.md).
|
|
3
|
+
// v1 translates the CORE machine (states, events, transitions, integer/boolean guards
|
|
4
|
+
// and updates). Constructs a target cannot express are emitted as warnings/comments,
|
|
5
|
+
// never silently dropped. Booleans are exported as 0/1 integers everywhere.
|
|
6
|
+
import { validateModel } from './engine.js';
|
|
7
|
+
export function prepareModel(input) {
|
|
8
|
+
const validation = validateModel(input);
|
|
9
|
+
if (!validation.ok)
|
|
10
|
+
throw new Error('model invalid: ' + validation.errors.join('; '));
|
|
11
|
+
const model = validation.model;
|
|
12
|
+
const used = new Set();
|
|
13
|
+
const stateIds = [];
|
|
14
|
+
const indexOf = new Map();
|
|
15
|
+
const stateIdOf = new Map();
|
|
16
|
+
const sanitize = (raw) => {
|
|
17
|
+
let out = raw.replace(/[^A-Za-z0-9_]/g, '_');
|
|
18
|
+
if (out.length === 0 || /^[0-9]/.test(out))
|
|
19
|
+
out = 's_' + out;
|
|
20
|
+
return out;
|
|
21
|
+
};
|
|
22
|
+
model.states.forEach((state, index) => {
|
|
23
|
+
let candidate = sanitize(state.id);
|
|
24
|
+
if (used.has(candidate))
|
|
25
|
+
candidate = candidate + '_' + index;
|
|
26
|
+
used.add(candidate);
|
|
27
|
+
stateIds.push(candidate);
|
|
28
|
+
indexOf.set(state.id, index);
|
|
29
|
+
stateIdOf.set(state.id, candidate);
|
|
30
|
+
});
|
|
31
|
+
return { model, stateIds, indexOf, initId: stateIdOf.get(model.init) ?? stateIds[0], stateIdOf };
|
|
32
|
+
}
|
|
33
|
+
function numValue(value) {
|
|
34
|
+
return typeof value === 'boolean' ? (value ? 1 : 0) : value;
|
|
35
|
+
}
|
|
36
|
+
function leafExpr(node, varName) {
|
|
37
|
+
return varName(node.variable) + ' ' + node.op + ' ' + String(numValue(node.value));
|
|
38
|
+
}
|
|
39
|
+
function guardExpr(node, and, or, not, varName, opFor = (op) => op) {
|
|
40
|
+
if (node === undefined)
|
|
41
|
+
return '';
|
|
42
|
+
if ('variable' in node)
|
|
43
|
+
return varName(node.variable) + ' ' + opFor(node.op) + ' ' + String(numValue(node.value));
|
|
44
|
+
if ('all' in node)
|
|
45
|
+
return '(' + node.all.map((g) => guardExpr(g, and, or, not, varName, opFor)).join(' ' + and + ' ') + ')';
|
|
46
|
+
if ('any' in node)
|
|
47
|
+
return '(' + node.any.map((g) => guardExpr(g, and, or, not, varName, opFor)).join(' ' + or + ' ') + ')';
|
|
48
|
+
if ('not' in node)
|
|
49
|
+
return not + '(' + guardExpr(node.not, and, or, not, varName, opFor) + ')';
|
|
50
|
+
return 'true';
|
|
51
|
+
}
|
|
52
|
+
function updateAssignments(updates, varName) {
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const update of updates ?? []) {
|
|
55
|
+
const name = varName(update.variable);
|
|
56
|
+
const value = update.value ?? 1;
|
|
57
|
+
if (update.op === 'set')
|
|
58
|
+
out.push(name + ' := ' + String(value));
|
|
59
|
+
else if (update.op === 'inc')
|
|
60
|
+
out.push(name + ' := ' + name + ' + ' + String(value));
|
|
61
|
+
else
|
|
62
|
+
out.push(name + ' := ' + name + ' - ' + String(value));
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function forbiddenIndexes(ex) {
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const invariant of ex.model.invariants ?? []) {
|
|
69
|
+
if (invariant.kind === 'never-states') {
|
|
70
|
+
for (const s of invariant.states) {
|
|
71
|
+
const index = ex.indexOf.get(s);
|
|
72
|
+
if (index !== undefined)
|
|
73
|
+
out.push(index);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------- UPPAAL ----
|
|
80
|
+
// UPPAAL .xta is XML (flat-1_6 DTD): nta/template/location/transition with label
|
|
81
|
+
// kinds guard | assignment | synchronisation. Queries are plain text (one per line).
|
|
82
|
+
function exportUppaal(ex) {
|
|
83
|
+
const { model, stateIds, indexOf, initId } = ex;
|
|
84
|
+
const warnings = [];
|
|
85
|
+
const xml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
86
|
+
const safeEvent = (event) => xml(event.replace(/[^A-Za-z0-9_]/g, '_'));
|
|
87
|
+
const globals = (model.variables ?? []).map((v) => 'int ' + v.name + ' = ' + String(numValue(v.init)) + ';').join('\n');
|
|
88
|
+
const locs = model.states.map((state, i) => {
|
|
89
|
+
const nameEl = '\n <name x=\"16\" y=\"16\">' + xml(stateIdOf(ex, state.id)) + '</name>';
|
|
90
|
+
return ' <location id=\"id' + i + '\" x=\"' + (i * 120) + '\" y=\"0\">' + nameEl + '\n </location>';
|
|
91
|
+
}).join('\n');
|
|
92
|
+
const edges = model.transitions.map((tr, i) => {
|
|
93
|
+
const guard = guardExpr(tr.guard, '&&', '||', '!', (v) => v);
|
|
94
|
+
const assigns = updateAssignments(tr.updates, (v) => v);
|
|
95
|
+
const labels = [];
|
|
96
|
+
if (guard !== '')
|
|
97
|
+
labels.push(' <label kind=\"guard\" x=\"16\" y=\"16\">' + xml(guard) + '</label>');
|
|
98
|
+
if (assigns.length > 0)
|
|
99
|
+
labels.push(' <label kind=\"assignment\" x=\"16\" y=\"16\">' + xml(assigns.join(',\n')) + '</label>');
|
|
100
|
+
labels.push(' <label kind=\"synchronisation\" x=\"16\" y=\"16\">' + safeEvent(tr.event) + '!</label>');
|
|
101
|
+
return ' <transition id=\"id' + String(1000 + i) + '\">\n <source ref=\"id' + String(indexOf.get(tr.from)) + '\"/>\n <target ref=\"id' + String(indexOf.get(tr.to)) + '\"/>\n' + labels.join('\n') + '\n </transition>';
|
|
102
|
+
}).join('\n');
|
|
103
|
+
const queries = [];
|
|
104
|
+
for (const state of model.states)
|
|
105
|
+
if (state.terminal)
|
|
106
|
+
queries.push('E<> LogicProbe.' + stateIdOf(ex, state.id));
|
|
107
|
+
for (const index of forbiddenIndexes(ex))
|
|
108
|
+
queries.push('A[] not LogicProbe.' + stateIds[index]);
|
|
109
|
+
for (const invariant of model.invariants ?? []) {
|
|
110
|
+
if (invariant.kind !== 'never-states')
|
|
111
|
+
warnings.push('UPPAAL export carries invariant kind ' + invariant.kind + ' only as a comment (not expressible in the exported XML).');
|
|
112
|
+
}
|
|
113
|
+
const initIndex = indexOf.get(model.init) ?? 0;
|
|
114
|
+
const xta = '<?xml version="1.0" encoding="utf-8"?>\n' +
|
|
115
|
+
'<!DOCTYPE nta PUBLIC \'-//Uppaal Team//DTD Flat System 1.6//EN\' \'http://www.it.uu.se/research/group/darts/uppaal/flat-1_6.dtd\'>\n' +
|
|
116
|
+
'<nta>\n' +
|
|
117
|
+
' <declaration>// Generated by logicprobe (UPPAAL XML export; booleans as int).\n' + (globals === '' ? '' : globals + '\n') + '</declaration>\n' +
|
|
118
|
+
' <template>\n' +
|
|
119
|
+
' <name x=\"5\" y=\"5\">LogicProbe</name>\n' +
|
|
120
|
+
' <declaration/>\n' +
|
|
121
|
+
locs + '\n' +
|
|
122
|
+
' <init ref=\"id' + String(initIndex) + '\"/>\n' +
|
|
123
|
+
edges + '\n' +
|
|
124
|
+
' </template>\n' +
|
|
125
|
+
' <system>system LogicProbe;</system>\n' +
|
|
126
|
+
'</nta>\n';
|
|
127
|
+
const extras = { queries: queries.join('\n') + (queries.length ? '\n' : '') };
|
|
128
|
+
void initId;
|
|
129
|
+
return { format: 'uppaal', primary: xta, extras, warnings };
|
|
130
|
+
}
|
|
131
|
+
function stateIdOf(ex, id) {
|
|
132
|
+
return ex.stateIdOf.get(id) ?? id;
|
|
133
|
+
}
|
|
134
|
+
// ------------------------------------------------------------------ TLA+ ----
|
|
135
|
+
// ------------------------------------------------------------------ TLA+ ----
|
|
136
|
+
function exportTla(ex) {
|
|
137
|
+
const { model, stateIds, stateIdOf, initId } = ex;
|
|
138
|
+
const warnings = [];
|
|
139
|
+
const bs = String.fromCharCode(92); // backslash, composed so no escape pitfalls
|
|
140
|
+
const CONJ = '/' + bs; // /\ (TLA conjunction)
|
|
141
|
+
const DISJ = bs + '/'; // \/ (TLA disjunction)
|
|
142
|
+
const MEM = bs + 'in'; // \in membership
|
|
143
|
+
const NOTIN = bs + 'notin'; // \notin
|
|
144
|
+
const variables = ['pc'].concat((model.variables ?? []).map((v) => v.name));
|
|
145
|
+
const typeVar = (model.variables ?? []).map((v) => v.name + ' ' + MEM + ' ' + String(numValue(v.min ?? 0)) + '..' + String(Math.max(1, numValue(v.max ?? 1)))).join(' ' + CONJ + ' ');
|
|
146
|
+
const initParts = ['pc = "' + initId + '"'].concat((model.variables ?? []).map((v) => v.name + ' = ' + String(numValue(v.init)))).join(' ' + CONJ + ' ');
|
|
147
|
+
const nextParts = model.transitions.map((tr) => {
|
|
148
|
+
const guard = guardExpr(tr.guard, CONJ, DISJ, '~', (v) => v).replace(/ == /g, ' = ').replace(/ != /g, ' /= ');
|
|
149
|
+
const updates = [];
|
|
150
|
+
for (const u of tr.updates ?? []) {
|
|
151
|
+
const value = u.value ?? 1;
|
|
152
|
+
if (u.op === 'set')
|
|
153
|
+
updates.push(u.variable + ' = ' + String(value));
|
|
154
|
+
else if (u.op === 'inc')
|
|
155
|
+
updates.push(u.variable + ' = ' + u.variable + ' + ' + String(value));
|
|
156
|
+
else
|
|
157
|
+
updates.push(u.variable + ' = ' + u.variable + ' - ' + String(value));
|
|
158
|
+
}
|
|
159
|
+
const pcPart = 'pc' + "'" + ' = "' + stateIdOf.get(tr.to) + '"';
|
|
160
|
+
const rest = updates.length ? ' ' + CONJ + ' ' + updates.join(' ' + CONJ + ' ') : '';
|
|
161
|
+
const guardPart = guard === '' ? 'pc = "' + stateIdOf.get(tr.from) + '"' : 'pc = "' + stateIdOf.get(tr.from) + '" ' + CONJ + ' ' + guard;
|
|
162
|
+
return DISJ + ' ' + guardPart + ' ' + CONJ + ' ' + pcPart + rest;
|
|
163
|
+
}).join('\n');
|
|
164
|
+
const forbids = forbiddenIndexes(ex).map((i) => '"' + stateIds[i] + '"');
|
|
165
|
+
const props = [];
|
|
166
|
+
if (forbids.length)
|
|
167
|
+
props.push('CheckSafety == [] (pc ' + NOTIN + ' {' + forbids.join(', ') + '})');
|
|
168
|
+
for (const invariant of model.invariants ?? []) {
|
|
169
|
+
if (invariant.kind !== 'never-states')
|
|
170
|
+
warnings.push('TLA+ v1 does not translate invariant kind ' + invariant.kind + '; only never-states becomes a property.');
|
|
171
|
+
}
|
|
172
|
+
let spec = '---- MODULE LogicProbe ----\n' +
|
|
173
|
+
'EXTENDS Integers\n' +
|
|
174
|
+
'VARIABLES ' + variables.join(', ') + '\n' +
|
|
175
|
+
'States == {' + stateIds.map((s) => '"' + s + '"').join(', ') + '}\n' +
|
|
176
|
+
'TypeOK == pc ' + MEM + ' States' + (typeVar === '' ? '' : ' ' + CONJ + ' ' + typeVar) + '\n' +
|
|
177
|
+
'Init == ' + initParts + '\n' +
|
|
178
|
+
'Next ==\n' + nextParts + '\n' +
|
|
179
|
+
(props.length ? '\n' + props.join('\n\n') + '\n' : '') +
|
|
180
|
+
'====\n';
|
|
181
|
+
const extras = props.length ? { properties: props.join('\n') + '\n' } : {};
|
|
182
|
+
return { format: 'tla', primary: spec, extras, warnings };
|
|
183
|
+
}
|
|
184
|
+
// ----------------------------------------------------------------- PRISM ----
|
|
185
|
+
function exportPrism(ex) {
|
|
186
|
+
const { model, indexOf, stateIdOf, stateIds } = ex;
|
|
187
|
+
const warnings = [];
|
|
188
|
+
const ranges = (model.variables ?? []).map((v) => ({
|
|
189
|
+
name: v.name,
|
|
190
|
+
lo: numValue(v.min ?? 0),
|
|
191
|
+
hi: Math.max(1, numValue(v.max ?? 3)),
|
|
192
|
+
init: numValue(v.init),
|
|
193
|
+
}));
|
|
194
|
+
const valuations = [];
|
|
195
|
+
const build = (i, acc) => {
|
|
196
|
+
if (i === ranges.length) {
|
|
197
|
+
valuations.push(acc);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
for (let v = ranges[i].lo; v <= ranges[i].hi; v++) {
|
|
201
|
+
const copy = { ...acc };
|
|
202
|
+
copy[ranges[i].name] = v;
|
|
203
|
+
build(i + 1, copy);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
build(0, {});
|
|
207
|
+
const size = model.states.length * Math.max(1, valuations.length);
|
|
208
|
+
if (size > 20000)
|
|
209
|
+
throw new Error('PRISM enumeration too large: ' + model.states.length + ' x ' + valuations.length);
|
|
210
|
+
const evalGuard = (guard, val) => {
|
|
211
|
+
if (guard === undefined)
|
|
212
|
+
return true;
|
|
213
|
+
if ('variable' in guard) {
|
|
214
|
+
const left = val[guard.variable] ?? numValue((model.variables ?? []).find((v) => v.name === guard.variable)?.init ?? 0);
|
|
215
|
+
const right = numValue(guard.value);
|
|
216
|
+
switch (guard.op) {
|
|
217
|
+
case '==': return left === right;
|
|
218
|
+
case '!=': return left !== right;
|
|
219
|
+
case '<': return left < right;
|
|
220
|
+
case '<=': return left <= right;
|
|
221
|
+
case '>': return left > right;
|
|
222
|
+
case '>=': return left >= right;
|
|
223
|
+
default: return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if ('all' in guard)
|
|
227
|
+
return guard.all.every((g) => evalGuard(g, val));
|
|
228
|
+
if ('any' in guard)
|
|
229
|
+
return guard.any.some((g) => evalGuard(g, val));
|
|
230
|
+
if ('not' in guard)
|
|
231
|
+
return !evalGuard(guard.not, val);
|
|
232
|
+
return false;
|
|
233
|
+
};
|
|
234
|
+
const applyUpdates = (t, val) => {
|
|
235
|
+
const out = { ...val };
|
|
236
|
+
for (const u of t.updates ?? []) {
|
|
237
|
+
const cur = out[u.variable] ?? 0;
|
|
238
|
+
const value = u.value ?? 1;
|
|
239
|
+
if (u.op === 'set')
|
|
240
|
+
out[u.variable] = value;
|
|
241
|
+
else if (u.op === 'inc')
|
|
242
|
+
out[u.variable] = cur + value;
|
|
243
|
+
else
|
|
244
|
+
out[u.variable] = cur - value;
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
};
|
|
248
|
+
const commands = [];
|
|
249
|
+
for (const state of model.states) {
|
|
250
|
+
for (const val of valuations) {
|
|
251
|
+
const groups = new Map();
|
|
252
|
+
for (const t of model.transitions)
|
|
253
|
+
if (t.from === state.id) {
|
|
254
|
+
const list = groups.get(t.event) ?? [];
|
|
255
|
+
list.push(t);
|
|
256
|
+
groups.set(t.event, list);
|
|
257
|
+
}
|
|
258
|
+
const outcomes = new Map();
|
|
259
|
+
let totalW = 0;
|
|
260
|
+
for (const group of groups.values()) {
|
|
261
|
+
const guarded = group.filter((t) => t.guard !== undefined && evalGuard(t.guard, val));
|
|
262
|
+
const chosen = guarded.length > 0 ? guarded : group.filter((t) => t.guard === undefined);
|
|
263
|
+
for (const t of chosen) {
|
|
264
|
+
const weight = t.weight ?? 1;
|
|
265
|
+
const nv = applyUpdates(t, val);
|
|
266
|
+
const key = String(indexOf.get(t.to)) + '|' + JSON.stringify(nv);
|
|
267
|
+
const cur = outcomes.get(key);
|
|
268
|
+
if (cur === undefined) {
|
|
269
|
+
outcomes.set(key, { weight, pc: indexOf.get(t.to) ?? 0, val: nv });
|
|
270
|
+
totalW += weight;
|
|
271
|
+
}
|
|
272
|
+
else
|
|
273
|
+
cur.weight += weight;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (outcomes.size === 0)
|
|
277
|
+
continue;
|
|
278
|
+
const guardParts = ['pc = ' + String(indexOf.get(state.id) ?? 0)];
|
|
279
|
+
for (const r of ranges)
|
|
280
|
+
guardParts.push(r.name + ' = ' + String(val[r.name] ?? r.init));
|
|
281
|
+
const terms = [];
|
|
282
|
+
for (const item of outcomes.values()) {
|
|
283
|
+
const p = totalW > 0 ? item.weight / totalW : 0;
|
|
284
|
+
const prob = p.toFixed(6).replace(/0+$/, '').replace(/\.$/, '');
|
|
285
|
+
const upd = ["(pc' = " + String(item.pc) + ")"];
|
|
286
|
+
for (const r of ranges) {
|
|
287
|
+
const v = item.val[r.name] ?? r.init;
|
|
288
|
+
if (v !== (val[r.name] ?? r.init))
|
|
289
|
+
upd.push("(" + r.name + "' = " + String(v) + ")");
|
|
290
|
+
}
|
|
291
|
+
terms.push(prob + ' : ' + upd.join(' & '));
|
|
292
|
+
}
|
|
293
|
+
commands.push('[] ' + guardParts.join(' & ') + ' ->\n ' + terms.join(' +\n '));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
let module = '// Generated by logicprobe (DTMC export; booleans as 0/1 integers).\n' +
|
|
297
|
+
'dtmc\n\n' +
|
|
298
|
+
'module LogicProbe\n' +
|
|
299
|
+
' pc : [0..' + String(model.states.length - 1) + '] init ' + String(indexOf.get(model.init) ?? 0) + ';\n' +
|
|
300
|
+
(ranges.map((r) => ' ' + r.name + ' : [' + String(r.lo) + '..' + String(r.hi) + '] init ' + String(r.init) + ';').join('\n')) + '\n\n' +
|
|
301
|
+
commands.join('\n\n') + '\n' +
|
|
302
|
+
'endmodule\n';
|
|
303
|
+
const labels = [];
|
|
304
|
+
for (const state of model.states)
|
|
305
|
+
if (state.terminal)
|
|
306
|
+
labels.push('label "term_' + (stateIdOf.get(state.id) ?? 's') + '" = pc = ' + String(indexOf.get(state.id)));
|
|
307
|
+
const forb = forbiddenIndexes(ex);
|
|
308
|
+
if (forb.length)
|
|
309
|
+
labels.push('label "forbidden" = ' + forb.map((i) => 'pc = ' + String(i)).join(' | '));
|
|
310
|
+
const pctl = [];
|
|
311
|
+
for (const invariant of model.invariants ?? []) {
|
|
312
|
+
if (invariant.kind === 'probability') {
|
|
313
|
+
const target = indexOf.get(invariant.target);
|
|
314
|
+
if (target !== undefined) {
|
|
315
|
+
labels.push('label "target" = pc = ' + String(target));
|
|
316
|
+
pctl.push('P' + invariant.op + String(invariant.p) + ' [ F "target" ]');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (forb.length)
|
|
321
|
+
pctl.push('P>=1 [ G !"forbidden" ]');
|
|
322
|
+
module = module + '\n' + labels.join('\n') + '\n';
|
|
323
|
+
const extras = { properties: pctl.join('\n') + '\n' };
|
|
324
|
+
for (const invariant of model.invariants ?? []) {
|
|
325
|
+
if (invariant.kind !== 'probability' && invariant.kind !== 'never-states') {
|
|
326
|
+
warnings.push('PRISM v1 does not translate invariant kind ' + invariant.kind + '.');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
void stateIds;
|
|
330
|
+
return { format: 'prism', primary: module, extras, warnings };
|
|
331
|
+
}
|
|
332
|
+
// ------------------------------------------------------------------ SPIN ----
|
|
333
|
+
function exportSpin(ex) {
|
|
334
|
+
const { model, indexOf } = ex;
|
|
335
|
+
const warnings = [];
|
|
336
|
+
const varDecl = (model.variables ?? []).map((v) => 'int ' + v.name + ' = ' + String(numValue(v.init)) + ';').join('\n');
|
|
337
|
+
const lines = [];
|
|
338
|
+
const promAssigns = (updates) => {
|
|
339
|
+
const out = [];
|
|
340
|
+
for (const u of updates ?? []) {
|
|
341
|
+
const value = u.value ?? 1;
|
|
342
|
+
if (u.op === 'set')
|
|
343
|
+
out.push(u.variable + ' = ' + String(value));
|
|
344
|
+
else if (u.op === 'inc')
|
|
345
|
+
out.push(u.variable + ' = ' + u.variable + ' + ' + String(value));
|
|
346
|
+
else
|
|
347
|
+
out.push(u.variable + ' = ' + u.variable + ' - ' + String(value));
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
};
|
|
351
|
+
for (const t of model.transitions) {
|
|
352
|
+
const guard = guardExpr(t.guard, '&&', '||', '!', (v) => v);
|
|
353
|
+
const assigns = promAssigns(t.updates);
|
|
354
|
+
const cond = guard === '' ? 'pc == ' + String(indexOf.get(t.from)) : 'pc == ' + String(indexOf.get(t.from)) + ' && ' + guard;
|
|
355
|
+
const prefix = assigns.length ? ' ' + assigns.join('; ') + ';' : '';
|
|
356
|
+
lines.push(' :: (' + cond + ') ->' + prefix + ' pc = ' + String(indexOf.get(t.to)) + ';');
|
|
357
|
+
}
|
|
358
|
+
for (const state of model.states)
|
|
359
|
+
if (state.terminal)
|
|
360
|
+
lines.push(' :: (pc == ' + String(indexOf.get(state.id)) + ') -> goto done;');
|
|
361
|
+
const promela = '// Generated by logicprobe (Promela v1; booleans as 0/1 integers).\n' +
|
|
362
|
+
'int pc = ' + String(indexOf.get(model.init) ?? 0) + ';\n' +
|
|
363
|
+
(varDecl === '' ? '' : varDecl + '\n') +
|
|
364
|
+
'active proctype LogicProbe() {\n' +
|
|
365
|
+
' do\n' +
|
|
366
|
+
lines.join('\n') + '\n' +
|
|
367
|
+
' od\n' +
|
|
368
|
+
'done: skip\n' +
|
|
369
|
+
'}\n';
|
|
370
|
+
const extras = {};
|
|
371
|
+
const forb = forbiddenIndexes(ex);
|
|
372
|
+
if (forb.length)
|
|
373
|
+
extras.properties = 'ltl safety { [] (!(pc == ' + forb.map((i) => String(i)).join(') && !(pc == ') + ')) }\n';
|
|
374
|
+
for (const invariant of model.invariants ?? []) {
|
|
375
|
+
if (invariant.kind !== 'never-states')
|
|
376
|
+
warnings.push('SPIN v1 does not translate invariant kind ' + invariant.kind + '.');
|
|
377
|
+
}
|
|
378
|
+
return { format: 'spin', primary: promela, extras, warnings };
|
|
379
|
+
}
|
|
380
|
+
export function exportModel(input, format) {
|
|
381
|
+
const ex = prepareModel(input);
|
|
382
|
+
if (format === 'uppaal')
|
|
383
|
+
return exportUppaal(ex);
|
|
384
|
+
if (format === 'tla')
|
|
385
|
+
return exportTla(ex);
|
|
386
|
+
if (format === 'prism')
|
|
387
|
+
return exportPrism(ex);
|
|
388
|
+
return exportSpin(ex);
|
|
389
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -31,6 +31,8 @@ import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem';
|
|
|
31
31
|
import { logicProbeVerifyTool } from './tool.js';
|
|
32
32
|
import { logicProbeDataModelVerifyTool, DATA_ENGINE_SCHEMA_VERSION } from './data-tool.js';
|
|
33
33
|
import { logicProbeConcurrencyScanTool } from './concurrency-tool.js';
|
|
34
|
+
import { logicProbeComposeTool } from './compose-tool.js';
|
|
35
|
+
import { logicProbeExportTool } from './export-tool.js';
|
|
34
36
|
import { ENGINE_SCHEMA_VERSION } from './engine.js';
|
|
35
37
|
export const name = 'logicprobe';
|
|
36
38
|
// Skills are contributed through the registry service, which dsh-base always
|
|
@@ -41,23 +43,23 @@ export const inject = ['skills'];
|
|
|
41
43
|
// lands on `<package>/skills` regardless of where the package was installed.
|
|
42
44
|
const SKILLS_DIR = fileURLToPath(new URL('../skills', import.meta.url));
|
|
43
45
|
const GATE_PLUGIN_ID = 'logicprobe';
|
|
44
|
-
const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
|
|
45
|
-
Plugin logicprobe is active. Documents are not truth — code is. Verify every verifiable claim before accepting or acting on any design.
|
|
46
|
-
|
|
47
|
-
**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, or behavioral guarantees ("always"/"never"/"guaranteed") — load it with the skill tool before responding. The cost of loading is trivial compared to the cost of a false claim.
|
|
48
|
-
|
|
49
|
-
**Red Flags** — if you think any of these, STOP. You are rationalizing:
|
|
50
|
-
|
|
51
|
-
| You think | Reality |
|
|
52
|
-
|-----------|---------|
|
|
53
|
-
| "This plan is too simple to verify" | The skill auto-classifies depth (LIGHTWEIGHT / STANDARD / ESCALATED). You don't decide. |
|
|
54
|
-
| "I already know the file paths are correct" | Organic verification leaves no audit trail. Run Phase 0, append the "## Plan Verification" block. |
|
|
55
|
-
| "I'll verify while implementing" | Verification happens before implementation, not during. |
|
|
56
|
-
| "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
|
|
57
|
-
|
|
58
|
-
**Native verification path**: In dsh, prefer the \`logicprobe_verify\` tool for state-machine checks and \`logicprobe_datamodel_verify\` for data-model/schema migration checks. Both support before/after regression and common domain constraints (idempotency, monotonic, sequence, leads-to, atomicity). Python harnesses remain the fallback for non-dsh hosts.
|
|
59
|
-
|
|
60
|
-
**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).
|
|
46
|
+
const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
|
|
47
|
+
Plugin logicprobe is active. Documents are not truth — code is. Verify every verifiable claim before accepting or acting on any design.
|
|
48
|
+
|
|
49
|
+
**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, or behavioral guarantees ("always"/"never"/"guaranteed") — load it with the skill tool before responding. The cost of loading is trivial compared to the cost of a false claim.
|
|
50
|
+
|
|
51
|
+
**Red Flags** — if you think any of these, STOP. You are rationalizing:
|
|
52
|
+
|
|
53
|
+
| You think | Reality |
|
|
54
|
+
|-----------|---------|
|
|
55
|
+
| "This plan is too simple to verify" | The skill auto-classifies depth (LIGHTWEIGHT / STANDARD / ESCALATED). You don't decide. |
|
|
56
|
+
| "I already know the file paths are correct" | Organic verification leaves no audit trail. Run Phase 0, append the "## Plan Verification" block. |
|
|
57
|
+
| "I'll verify while implementing" | Verification happens before implementation, not during. |
|
|
58
|
+
| "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
|
|
59
|
+
|
|
60
|
+
**Native verification path**: In dsh, prefer the \`logicprobe_verify\` tool for state-machine checks and \`logicprobe_datamodel_verify\` for data-model/schema migration checks. Both support before/after regression and common domain constraints (idempotency, monotonic, sequence, leads-to, atomicity). Python harnesses remain the fallback for non-dsh hosts.
|
|
61
|
+
|
|
62
|
+
**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).
|
|
61
63
|
</EXTREMELY_IMPORTANT>`;
|
|
62
64
|
export const Config = z.object({
|
|
63
65
|
enabled: z.boolean().default(true),
|
|
@@ -71,8 +73,19 @@ function gateMessage(text) {
|
|
|
71
73
|
source: { kind: 'plugin', plugin: GATE_PLUGIN_ID },
|
|
72
74
|
});
|
|
73
75
|
}
|
|
76
|
+
function readSessionEvents(session) {
|
|
77
|
+
const source = session;
|
|
78
|
+
if (typeof source.snapshotEvents === 'function') {
|
|
79
|
+
const snapshot = source.snapshotEvents();
|
|
80
|
+
if (Array.isArray(snapshot))
|
|
81
|
+
return snapshot;
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(source.events))
|
|
84
|
+
return source.events;
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
74
87
|
function lastApprovalPolicy(session) {
|
|
75
|
-
const events = session
|
|
88
|
+
const events = readSessionEvents(session);
|
|
76
89
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
77
90
|
const event = events[index];
|
|
78
91
|
if (event.type === 'approval/policy') {
|
|
@@ -82,7 +95,7 @@ function lastApprovalPolicy(session) {
|
|
|
82
95
|
return undefined;
|
|
83
96
|
}
|
|
84
97
|
function planModeActive(session) {
|
|
85
|
-
const events = session
|
|
98
|
+
const events = readSessionEvents(session);
|
|
86
99
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
87
100
|
const event = events[index];
|
|
88
101
|
if (event.type === 'plan/mode')
|
|
@@ -113,7 +126,7 @@ function modeContextText(config, session) {
|
|
|
113
126
|
* lets the model read this plugin's runtime status without guessing. Mirrors
|
|
114
127
|
* the registration pattern of the official dsh-tool-cordis host providers.
|
|
115
128
|
*/
|
|
116
|
-
function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcurrencyToolRegistered) {
|
|
129
|
+
function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcurrencyToolRegistered, isComposeToolRegistered, isExportToolRegistered) {
|
|
117
130
|
return {
|
|
118
131
|
manifest: {
|
|
119
132
|
id: 'logicprobe',
|
|
@@ -137,10 +150,12 @@ function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcu
|
|
|
137
150
|
toolRegistered: { type: 'boolean', description: 'Whether the logicprobe_verify tool is registered on ctx.tools.' },
|
|
138
151
|
dataToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_datamodel_verify tool is registered on ctx.tools.' },
|
|
139
152
|
concurrencyToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_concurrency_scan tool is registered on ctx.tools.' },
|
|
153
|
+
composeToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_compose_verify tool is registered on ctx.tools.' },
|
|
154
|
+
exportToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_export tool is registered on ctx.tools.' },
|
|
140
155
|
engineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled state-machine verification engine accepts.' },
|
|
141
156
|
dataEngineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled data-model verification engine accepts.' },
|
|
142
157
|
},
|
|
143
|
-
required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
|
|
158
|
+
required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'composeToolRegistered', 'exportToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
|
|
144
159
|
additionalProperties: false,
|
|
145
160
|
},
|
|
146
161
|
},
|
|
@@ -155,6 +170,8 @@ function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcu
|
|
|
155
170
|
toolRegistered: isToolRegistered(),
|
|
156
171
|
dataToolRegistered: isDataToolRegistered(),
|
|
157
172
|
concurrencyToolRegistered: isConcurrencyToolRegistered(),
|
|
173
|
+
composeToolRegistered: isComposeToolRegistered(),
|
|
174
|
+
exportToolRegistered: isExportToolRegistered(),
|
|
158
175
|
engineSchemaVersion: ENGINE_SCHEMA_VERSION,
|
|
159
176
|
dataEngineSchemaVersion: DATA_ENGINE_SCHEMA_VERSION,
|
|
160
177
|
};
|
|
@@ -171,6 +188,8 @@ export function apply(ctx, config) {
|
|
|
171
188
|
let toolRegistered = false;
|
|
172
189
|
let dataToolRegistered = false;
|
|
173
190
|
let concurrencyToolRegistered = false;
|
|
191
|
+
let composeToolRegistered = false;
|
|
192
|
+
let exportToolRegistered = false;
|
|
174
193
|
let modeContextRegistered = false;
|
|
175
194
|
const registerProvider = () => {
|
|
176
195
|
if (providerRegistered)
|
|
@@ -179,7 +198,7 @@ export function apply(ctx, config) {
|
|
|
179
198
|
if (inspect === undefined)
|
|
180
199
|
return;
|
|
181
200
|
try {
|
|
182
|
-
ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered)), 'logicprobe: inspect provider');
|
|
201
|
+
ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered, () => composeToolRegistered, () => exportToolRegistered)), 'logicprobe: inspect provider');
|
|
183
202
|
providerRegistered = true;
|
|
184
203
|
}
|
|
185
204
|
catch (err) {
|
|
@@ -196,9 +215,13 @@ export function apply(ctx, config) {
|
|
|
196
215
|
ctx.effect(() => tools.register(logicProbeVerifyTool), 'logicprobe: verify tool');
|
|
197
216
|
ctx.effect(() => tools.register(logicProbeDataModelVerifyTool), 'logicprobe: data verify tool');
|
|
198
217
|
ctx.effect(() => tools.register(logicProbeConcurrencyScanTool), 'logicprobe: concurrency scan tool');
|
|
218
|
+
ctx.effect(() => tools.register(logicProbeComposeTool), 'logicprobe: compose tool');
|
|
219
|
+
ctx.effect(() => tools.register(logicProbeExportTool), 'logicprobe: export tool');
|
|
199
220
|
toolRegistered = true;
|
|
200
221
|
dataToolRegistered = true;
|
|
201
222
|
concurrencyToolRegistered = true;
|
|
223
|
+
composeToolRegistered = true;
|
|
224
|
+
exportToolRegistered = true;
|
|
202
225
|
}
|
|
203
226
|
catch (err) {
|
|
204
227
|
console.warn('[logicprobe] logicprobe_verify/logicprobe_datamodel_verify tool registration failed', err);
|
|
@@ -282,10 +305,10 @@ export function apply(ctx, config) {
|
|
|
282
305
|
* first step that runs.
|
|
283
306
|
*/
|
|
284
307
|
function gateInHistory(session) {
|
|
285
|
-
return session.
|
|
308
|
+
return readSessionEvents(session).some((event) => {
|
|
286
309
|
if (event.type !== 'user/message')
|
|
287
310
|
return false;
|
|
288
|
-
const source = event.data
|
|
289
|
-
return source
|
|
311
|
+
const source = event.data?.source;
|
|
312
|
+
return source?.kind === 'plugin' && source.plugin === GATE_PLUGIN_ID;
|
|
290
313
|
});
|
|
291
314
|
}
|