dsh-logicprobe 0.6.7 → 0.7.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/engine.js +127 -2
- package/lib/index.js +26 -19
- package/lib/tool.js +1 -1
- package/lib/types/engine.d.ts +22 -0
- package/lib/types/index.d.ts +8 -0
- package/package.json +9 -4
- package/skills/logicprobe/references/__pycache__/logicprobe-engine.cpython-310.pyc +0 -0
- package/skills/logicprobe/references/dsh-model-schema.md +322 -266
- package/skills/logicprobe/references/logicprobe-engine.py +3169 -3028
- package/src/engine.ts +2714 -2568
- package/src/index.ts +369 -349
- package/src/tool.ts +61 -61
package/lib/engine.js
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
export const ENGINE_SCHEMA_VERSION = 1;
|
|
3
|
+
export function isStateGuard(when) {
|
|
4
|
+
return 'state' in when;
|
|
5
|
+
}
|
|
6
|
+
/** Allowed keys per invariant kind — the schema is closed, so anything else is a typo or an unsupported feature. */
|
|
7
|
+
const INVARIANT_KEYS = {
|
|
8
|
+
'never-states': new Set(['id', 'description', 'kind', 'states']),
|
|
9
|
+
'var-in-range': new Set(['id', 'description', 'kind', 'variable', 'min', 'max', 'when']),
|
|
10
|
+
'event-before-state': new Set(['id', 'description', 'kind', 'event', 'state']),
|
|
11
|
+
'leads-to': new Set(['id', 'description', 'kind', 'from', 'to']),
|
|
12
|
+
'sequence': new Set(['id', 'description', 'kind', 'events']),
|
|
13
|
+
'atomicity': new Set(['id', 'description', 'kind', 'events', 'commit', 'rollback']),
|
|
14
|
+
'budget': new Set(['id', 'description', 'kind', 'budget']),
|
|
15
|
+
'probability': new Set(['id', 'description', 'kind', 'target', 'op', 'p']),
|
|
16
|
+
};
|
|
17
|
+
const KNOWN_INVARIANT_KINDS = new Set(Object.keys(INVARIANT_KEYS));
|
|
18
|
+
/**
|
|
19
|
+
* Declared keys per model part. The model schema is closed: a key that is not
|
|
20
|
+
* declared is a typo or an unsupported feature, and silently ignoring it can make
|
|
21
|
+
* the engine report a property that the model does not actually have (a mistyped
|
|
22
|
+
* guard variable reads as an always-false guard, which prunes real paths and can
|
|
23
|
+
* yield a false "no deadlock"). Rejecting is the only safe option for a verifier.
|
|
24
|
+
*/
|
|
25
|
+
const MODEL_KEYS = {
|
|
26
|
+
root: new Set(['schemaVersion', 'init', 'states', 'transitions', 'variables', 'invariants', 'concurrentPairs', 'boundaryChecks', 'resourcePairs', 'idempotentEvents', 'tickEvents', 'narrative']),
|
|
27
|
+
state: new Set(['id', 'terminal', 'onEntry', 'onExit', 'maxTicks']),
|
|
28
|
+
transition: new Set(['from', 'event', 'to', 'guard', 'updates', 'cost', 'weight']),
|
|
29
|
+
update: new Set(['variable', 'op', 'value']),
|
|
30
|
+
variable: new Set(['name', 'kind', 'init', 'min', 'max', 'monotonic']),
|
|
31
|
+
// LogicModelV1 boundary checks are (variable, values). The data-model engine has
|
|
32
|
+
// its own (entity, field) shape and its own validator — the two must not be mixed.
|
|
33
|
+
boundaryCheck: new Set(['variable', 'values']),
|
|
34
|
+
resourcePair: new Set(['resource', 'acquireEvent', 'releaseEvent', 'failEvent']),
|
|
35
|
+
narrative: new Set(['states', 'events', 'scenarios']),
|
|
36
|
+
scenario: new Set(['from', 'event', 'scenario']),
|
|
37
|
+
};
|
|
3
38
|
const DEFAULT_MAX_STATES = 10_000;
|
|
4
39
|
const DEFAULT_MAX_PERMUTATION_EVENTS = 5;
|
|
5
40
|
function stableStringify(value) {
|
|
@@ -22,6 +57,13 @@ export function validateModel(input) {
|
|
|
22
57
|
return { ok: false, errors: ['model: must be an object'] };
|
|
23
58
|
}
|
|
24
59
|
const root = input;
|
|
60
|
+
const rejectUnknownKeys = (value, allowed, path) => {
|
|
61
|
+
const unexpected = Object.keys(value).filter((key) => !allowed.has(key));
|
|
62
|
+
if (unexpected.length > 0) {
|
|
63
|
+
bad(path + '.' + unexpected[0], 'unknown field (allowed: ' + [...allowed].join(', ') + ')');
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
rejectUnknownKeys(root, MODEL_KEYS.root, 'model');
|
|
25
67
|
if (root.schemaVersion !== 1)
|
|
26
68
|
bad('schemaVersion', 'must be 1');
|
|
27
69
|
if (typeof root.init !== 'string' || root.init.length === 0)
|
|
@@ -37,6 +79,7 @@ export function validateModel(input) {
|
|
|
37
79
|
return;
|
|
38
80
|
}
|
|
39
81
|
const state = entry;
|
|
82
|
+
rejectUnknownKeys(state, MODEL_KEYS.state, 'states[' + index + ']');
|
|
40
83
|
if (typeof state.id !== 'string' || state.id.length === 0)
|
|
41
84
|
bad('states[' + index + '].id', 'must be a non-empty string');
|
|
42
85
|
else if (seen.has(state.id))
|
|
@@ -75,6 +118,7 @@ export function validateModel(input) {
|
|
|
75
118
|
return;
|
|
76
119
|
}
|
|
77
120
|
const transition = entry;
|
|
121
|
+
rejectUnknownKeys(transition, MODEL_KEYS.transition, path);
|
|
78
122
|
if (typeof transition.from !== 'string' || transition.from.length === 0)
|
|
79
123
|
bad(path + '.from', 'must be a non-empty string');
|
|
80
124
|
else if (!stateIds.has(transition.from))
|
|
@@ -98,6 +142,7 @@ export function validateModel(input) {
|
|
|
98
142
|
return;
|
|
99
143
|
}
|
|
100
144
|
const record = update;
|
|
145
|
+
rejectUnknownKeys(record, MODEL_KEYS.update, updatePath);
|
|
101
146
|
if (typeof record.variable !== 'string' || record.variable.length === 0)
|
|
102
147
|
bad(updatePath + '.variable', 'must be a non-empty string');
|
|
103
148
|
if (record.op !== 'set' && record.op !== 'inc' && record.op !== 'dec')
|
|
@@ -135,6 +180,7 @@ export function validateModel(input) {
|
|
|
135
180
|
return;
|
|
136
181
|
}
|
|
137
182
|
const variable = entry;
|
|
183
|
+
rejectUnknownKeys(variable, MODEL_KEYS.variable, path);
|
|
138
184
|
if (typeof variable.name !== 'string' || variable.name.length === 0)
|
|
139
185
|
bad(path + '.name', 'must be a non-empty string');
|
|
140
186
|
else if (variableNames.has(variable.name))
|
|
@@ -168,6 +214,10 @@ export function validateModel(input) {
|
|
|
168
214
|
else if (!variableNames.has(name))
|
|
169
215
|
bad(path, 'references unknown variable ' + name);
|
|
170
216
|
};
|
|
217
|
+
const invariantStateIds = new Set(Array.isArray(root.states) ? root.states.map((state) => state.id) : []);
|
|
218
|
+
// Reference sets for invariant targets: denormalising a kind's target into the
|
|
219
|
+
// report makes a mistyped id look authoritative, so every reference must resolve.
|
|
220
|
+
const invariantEventIds = new Set(Array.isArray(root.transitions) ? root.transitions.map((transition) => transition.event) : []);
|
|
171
221
|
if (root.invariants !== undefined) {
|
|
172
222
|
if (!Array.isArray(root.invariants))
|
|
173
223
|
bad('invariants', 'must be an array');
|
|
@@ -183,6 +233,17 @@ export function validateModel(input) {
|
|
|
183
233
|
bad(path + '.id', 'must be a non-empty string');
|
|
184
234
|
if (typeof invariant.description !== 'string')
|
|
185
235
|
bad(path + '.description', 'must be a string');
|
|
236
|
+
// Reject keys the selected kind does not declare instead of silently ignoring
|
|
237
|
+
// them: an ignored field (e.g. a `when` scope on a kind that cannot honor it)
|
|
238
|
+
// still shows up in the echoed report and reads as if it took effect. Kinds an
|
|
239
|
+
// older reader may not know are skipped so they still fail on `.kind` alone.
|
|
240
|
+
if (KNOWN_INVARIANT_KINDS.has(invariant.kind)) {
|
|
241
|
+
const allowed = INVARIANT_KEYS[invariant.kind];
|
|
242
|
+
const unexpected = Object.keys(invariant).filter((key) => !allowed.has(key));
|
|
243
|
+
if (unexpected.length > 0) {
|
|
244
|
+
bad(path + '.' + unexpected[0], 'unknown field for kind ' + String(invariant.kind) + ' (allowed: ' + [...allowed].join(', ') + ')');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
186
247
|
if (invariant.kind === 'never-states') {
|
|
187
248
|
if (!Array.isArray(invariant.states) || invariant.states.length === 0)
|
|
188
249
|
bad(path + '.states', 'must be a non-empty array');
|
|
@@ -190,6 +251,8 @@ export function validateModel(input) {
|
|
|
190
251
|
invariant.states.forEach((state, stateIndex) => {
|
|
191
252
|
if (typeof state !== 'string' || state.length === 0)
|
|
192
253
|
bad(path + '.states[' + stateIndex + ']', 'must be a non-empty string');
|
|
254
|
+
else if (!invariantStateIds.has(state))
|
|
255
|
+
bad(path + '.states[' + stateIndex + ']', 'references unknown state ' + state);
|
|
193
256
|
});
|
|
194
257
|
}
|
|
195
258
|
else if (invariant.kind === 'var-in-range') {
|
|
@@ -198,18 +261,57 @@ export function validateModel(input) {
|
|
|
198
261
|
bad(path + '.min', 'must be a number');
|
|
199
262
|
if (invariant.max !== undefined && typeof invariant.max !== 'number')
|
|
200
263
|
bad(path + '.max', 'must be a number');
|
|
264
|
+
if (invariant.min === undefined && invariant.max === undefined)
|
|
265
|
+
bad(path, 'requires min or max (a range with neither bound is vacuous)');
|
|
266
|
+
if (invariant.when !== undefined) {
|
|
267
|
+
const when = invariant.when;
|
|
268
|
+
if (typeof when !== 'object' || when === null || Array.isArray(when)) {
|
|
269
|
+
bad(path + '.when', 'must be an object');
|
|
270
|
+
}
|
|
271
|
+
else if ('state' in when) {
|
|
272
|
+
const scope = when;
|
|
273
|
+
for (const key of Object.keys(scope)) {
|
|
274
|
+
if (key !== 'state')
|
|
275
|
+
bad(path + '.when.' + key, 'unknown field for a state scope (allowed: state)');
|
|
276
|
+
}
|
|
277
|
+
const state = scope.state;
|
|
278
|
+
if (typeof state !== 'string' || state.length === 0)
|
|
279
|
+
bad(path + '.when.state', 'must be a non-empty string');
|
|
280
|
+
else if (!invariantStateIds.has(state))
|
|
281
|
+
bad(path + '.when.state', 'references unknown state ' + state);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
validateGuard(when, path + '.when', errors, bad);
|
|
285
|
+
for (const variable of guardVariables(when)) {
|
|
286
|
+
if (variable === invariant.variable) {
|
|
287
|
+
bad(path + '.when', 'must not reference the constrained variable ' + invariant.variable + ' (it can mask its own violation)');
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
validateVariableRef(variable, path + '.when');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
201
295
|
}
|
|
202
296
|
else if (invariant.kind === 'event-before-state') {
|
|
203
297
|
if (typeof invariant.event !== 'string' || invariant.event.length === 0)
|
|
204
298
|
bad(path + '.event', 'must be a non-empty string');
|
|
299
|
+
else if (!invariantEventIds.has(invariant.event))
|
|
300
|
+
bad(path + '.event', 'references unknown event ' + invariant.event);
|
|
205
301
|
if (typeof invariant.state !== 'string' || invariant.state.length === 0)
|
|
206
302
|
bad(path + '.state', 'must be a non-empty string');
|
|
303
|
+
else if (!invariantStateIds.has(invariant.state))
|
|
304
|
+
bad(path + '.state', 'references unknown state ' + invariant.state);
|
|
207
305
|
}
|
|
208
306
|
else if (invariant.kind === 'leads-to') {
|
|
209
307
|
if (typeof invariant.from !== 'string' || invariant.from.length === 0)
|
|
210
308
|
bad(path + '.from', 'must be a non-empty string');
|
|
309
|
+
else if (!invariantStateIds.has(invariant.from))
|
|
310
|
+
bad(path + '.from', 'references unknown state ' + invariant.from);
|
|
211
311
|
if (typeof invariant.to !== 'string' || invariant.to.length === 0)
|
|
212
312
|
bad(path + '.to', 'must be a non-empty string');
|
|
313
|
+
else if (!invariantStateIds.has(invariant.to))
|
|
314
|
+
bad(path + '.to', 'references unknown state ' + invariant.to);
|
|
213
315
|
}
|
|
214
316
|
else if (invariant.kind === 'sequence') {
|
|
215
317
|
if (!Array.isArray(invariant.events) || invariant.events.length === 0)
|
|
@@ -240,6 +342,8 @@ export function validateModel(input) {
|
|
|
240
342
|
else if (invariant.kind === 'probability') {
|
|
241
343
|
if (typeof invariant.target !== 'string' || invariant.target.length === 0)
|
|
242
344
|
bad(path + '.target', 'must be a non-empty string');
|
|
345
|
+
else if (!invariantStateIds.has(invariant.target))
|
|
346
|
+
bad(path + '.target', 'references unknown state ' + invariant.target);
|
|
243
347
|
if (invariant.op !== '>=' && invariant.op !== '<=' && invariant.op !== '>' && invariant.op !== '<')
|
|
244
348
|
bad(path + '.op', "must be one of '>=', '<=', '>', '<'");
|
|
245
349
|
if (typeof invariant.p !== 'number' || !Number.isFinite(invariant.p) || invariant.p < 0 || invariant.p > 1)
|
|
@@ -272,6 +376,7 @@ export function validateModel(input) {
|
|
|
272
376
|
return;
|
|
273
377
|
}
|
|
274
378
|
const check = entry;
|
|
379
|
+
rejectUnknownKeys(check, MODEL_KEYS.boundaryCheck, path);
|
|
275
380
|
validateVariableRef(check.variable, path + '.variable');
|
|
276
381
|
if (!Array.isArray(check.values) || check.values.some((value) => typeof value !== 'number'))
|
|
277
382
|
bad(path + '.values', 'must be an array of numbers');
|
|
@@ -306,6 +411,7 @@ export function validateModel(input) {
|
|
|
306
411
|
return;
|
|
307
412
|
}
|
|
308
413
|
const pair = entry;
|
|
414
|
+
rejectUnknownKeys(pair, MODEL_KEYS.resourcePair, path);
|
|
309
415
|
if (typeof pair.resource !== 'string' || pair.resource.length === 0)
|
|
310
416
|
bad(path + '.resource', 'must be a non-empty string');
|
|
311
417
|
if (typeof pair.acquireEvent !== 'string' || pair.acquireEvent.length === 0)
|
|
@@ -323,6 +429,7 @@ export function validateModel(input) {
|
|
|
323
429
|
}
|
|
324
430
|
else {
|
|
325
431
|
const narrative = root.narrative;
|
|
432
|
+
rejectUnknownKeys(narrative, MODEL_KEYS.narrative, narrativePath);
|
|
326
433
|
const stateIds = new Set(Array.isArray(root.states) ? root.states.map((state) => state.id) : []);
|
|
327
434
|
const eventIds = new Set(Array.isArray(root.transitions) ? root.transitions.map((transition) => transition.event) : []);
|
|
328
435
|
const fromEventGroups = new Set(Array.isArray(root.transitions) ? root.transitions.map((transition) => transition.from + '|' + transition.event) : []);
|
|
@@ -368,6 +475,7 @@ export function validateModel(input) {
|
|
|
368
475
|
return;
|
|
369
476
|
}
|
|
370
477
|
const scenario = entry;
|
|
478
|
+
rejectUnknownKeys(scenario, MODEL_KEYS.scenario, scenarioPath);
|
|
371
479
|
if (typeof scenario.from !== 'string' || scenario.from.length === 0)
|
|
372
480
|
bad(scenarioPath + '.from', 'must be a non-empty string');
|
|
373
481
|
else if (!stateIds.has(scenario.from))
|
|
@@ -845,10 +953,18 @@ function S5_eventCompleteness(model) {
|
|
|
845
953
|
}
|
|
846
954
|
return checkResult('S5', 'Event Completeness', findings, findings.length === 0 ? 'All states handle all relevant events' : findings.length + ' unhandled (state, event) pairs');
|
|
847
955
|
}
|
|
848
|
-
|
|
956
|
+
/** Whether a `var-in-range` `when` scope selects this runtime state. */
|
|
957
|
+
function whenScopeHolds(when, runtime) {
|
|
958
|
+
return isStateGuard(when) ? when.state === runtime.state : evalGuard(when, runtime.vars);
|
|
959
|
+
}
|
|
960
|
+
function invariantHolds(invariant, runtime, isInitial = false) {
|
|
849
961
|
if (invariant.kind === 'never-states')
|
|
850
962
|
return !invariant.states.includes(runtime.state);
|
|
851
963
|
if (invariant.kind === 'var-in-range') {
|
|
964
|
+
// The initial runtime state is checked unconditionally: a machine must never be
|
|
965
|
+
// able to escape the range simply by starting outside the scope.
|
|
966
|
+
if (!isInitial && invariant.when !== undefined && !whenScopeHolds(invariant.when, runtime))
|
|
967
|
+
return true;
|
|
852
968
|
const value = runtime.vars[invariant.variable];
|
|
853
969
|
if (typeof value !== 'number')
|
|
854
970
|
return false;
|
|
@@ -865,7 +981,7 @@ function shortestViolationForInvariant(model, options, invariant) {
|
|
|
865
981
|
return shortestEventBeforeStateViolation(model, options, invariant);
|
|
866
982
|
}
|
|
867
983
|
const init = initialState(model);
|
|
868
|
-
if (!invariantHolds(invariant, init)) {
|
|
984
|
+
if (!invariantHolds(invariant, init, true)) {
|
|
869
985
|
return { invariant, path: [], reason: 'Initial state violates the invariant.' };
|
|
870
986
|
}
|
|
871
987
|
const visited = new Set([runtimeKey(init)]);
|
|
@@ -1375,6 +1491,15 @@ function mapInvariantForComparison(invariant, mapping) {
|
|
|
1375
1491
|
state: mapStateId(mapping, invariant.state),
|
|
1376
1492
|
};
|
|
1377
1493
|
}
|
|
1494
|
+
if (invariant.kind === 'var-in-range') {
|
|
1495
|
+
const mapped = { ...invariant, id: invariant.id + ':before', description: invariant.description + ' (from BEFORE)' };
|
|
1496
|
+
// A state scope must follow the state rename, otherwise D2 reports a spurious
|
|
1497
|
+
// regression when the scope later refers to a state id that no longer exists.
|
|
1498
|
+
if (mapped.when !== undefined && isStateGuard(mapped.when)) {
|
|
1499
|
+
mapped.when = { state: mapStateId(mapping, mapped.when.state) };
|
|
1500
|
+
}
|
|
1501
|
+
return mapped;
|
|
1502
|
+
}
|
|
1378
1503
|
return { ...invariant, id: invariant.id + ':before', description: invariant.description + ' (from BEFORE)' };
|
|
1379
1504
|
}
|
|
1380
1505
|
function D2_invariantContinuity(before, after, options, mapping) {
|
package/lib/index.js
CHANGED
|
@@ -43,23 +43,25 @@ export const inject = ['skills'];
|
|
|
43
43
|
// lands on `<package>/skills` regardless of where the package was installed.
|
|
44
44
|
const SKILLS_DIR = fileURLToPath(new URL('../skills', import.meta.url));
|
|
45
45
|
const GATE_PLUGIN_ID = 'logicprobe';
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
|
56
|
-
|
|
57
|
-
| "
|
|
58
|
-
| "I
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
**
|
|
46
|
+
/** Producer-owned message source kind declared in `MessageSourceMap` above. */
|
|
47
|
+
const GATE_SOURCE_KIND = 'plugin:logicprobe';
|
|
48
|
+
const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
|
|
49
|
+
Plugin logicprobe is active. Documents are not truth — code is. Verify every verifiable claim before accepting or acting on any design.
|
|
50
|
+
|
|
51
|
+
**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.
|
|
52
|
+
|
|
53
|
+
**Red Flags** — if you think any of these, STOP. You are rationalizing:
|
|
54
|
+
|
|
55
|
+
| You think | Reality |
|
|
56
|
+
|-----------|---------|
|
|
57
|
+
| "This plan is too simple to verify" | The skill auto-classifies depth (LIGHTWEIGHT / STANDARD / ESCALATED). You don't decide. |
|
|
58
|
+
| "I already know the file paths are correct" | Organic verification leaves no audit trail. Run Phase 0, append the "## Plan Verification" block. |
|
|
59
|
+
| "I'll verify while implementing" | Verification happens before implementation, not during. |
|
|
60
|
+
| "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
|
|
61
|
+
|
|
62
|
+
**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.
|
|
63
|
+
|
|
64
|
+
**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).
|
|
63
65
|
</EXTREMELY_IMPORTANT>`;
|
|
64
66
|
export const Config = z.object({
|
|
65
67
|
enabled: z.boolean().default(true),
|
|
@@ -70,7 +72,7 @@ function gateMessage(text) {
|
|
|
70
72
|
return createUserMessage({
|
|
71
73
|
content: [{ type: 'text', text }],
|
|
72
74
|
// `form` omitted — an undeclared context is the documented default.
|
|
73
|
-
source: { kind:
|
|
75
|
+
source: { kind: GATE_SOURCE_KIND },
|
|
74
76
|
});
|
|
75
77
|
}
|
|
76
78
|
function readSessionEvents(session) {
|
|
@@ -309,6 +311,11 @@ function gateInHistory(session) {
|
|
|
309
311
|
if (event.type !== 'user/message')
|
|
310
312
|
return false;
|
|
311
313
|
const source = event.data?.source;
|
|
312
|
-
|
|
314
|
+
if (source === undefined)
|
|
315
|
+
return false;
|
|
316
|
+
// The v4 producer-owned kind, plus the pre-v4 wrapper this bundle wrote
|
|
317
|
+
// before DSH 0.1.7-alpha.1 retired it.
|
|
318
|
+
return source.kind === GATE_SOURCE_KIND
|
|
319
|
+
|| (source.kind === 'plugin' && source.plugin === GATE_PLUGIN_ID);
|
|
313
320
|
});
|
|
314
321
|
}
|
package/lib/tool.js
CHANGED
|
@@ -12,7 +12,7 @@ export const LOGICPROBE_VERIFY_TOOL_NAME = 'logicprobe_verify';
|
|
|
12
12
|
*/
|
|
13
13
|
export const logicProbeVerifyTool = defineTool({
|
|
14
14
|
name: LOGICPROBE_VERIFY_TOOL_NAME,
|
|
15
|
-
description: '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, 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). 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-A13 adversarial/probability probes including shortest counterexample paths. If beforeModel is provided, also runs D1-D4 before/after regression checks. See skills/logicprobe/references/dsh-model-schema.md.',
|
|
15
|
+
description: '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-A13 adversarial/probability probes including shortest counterexample paths. If beforeModel is provided, also runs D1-D4 before/after regression checks. See skills/logicprobe/references/dsh-model-schema.md.',
|
|
16
16
|
parameters: {
|
|
17
17
|
model: {
|
|
18
18
|
type: 'json',
|
package/lib/types/engine.d.ts
CHANGED
|
@@ -65,6 +65,18 @@ export interface VariableSpec {
|
|
|
65
65
|
max?: number;
|
|
66
66
|
monotonic?: 'inc' | 'dec';
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Scope filter for a state-predicate invariant (see `InvariantSpec`): the range is
|
|
70
|
+
* only required to hold in runtime states that satisfy the filter. Exactly one of
|
|
71
|
+
* `state` / guard-node form must be present.
|
|
72
|
+
*/
|
|
73
|
+
export interface StateGuard {
|
|
74
|
+
/** Only runtime states whose id is this state are checked. */
|
|
75
|
+
state: string;
|
|
76
|
+
}
|
|
77
|
+
/** A `when` filter: either a single-state scope or the ordinary guard language. */
|
|
78
|
+
export type InvariantWhen = StateGuard | GuardNode;
|
|
79
|
+
export declare function isStateGuard(when: InvariantWhen): when is StateGuard;
|
|
68
80
|
export type InvariantSpec = {
|
|
69
81
|
id: string;
|
|
70
82
|
description: string;
|
|
@@ -77,6 +89,16 @@ export type InvariantSpec = {
|
|
|
77
89
|
variable: string;
|
|
78
90
|
min?: number;
|
|
79
91
|
max?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Optional scope. `when` is evaluated against the POST-state of every transition
|
|
94
|
+
* (and, for the `{ state }` form, against the initial state regardless), so a
|
|
95
|
+
* `{ state }` scope is sound: for every reachable runtime state inside the scope
|
|
96
|
+
* the variable is in range. A guard-node scope that references the constrained
|
|
97
|
+
* variable can mask its own violation — prefer `{ state }` or an independent
|
|
98
|
+
* control variable. Not offered on trace-property kinds (leads-to, sequence, ...),
|
|
99
|
+
* whose scope over a path would be ambiguous.
|
|
100
|
+
*/
|
|
101
|
+
when?: InvariantWhen;
|
|
80
102
|
} | {
|
|
81
103
|
id: string;
|
|
82
104
|
description: string;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -26,6 +26,14 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { Context } from '@deepseek-ai/cordis';
|
|
28
28
|
import z from '@deepseek-ai/schemastery';
|
|
29
|
+
import type { ContextFormed } from '@deepseek-ai/dsh-llm';
|
|
30
|
+
declare module '@deepseek-ai/dsh-llm' {
|
|
31
|
+
interface MessageSourceMap {
|
|
32
|
+
'plugin:logicprobe': {
|
|
33
|
+
kind: 'plugin:logicprobe';
|
|
34
|
+
} & ContextFormed;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
29
37
|
export declare const name = "logicprobe";
|
|
30
38
|
export declare const inject: string[];
|
|
31
39
|
export type InteractionMode = 'ask' | 'auto' | 'follow-approval';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-logicprobe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Design document & plan claim verification — enumerate claims, verify against codebase facts, then escalate to state-machine verification (S1-S8/A1-A12, including budget/worst-case path-cost checks) and data-model verification (DS/DA/DD) for behavioral claims. Supports before/after regression, idempotency/monotonic/sequence/leads-to/atomicity constraints, and concurrency risk mining. Ships a native DeepSeek Harness (dsh) bundle that injects the claim-verification gate into the first model step.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"patch": "./cordis.patch.yml"
|
|
30
30
|
},
|
|
31
31
|
"compatibility": {
|
|
32
|
-
"dsh": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || ^0.1.2-alpha.3 || ^0.1.2-alpha.4 || ^0.1.2-alpha.5 || ^0.1.2-rc.1 || ^0.1.3-alpha.1 || ^0.1.3-alpha.2 || ^0.1.5-alpha.1 || ^0.1.5-rc.1 || ^0.1.5-alpha.2 || ^0.1.5-rc.2 || ^0.1.6-alpha.1 || ^0.1.6-alpha.2",
|
|
32
|
+
"dsh": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || ^0.1.2-alpha.3 || ^0.1.2-alpha.4 || ^0.1.2-alpha.5 || ^0.1.2-rc.1 || ^0.1.3-alpha.1 || ^0.1.3-alpha.2 || ^0.1.5-alpha.1 || ^0.1.5-rc.1 || ^0.1.5-alpha.2 || ^0.1.5-rc.2 || ^0.1.5-rc.3 || ^0.1.6-alpha.1 || ^0.1.6-alpha.2 || ^0.1.7-alpha.1 || ^0.1.7-alpha.2 || ^0.1.7-rc.1 || ^0.1.7-rc.2",
|
|
33
33
|
"dshReleases": {
|
|
34
34
|
"0.1.0-rc.7": "compatible",
|
|
35
35
|
"0.1.0-rc.8": "compatible",
|
|
@@ -46,8 +46,13 @@
|
|
|
46
46
|
"0.1.5-alpha.2": "compatible",
|
|
47
47
|
"0.1.5-rc.1": "compatible",
|
|
48
48
|
"0.1.5-rc.2": "compatible",
|
|
49
|
+
"0.1.5-rc.3": "compatible",
|
|
49
50
|
"0.1.6-alpha.1": "compatible",
|
|
50
|
-
"0.1.6-alpha.2": "compatible"
|
|
51
|
+
"0.1.6-alpha.2": "compatible",
|
|
52
|
+
"0.1.7-alpha.1": "compatible",
|
|
53
|
+
"0.1.7-alpha.2": "compatible",
|
|
54
|
+
"0.1.7-rc.1": "compatible",
|
|
55
|
+
"0.1.7-rc.2": "compatible"
|
|
51
56
|
},
|
|
52
57
|
"profiles": [
|
|
53
58
|
"headless"
|
|
@@ -85,7 +90,7 @@
|
|
|
85
90
|
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.6",
|
|
86
91
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
87
92
|
"@deepseek-ai/schemastery": "^3.18.2",
|
|
88
|
-
"@types/node": "^26.
|
|
93
|
+
"@types/node": "^26.6.2",
|
|
89
94
|
"typescript": "^7.0.2"
|
|
90
95
|
},
|
|
91
96
|
"author": {
|