dsh-logicprobe 0.4.0 → 0.5.1
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 +10 -3
- package/README.md +10 -3
- package/lib/concurrency-tool.js +34 -0
- package/lib/concurrency.js +76 -0
- package/lib/data-engine.js +930 -0
- package/lib/data-tool.js +61 -0
- package/lib/engine.js +258 -0
- package/lib/index.js +37 -23
- package/lib/tool.js +1 -1
- package/lib/types/concurrency-tool.d.ts +8 -0
- package/lib/types/concurrency.d.ts +20 -0
- package/lib/types/data-engine.d.ts +199 -0
- package/lib/types/data-tool.d.ts +10 -0
- package/lib/types/engine.d.ts +20 -0
- package/package.json +82 -81
- package/skills/logicprobe/SKILL.md +285 -268
- package/skills/logicprobe/references/__pycache__/verification-harness.cpython-312.pyc +0 -0
- package/skills/logicprobe/references/concurrency-risk-guide.md +54 -0
- package/skills/logicprobe/references/dsh-model-schema.md +145 -129
- package/skills/logicprobe/references/logic-verification-guide.md +463 -413
- package/skills/logicprobe/references/verification-harness.py +806 -582
- package/skills/logicprobe-datamodel/SKILL.md +124 -0
- package/skills/logicprobe-datamodel/references/__pycache__/data-model-harness.cpython-312.pyc +0 -0
- package/skills/logicprobe-datamodel/references/data-model-guide.md +62 -0
- package/skills/logicprobe-datamodel/references/data-model-harness.py +528 -0
- package/skills/logicprobe-datamodel/references/data-model-schema.md +128 -0
- package/src/concurrency-tool.ts +37 -0
- package/src/concurrency.ts +102 -0
- package/src/data-engine.ts +1001 -0
- package/src/data-tool.ts +65 -0
- package/src/engine.ts +234 -0
- package/src/index.ts +315 -301
- package/src/tool.ts +60 -60
package/lib/data-tool.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { runDataVerification, DATA_ENGINE_SCHEMA_VERSION } from './data-engine.js';
|
|
3
|
+
export const LOGICPROBE_DATAMODEL_VERIFY_TOOL_NAME = 'logicprobe_datamodel_verify';
|
|
4
|
+
/**
|
|
5
|
+
* Model-visible DSH tool wrapping the bundled data-model verification engine.
|
|
6
|
+
* The model passes a DataModelV1 object; the engine validates it and returns
|
|
7
|
+
* DS/DA/DD checks. Optional beforeModel/fieldMapping/copyPairs/migrationMappings
|
|
8
|
+
* enable before/after data-model regression and migration coverage checks.
|
|
9
|
+
*/
|
|
10
|
+
export const logicProbeDataModelVerifyTool = defineTool({
|
|
11
|
+
name: LOGICPROBE_DATAMODEL_VERIFY_TOOL_NAME,
|
|
12
|
+
description: 'Run executable data-model verification (logicprobe-datamodel). Takes a DataModelV1 object with schemaVersion=1, entities ({name, fields, primaryKey?, uniqueKeys?, indexes?}), relationships?, invariants?, boundaryChecks?. Optional beforeModel/fieldMapping/copyPairs/migrationMappings/backupPairs enable migration coverage, copy consistency, rollback symmetry, and before/after data regression (DD1-DD4). Returns a report with DS/DA/DD checks. See skills/logicprobe-datamodel/references/data-model-schema.md.',
|
|
13
|
+
parameters: {
|
|
14
|
+
model: {
|
|
15
|
+
type: 'json',
|
|
16
|
+
required: true,
|
|
17
|
+
description: 'DataModelV1 data model to verify.',
|
|
18
|
+
},
|
|
19
|
+
beforeModel: {
|
|
20
|
+
type: 'json',
|
|
21
|
+
description: 'Optional BEFORE DataModelV1 data model for regression comparison.',
|
|
22
|
+
},
|
|
23
|
+
fieldMapping: {
|
|
24
|
+
type: 'json',
|
|
25
|
+
description: 'Optional object mapping BEFORE "Entity.field" paths to AFTER "Entity.field" paths.',
|
|
26
|
+
},
|
|
27
|
+
copyPairs: {
|
|
28
|
+
type: 'json',
|
|
29
|
+
description: 'Optional copy pairs: { id, sourceEntity, targetEntity, mapping: {sourceField: targetField} }.',
|
|
30
|
+
},
|
|
31
|
+
migrationMappings: {
|
|
32
|
+
type: 'json',
|
|
33
|
+
description: 'Optional migration mappings: { from: "Entity.field", to: "Entity.field", transform?, note? }.',
|
|
34
|
+
},
|
|
35
|
+
backupPairs: {
|
|
36
|
+
type: 'json',
|
|
37
|
+
description: 'Optional backup/restore pairs for DA7 rollback symmetry.',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
output: {
|
|
41
|
+
schema: {
|
|
42
|
+
type: 'json',
|
|
43
|
+
description: 'logicprobe-datamodel verification report with summary and per-check findings.',
|
|
44
|
+
},
|
|
45
|
+
render(_args, value) {
|
|
46
|
+
return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
timeoutMs: 10_000,
|
|
50
|
+
isConcurrencySafe: () => true,
|
|
51
|
+
async execute(args) {
|
|
52
|
+
return runDataVerification(args.model, {
|
|
53
|
+
beforeModel: args.beforeModel,
|
|
54
|
+
fieldMapping: args.fieldMapping,
|
|
55
|
+
copyPairs: args.copyPairs,
|
|
56
|
+
migrationMappings: args.migrationMappings,
|
|
57
|
+
backupPairs: args.backupPairs,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
export { DATA_ENGINE_SCHEMA_VERSION };
|
package/lib/engine.js
CHANGED
|
@@ -123,6 +123,8 @@ export function validateModel(input) {
|
|
|
123
123
|
bad(path + '.max', 'must be a number');
|
|
124
124
|
if (typeof variable.min === 'number' && typeof variable.max === 'number' && variable.min > variable.max)
|
|
125
125
|
bad(path + '.max', 'must be >= min');
|
|
126
|
+
if (variable.monotonic !== undefined && variable.monotonic !== 'inc' && variable.monotonic !== 'dec')
|
|
127
|
+
bad(path + '.monotonic', "must be 'inc' or 'dec'");
|
|
126
128
|
if (variable.kind === 'integer' && typeof variable.init === 'number') {
|
|
127
129
|
if (typeof variable.min === 'number' && variable.init < variable.min)
|
|
128
130
|
bad(path + '.init', 'must be >= min');
|
|
@@ -174,6 +176,34 @@ export function validateModel(input) {
|
|
|
174
176
|
if (typeof invariant.state !== 'string' || invariant.state.length === 0)
|
|
175
177
|
bad(path + '.state', 'must be a non-empty string');
|
|
176
178
|
}
|
|
179
|
+
else if (invariant.kind === 'leads-to') {
|
|
180
|
+
if (typeof invariant.from !== 'string' || invariant.from.length === 0)
|
|
181
|
+
bad(path + '.from', 'must be a non-empty string');
|
|
182
|
+
if (typeof invariant.to !== 'string' || invariant.to.length === 0)
|
|
183
|
+
bad(path + '.to', 'must be a non-empty string');
|
|
184
|
+
}
|
|
185
|
+
else if (invariant.kind === 'sequence') {
|
|
186
|
+
if (!Array.isArray(invariant.events) || invariant.events.length === 0)
|
|
187
|
+
bad(path + '.events', 'must be a non-empty array');
|
|
188
|
+
else
|
|
189
|
+
invariant.events.forEach((event, eventIndex) => {
|
|
190
|
+
if (typeof event !== 'string' || event.length === 0)
|
|
191
|
+
bad(path + '.events[' + eventIndex + ']', 'must be a non-empty string');
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
else if (invariant.kind === 'atomicity') {
|
|
195
|
+
if (!Array.isArray(invariant.events) || invariant.events.length === 0)
|
|
196
|
+
bad(path + '.events', 'must be a non-empty array');
|
|
197
|
+
else
|
|
198
|
+
invariant.events.forEach((event, eventIndex) => {
|
|
199
|
+
if (typeof event !== 'string' || event.length === 0)
|
|
200
|
+
bad(path + '.events[' + eventIndex + ']', 'must be a non-empty string');
|
|
201
|
+
});
|
|
202
|
+
if (typeof invariant.commit !== 'string' || invariant.commit.length === 0)
|
|
203
|
+
bad(path + '.commit', 'must be a non-empty string');
|
|
204
|
+
if (invariant.rollback !== undefined && typeof invariant.rollback !== 'string')
|
|
205
|
+
bad(path + '.rollback', 'must be a string');
|
|
206
|
+
}
|
|
177
207
|
else {
|
|
178
208
|
bad(path + '.kind', 'unknown invariant kind');
|
|
179
209
|
}
|
|
@@ -206,6 +236,15 @@ export function validateModel(input) {
|
|
|
206
236
|
bad(path + '.values', 'must be an array of numbers');
|
|
207
237
|
});
|
|
208
238
|
}
|
|
239
|
+
if (root.idempotentEvents !== undefined) {
|
|
240
|
+
if (!Array.isArray(root.idempotentEvents))
|
|
241
|
+
bad('idempotentEvents', 'must be an array');
|
|
242
|
+
else
|
|
243
|
+
root.idempotentEvents.forEach((entry, index) => {
|
|
244
|
+
if (typeof entry !== 'string' || entry.length === 0)
|
|
245
|
+
bad('idempotentEvents[' + index + ']', 'must be a non-empty string');
|
|
246
|
+
});
|
|
247
|
+
}
|
|
209
248
|
if (root.resourcePairs !== undefined) {
|
|
210
249
|
if (!Array.isArray(root.resourcePairs))
|
|
211
250
|
bad('resourcePairs', 'must be an array');
|
|
@@ -1269,6 +1308,220 @@ function buildComparisonSummary(before, after, mapping) {
|
|
|
1269
1308
|
removedTransitions,
|
|
1270
1309
|
};
|
|
1271
1310
|
}
|
|
1311
|
+
function A8_idempotentReplay(model, exploration) {
|
|
1312
|
+
const events = model.idempotentEvents ?? [];
|
|
1313
|
+
const findings = [];
|
|
1314
|
+
for (const event of events) {
|
|
1315
|
+
for (const runtime of exploration.reachable) {
|
|
1316
|
+
const onceOptions = stepRuntime(model, runtime, event);
|
|
1317
|
+
if (onceOptions.length === 0)
|
|
1318
|
+
continue;
|
|
1319
|
+
for (const once of onceOptions) {
|
|
1320
|
+
const twiceOptions = stepRuntime(model, once, event);
|
|
1321
|
+
if (twiceOptions.length === 0) {
|
|
1322
|
+
findings.push({
|
|
1323
|
+
code: 'A8_NOT_REPLAYABLE',
|
|
1324
|
+
severity: 'warning',
|
|
1325
|
+
message: 'Idempotent event ' + event + ' is not replayable after first application from ' + runtime.state + '.',
|
|
1326
|
+
path: [{ from: runtime.state, event, to: once.state }],
|
|
1327
|
+
evidence: { state: runtime.state, event },
|
|
1328
|
+
});
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
for (const twice of twiceOptions) {
|
|
1332
|
+
if (runtimeKey(twice) !== runtimeKey(once)) {
|
|
1333
|
+
findings.push({
|
|
1334
|
+
code: 'A8_NOT_IDEMPOTENT',
|
|
1335
|
+
severity: 'error',
|
|
1336
|
+
message: 'Idempotent event ' + event + ' changes state when applied twice from ' + runtime.state + '.',
|
|
1337
|
+
path: [{ from: runtime.state, event, to: once.state }, { from: once.state, event, to: twice.state }],
|
|
1338
|
+
evidence: { state: runtime.state, event, afterOnce: once, afterTwice: twice },
|
|
1339
|
+
});
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return checkResult('A8', 'Idempotent Replay', findings, findings.length === 0 ? 'Idempotent events are replay-safe' : 'Idempotent replay findings: ' + findings.length);
|
|
1347
|
+
}
|
|
1348
|
+
function S8_monotonicVariables(model) {
|
|
1349
|
+
const findings = [];
|
|
1350
|
+
for (const variable of model.variables ?? []) {
|
|
1351
|
+
if (variable.monotonic === undefined)
|
|
1352
|
+
continue;
|
|
1353
|
+
for (const transition of model.transitions) {
|
|
1354
|
+
for (const update of transition.updates ?? []) {
|
|
1355
|
+
if (update.variable !== variable.name)
|
|
1356
|
+
continue;
|
|
1357
|
+
if (variable.monotonic === 'inc' && update.op === 'dec') {
|
|
1358
|
+
findings.push({ code: 'S8_MONOTONIC_DECREASE', severity: 'error', message: 'Monotonic (inc) variable ' + variable.name + ' is decreased by ' + transition.event + '.', evidence: { variable: variable.name, transition } });
|
|
1359
|
+
}
|
|
1360
|
+
else if (variable.monotonic === 'dec' && update.op === 'inc') {
|
|
1361
|
+
findings.push({ code: 'S8_MONOTONIC_INCREASE', severity: 'error', message: 'Monotonic (dec) variable ' + variable.name + ' is increased by ' + transition.event + '.', evidence: { variable: variable.name, transition } });
|
|
1362
|
+
}
|
|
1363
|
+
else if (update.op === 'set') {
|
|
1364
|
+
findings.push({ code: 'S8_MONOTONIC_SET_REVIEW', severity: 'warning', message: 'Monotonic variable ' + variable.name + ' uses set in ' + transition.event + '; verify it cannot move backwards.', evidence: { variable: variable.name, transition } });
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return checkResult('S8', 'Monotonic Variables', findings, findings.length === 0 ? 'Monotonic variables are respected' : 'Monotonic findings: ' + findings.length);
|
|
1370
|
+
}
|
|
1371
|
+
function findLeadsToBadPath(model, start, target) {
|
|
1372
|
+
if (start.state === target)
|
|
1373
|
+
return undefined;
|
|
1374
|
+
const visited = new Set();
|
|
1375
|
+
const queue = [{ runtime: start, path: [] }];
|
|
1376
|
+
while (queue.length > 0) {
|
|
1377
|
+
const entry = queue.shift();
|
|
1378
|
+
const key = runtimeKey(entry.runtime);
|
|
1379
|
+
if (entry.runtime.state === target)
|
|
1380
|
+
continue;
|
|
1381
|
+
if (visited.has(key))
|
|
1382
|
+
return { path: entry.path, reason: 'Cycle avoids target ' + target };
|
|
1383
|
+
visited.add(key);
|
|
1384
|
+
const nexts = [];
|
|
1385
|
+
for (const event of allEvents(model)) {
|
|
1386
|
+
for (const next of stepRuntime(model, entry.runtime, event))
|
|
1387
|
+
nexts.push({ next, event });
|
|
1388
|
+
}
|
|
1389
|
+
if (nexts.length === 0)
|
|
1390
|
+
return { path: entry.path, reason: 'Dead end before target ' + target };
|
|
1391
|
+
for (const { next, event } of nexts) {
|
|
1392
|
+
queue.push({ runtime: next, path: [...entry.path, { from: entry.runtime.state, event, to: next.state }] });
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return { path: [], reason: 'No path reaches target ' + target };
|
|
1396
|
+
}
|
|
1397
|
+
function A9_leadsTo(model, exploration) {
|
|
1398
|
+
const findings = [];
|
|
1399
|
+
for (const invariant of model.invariants ?? []) {
|
|
1400
|
+
if (invariant.kind !== 'leads-to')
|
|
1401
|
+
continue;
|
|
1402
|
+
for (const runtime of exploration.reachable) {
|
|
1403
|
+
if (runtime.state !== invariant.from)
|
|
1404
|
+
continue;
|
|
1405
|
+
const bad = findLeadsToBadPath(model, runtime, invariant.to);
|
|
1406
|
+
if (bad !== undefined) {
|
|
1407
|
+
findings.push({
|
|
1408
|
+
code: 'A9_LEADS_TO_VIOLATION',
|
|
1409
|
+
severity: 'error',
|
|
1410
|
+
message: 'Leads-to invariant "' + invariant.id + '" violated from ' + invariant.from + ': ' + bad.reason,
|
|
1411
|
+
path: bad.path,
|
|
1412
|
+
evidence: { invariant },
|
|
1413
|
+
});
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return checkResult('A9', 'Leads-To', findings, findings.length === 0 ? 'All leads-to invariants hold' : 'Leads-to findings: ' + findings.length);
|
|
1419
|
+
}
|
|
1420
|
+
function findSequenceViolation(model, options, invariant) {
|
|
1421
|
+
const events = invariant.events;
|
|
1422
|
+
const init = initialState(model);
|
|
1423
|
+
const key = (runtime, progress) => runtimeKey(runtime) + '|' + progress;
|
|
1424
|
+
const visited = new Set([key(init, 0)]);
|
|
1425
|
+
const queue = [{ runtime: init, progress: 0, path: [] }];
|
|
1426
|
+
let steps = 0;
|
|
1427
|
+
while (queue.length > 0) {
|
|
1428
|
+
const entry = queue.shift();
|
|
1429
|
+
if (++steps > options.maxStates)
|
|
1430
|
+
break;
|
|
1431
|
+
for (const event of allEvents(model)) {
|
|
1432
|
+
for (const next of stepRuntime(model, entry.runtime, event)) {
|
|
1433
|
+
let progress = entry.progress;
|
|
1434
|
+
let violation = false;
|
|
1435
|
+
if (progress < events.length && event === events[progress]) {
|
|
1436
|
+
progress += 1;
|
|
1437
|
+
}
|
|
1438
|
+
else {
|
|
1439
|
+
const index = events.indexOf(event);
|
|
1440
|
+
if (index > progress)
|
|
1441
|
+
violation = true;
|
|
1442
|
+
}
|
|
1443
|
+
const path = [...entry.path, { from: entry.runtime.state, event, to: next.state }];
|
|
1444
|
+
if (violation) {
|
|
1445
|
+
return { invariant, path, reason: 'Event ' + event + ' occurred before ' + events[progress] };
|
|
1446
|
+
}
|
|
1447
|
+
const nextKey = key(next, progress);
|
|
1448
|
+
if (!visited.has(nextKey)) {
|
|
1449
|
+
visited.add(nextKey);
|
|
1450
|
+
queue.push({ runtime: next, progress, path });
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return undefined;
|
|
1456
|
+
}
|
|
1457
|
+
function A10_sequenceOrder(model, options) {
|
|
1458
|
+
const findings = [];
|
|
1459
|
+
for (const invariant of model.invariants ?? []) {
|
|
1460
|
+
if (invariant.kind !== 'sequence')
|
|
1461
|
+
continue;
|
|
1462
|
+
const violation = findSequenceViolation(model, options, invariant);
|
|
1463
|
+
if (violation !== undefined) {
|
|
1464
|
+
findings.push({
|
|
1465
|
+
code: 'A10_SEQUENCE_VIOLATION',
|
|
1466
|
+
severity: 'error',
|
|
1467
|
+
message: 'Sequence invariant "' + invariant.id + '" violated: ' + violation.reason,
|
|
1468
|
+
path: violation.path,
|
|
1469
|
+
evidence: { invariant },
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return checkResult('A10', 'Sequence Order', findings, findings.length === 0 ? 'All sequence invariants hold' : 'Sequence findings: ' + findings.length);
|
|
1474
|
+
}
|
|
1475
|
+
function findAtomicityViolation(model, options, invariant) {
|
|
1476
|
+
const atomic = new Set(invariant.events);
|
|
1477
|
+
const init = initialState(model);
|
|
1478
|
+
const key = (runtime, started, closed) => runtimeKey(runtime) + '|' + (started ? '1' : '0') + '|' + (closed ? '1' : '0');
|
|
1479
|
+
const visited = new Set([key(init, false, false)]);
|
|
1480
|
+
const queue = [{ runtime: init, started: false, closed: false, path: [] }];
|
|
1481
|
+
let steps = 0;
|
|
1482
|
+
while (queue.length > 0) {
|
|
1483
|
+
const entry = queue.shift();
|
|
1484
|
+
if (++steps > options.maxStates)
|
|
1485
|
+
break;
|
|
1486
|
+
for (const event of allEvents(model)) {
|
|
1487
|
+
for (const next of stepRuntime(model, entry.runtime, event)) {
|
|
1488
|
+
const started = entry.started || atomic.has(event);
|
|
1489
|
+
const closed = entry.closed || event === invariant.commit || (invariant.rollback !== undefined && event === invariant.rollback);
|
|
1490
|
+
const path = [...entry.path, { from: entry.runtime.state, event, to: next.state }];
|
|
1491
|
+
if (entry.started && !entry.closed && !atomic.has(event) && event !== invariant.commit && event !== invariant.rollback) {
|
|
1492
|
+
return { invariant, path, reason: 'Left atomic scope via ' + event + ' without commit/rollback' };
|
|
1493
|
+
}
|
|
1494
|
+
if (started && !closed && isTerminal(model, next.state)) {
|
|
1495
|
+
return { invariant, path, reason: 'Terminal state reached with incomplete atomic group' };
|
|
1496
|
+
}
|
|
1497
|
+
const nextKey = key(next, started, closed);
|
|
1498
|
+
if (!visited.has(nextKey)) {
|
|
1499
|
+
visited.add(nextKey);
|
|
1500
|
+
queue.push({ runtime: next, started, closed, path });
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return undefined;
|
|
1506
|
+
}
|
|
1507
|
+
function A11_atomicity(model, options) {
|
|
1508
|
+
const findings = [];
|
|
1509
|
+
for (const invariant of model.invariants ?? []) {
|
|
1510
|
+
if (invariant.kind !== 'atomicity')
|
|
1511
|
+
continue;
|
|
1512
|
+
const violation = findAtomicityViolation(model, options, invariant);
|
|
1513
|
+
if (violation !== undefined) {
|
|
1514
|
+
findings.push({
|
|
1515
|
+
code: 'A11_ATOMICITY_VIOLATION',
|
|
1516
|
+
severity: 'error',
|
|
1517
|
+
message: 'Atomicity invariant "' + invariant.id + '" violated: ' + violation.reason,
|
|
1518
|
+
path: violation.path,
|
|
1519
|
+
evidence: { invariant },
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return checkResult('A11', 'Atomicity', findings, findings.length === 0 ? 'All atomicity invariants hold' : 'Atomicity findings: ' + findings.length);
|
|
1524
|
+
}
|
|
1272
1525
|
// ---------------------------------------------------------------------------
|
|
1273
1526
|
// main entry
|
|
1274
1527
|
// ---------------------------------------------------------------------------
|
|
@@ -1303,6 +1556,7 @@ export function runVerification(input, options = {}) {
|
|
|
1303
1556
|
S5_eventCompleteness(model),
|
|
1304
1557
|
S6_guardCompleteness(model),
|
|
1305
1558
|
S7_invariants(model, normalized),
|
|
1559
|
+
S8_monotonicVariables(model),
|
|
1306
1560
|
A1_unexpectedEvents(model),
|
|
1307
1561
|
A2_raceInterleaving(model, exploration),
|
|
1308
1562
|
A3_orderPermutation(model, normalized),
|
|
@@ -1310,6 +1564,10 @@ export function runVerification(input, options = {}) {
|
|
|
1310
1564
|
A5_boundaryBlast(model),
|
|
1311
1565
|
A6_resourceInjection(model),
|
|
1312
1566
|
A7_shortestViolations(model, normalized),
|
|
1567
|
+
A8_idempotentReplay(model, exploration),
|
|
1568
|
+
A9_leadsTo(model, exploration),
|
|
1569
|
+
A10_sequenceOrder(model, normalized),
|
|
1570
|
+
A11_atomicity(model, normalized),
|
|
1313
1571
|
];
|
|
1314
1572
|
let comparison;
|
|
1315
1573
|
if (options.beforeModel !== undefined) {
|
package/lib/index.js
CHANGED
|
@@ -29,6 +29,8 @@ import z from '@deepseek-ai/schemastery';
|
|
|
29
29
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
30
30
|
import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem';
|
|
31
31
|
import { logicProbeVerifyTool } from './tool.js';
|
|
32
|
+
import { logicProbeDataModelVerifyTool, DATA_ENGINE_SCHEMA_VERSION } from './data-tool.js';
|
|
33
|
+
import { logicProbeConcurrencyScanTool } from './concurrency-tool.js';
|
|
32
34
|
import { ENGINE_SCHEMA_VERSION } from './engine.js';
|
|
33
35
|
export const name = 'logicprobe';
|
|
34
36
|
// Skills are contributed through the registry service, which dsh-base always
|
|
@@ -39,23 +41,23 @@ export const inject = ['skills'];
|
|
|
39
41
|
// lands on `<package>/skills` regardless of where the package was installed.
|
|
40
42
|
const SKILLS_DIR = fileURLToPath(new URL('../skills', import.meta.url));
|
|
41
43
|
const GATE_PLUGIN_ID = 'logicprobe';
|
|
42
|
-
const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
|
|
43
|
-
Plugin logicprobe is active. Documents are not truth — code is. Verify every verifiable claim before accepting or acting on any design.
|
|
44
|
-
|
|
45
|
-
**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, 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.
|
|
46
|
-
|
|
47
|
-
**Red Flags** — if you think any of these, STOP. You are rationalizing:
|
|
48
|
-
|
|
49
|
-
| You think | Reality |
|
|
50
|
-
|-----------|---------|
|
|
51
|
-
| "This plan is too simple to verify" | The skill auto-classifies depth (LIGHTWEIGHT / STANDARD / ESCALATED). You don't decide. |
|
|
52
|
-
| "I already know the file paths are correct" | Organic verification leaves no audit trail. Run Phase 0, append the "## Plan Verification" block. |
|
|
53
|
-
| "I'll verify while implementing" | Verification happens before implementation, not during. |
|
|
54
|
-
| "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
|
|
55
|
-
|
|
56
|
-
**Native verification path**: In dsh, prefer the \`logicprobe_verify\` tool for
|
|
57
|
-
|
|
58
|
-
**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" — suggest logicprobe as an optional verification pass (do not auto-escalate).
|
|
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).
|
|
59
61
|
</EXTREMELY_IMPORTANT>`;
|
|
60
62
|
export const Config = z.object({
|
|
61
63
|
enabled: z.boolean().default(true),
|
|
@@ -96,7 +98,7 @@ function resolveInteraction(config, session) {
|
|
|
96
98
|
function modeContextText(config, session) {
|
|
97
99
|
const interaction = resolveInteraction(config, session);
|
|
98
100
|
const lines = [
|
|
99
|
-
'logicprobe: use
|
|
101
|
+
'logicprobe: use `logicprobe_verify` for state machines and `logicprobe_datamodel_verify` for data models; both cover before/after regression and common domain constraints.',
|
|
100
102
|
interaction === 'auto'
|
|
101
103
|
? '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.'
|
|
102
104
|
: 'logicprobe interaction=ask: show the extracted transition table and get user confirmation before running verification.',
|
|
@@ -111,7 +113,7 @@ function modeContextText(config, session) {
|
|
|
111
113
|
* lets the model read this plugin's runtime status without guessing. Mirrors
|
|
112
114
|
* the registration pattern of the official dsh-tool-cordis host providers.
|
|
113
115
|
*/
|
|
114
|
-
function inspectProvider(config, isToolRegistered) {
|
|
116
|
+
function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcurrencyToolRegistered) {
|
|
115
117
|
return {
|
|
116
118
|
manifest: {
|
|
117
119
|
id: 'logicprobe',
|
|
@@ -133,9 +135,12 @@ function inspectProvider(config, isToolRegistered) {
|
|
|
133
135
|
gateContentLength: { type: 'integer', description: 'Length in characters of the injected gate text.' },
|
|
134
136
|
interaction: { type: 'string', enum: ['ask', 'auto', 'follow-approval'], description: 'Configured interaction mode. follow-approval resolves per session from approval/policy.' },
|
|
135
137
|
toolRegistered: { type: 'boolean', description: 'Whether the logicprobe_verify tool is registered on ctx.tools.' },
|
|
136
|
-
|
|
138
|
+
dataToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_datamodel_verify tool is registered on ctx.tools.' },
|
|
139
|
+
concurrencyToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_concurrency_scan tool is registered on ctx.tools.' },
|
|
140
|
+
engineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled state-machine verification engine accepts.' },
|
|
141
|
+
dataEngineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled data-model verification engine accepts.' },
|
|
137
142
|
},
|
|
138
|
-
required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'engineSchemaVersion'],
|
|
143
|
+
required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
|
|
139
144
|
additionalProperties: false,
|
|
140
145
|
},
|
|
141
146
|
},
|
|
@@ -148,7 +153,10 @@ function inspectProvider(config, isToolRegistered) {
|
|
|
148
153
|
gateContentLength: config.gateContent.length,
|
|
149
154
|
interaction: config.interaction,
|
|
150
155
|
toolRegistered: isToolRegistered(),
|
|
156
|
+
dataToolRegistered: isDataToolRegistered(),
|
|
157
|
+
concurrencyToolRegistered: isConcurrencyToolRegistered(),
|
|
151
158
|
engineSchemaVersion: ENGINE_SCHEMA_VERSION,
|
|
159
|
+
dataEngineSchemaVersion: DATA_ENGINE_SCHEMA_VERSION,
|
|
152
160
|
};
|
|
153
161
|
}
|
|
154
162
|
return null;
|
|
@@ -161,6 +169,8 @@ export function apply(ctx, config) {
|
|
|
161
169
|
// agent/pre-step — by then the app is fully booted.
|
|
162
170
|
let providerRegistered = false;
|
|
163
171
|
let toolRegistered = false;
|
|
172
|
+
let dataToolRegistered = false;
|
|
173
|
+
let concurrencyToolRegistered = false;
|
|
164
174
|
let modeContextRegistered = false;
|
|
165
175
|
const registerProvider = () => {
|
|
166
176
|
if (providerRegistered)
|
|
@@ -169,7 +179,7 @@ export function apply(ctx, config) {
|
|
|
169
179
|
if (inspect === undefined)
|
|
170
180
|
return;
|
|
171
181
|
try {
|
|
172
|
-
ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered)), 'logicprobe: inspect provider');
|
|
182
|
+
ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered)), 'logicprobe: inspect provider');
|
|
173
183
|
providerRegistered = true;
|
|
174
184
|
}
|
|
175
185
|
catch (err) {
|
|
@@ -184,10 +194,14 @@ export function apply(ctx, config) {
|
|
|
184
194
|
return;
|
|
185
195
|
try {
|
|
186
196
|
ctx.effect(() => tools.register(logicProbeVerifyTool), 'logicprobe: verify tool');
|
|
197
|
+
ctx.effect(() => tools.register(logicProbeDataModelVerifyTool), 'logicprobe: data verify tool');
|
|
198
|
+
ctx.effect(() => tools.register(logicProbeConcurrencyScanTool), 'logicprobe: concurrency scan tool');
|
|
187
199
|
toolRegistered = true;
|
|
200
|
+
dataToolRegistered = true;
|
|
201
|
+
concurrencyToolRegistered = true;
|
|
188
202
|
}
|
|
189
203
|
catch (err) {
|
|
190
|
-
console.warn('[logicprobe] logicprobe_verify tool registration failed', err);
|
|
204
|
+
console.warn('[logicprobe] logicprobe_verify/logicprobe_datamodel_verify tool registration failed', err);
|
|
191
205
|
}
|
|
192
206
|
};
|
|
193
207
|
const registerModeContext = () => {
|
package/lib/tool.js
CHANGED
|
@@ -11,7 +11,7 @@ export const LOGICPROBE_VERIFY_TOOL_NAME = 'logicprobe_verify';
|
|
|
11
11
|
*/
|
|
12
12
|
export const logicProbeVerifyTool = defineTool({
|
|
13
13
|
name: LOGICPROBE_VERIFY_TOOL_NAME,
|
|
14
|
-
description: 'Run executable state-machine verification (logicprobe). Takes a LogicModelV1 object with schemaVersion=1, init, states ({id, terminal?}), transitions ({from, event, to, guard?, updates?}), variables?, invariants?, concurrentPairs?, boundaryChecks?, resourcePairs?. Guards are structured ({variable, op, value} | {all} | {any} | {not}); invariants support never-states, var-in-range,
|
|
14
|
+
description: 'Run executable state-machine verification (logicprobe). Takes a LogicModelV1 object with schemaVersion=1, init, states ({id, terminal?}), transitions ({from, event, to, guard?, updates?}), variables?, invariants?, concurrentPairs?, boundaryChecks?, resourcePairs?, idempotentEvents?. Guards are structured ({variable, op, value} | {all} | {any} | {not}); invariants support never-states, var-in-range, event-before-state, leads-to, sequence, and atomicity; variables support monotonic inc/dec. Returns a report with S1-S7 structural checks and A1-A7 adversarial 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
15
|
parameters: {
|
|
16
16
|
model: {
|
|
17
17
|
type: 'json',
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const LOGICPROBE_CONCURRENCY_SCAN_TOOL_NAME = "logicprobe_concurrency_scan";
|
|
2
|
+
/**
|
|
3
|
+
* Model-visible DSH tool that mines design documents/plans for concurrency-related
|
|
4
|
+
* claims and risk keywords. It does not prove concurrency safety; it flags terms
|
|
5
|
+
* such as "thread-safe", "lock-free", "race condition", "atomic", "mutex", etc.,
|
|
6
|
+
* so the model can either provide dedicated evidence or mark the claim unverified.
|
|
7
|
+
*/
|
|
8
|
+
export declare const logicProbeConcurrencyScanTool: import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ConcurrencyFinding {
|
|
2
|
+
code: 'CONCURRENCY_KEYWORD' | 'CONCURRENCY_ABSOLUTE_CLAIM';
|
|
3
|
+
severity: 'warning' | 'error';
|
|
4
|
+
message: string;
|
|
5
|
+
line?: number;
|
|
6
|
+
snippet?: string;
|
|
7
|
+
keyword: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ConcurrencyScanReport {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
findings: ConcurrencyFinding[];
|
|
12
|
+
summary: {
|
|
13
|
+
lines: number;
|
|
14
|
+
keywords: number;
|
|
15
|
+
absoluteClaims: number;
|
|
16
|
+
warnings: number;
|
|
17
|
+
errors: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export declare function runConcurrencyScan(text: string): ConcurrencyScanReport;
|