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.
Files changed (33) hide show
  1. package/README.en-US.md +10 -3
  2. package/README.md +10 -3
  3. package/lib/concurrency-tool.js +34 -0
  4. package/lib/concurrency.js +76 -0
  5. package/lib/data-engine.js +930 -0
  6. package/lib/data-tool.js +61 -0
  7. package/lib/engine.js +258 -0
  8. package/lib/index.js +37 -23
  9. package/lib/tool.js +1 -1
  10. package/lib/types/concurrency-tool.d.ts +8 -0
  11. package/lib/types/concurrency.d.ts +20 -0
  12. package/lib/types/data-engine.d.ts +199 -0
  13. package/lib/types/data-tool.d.ts +10 -0
  14. package/lib/types/engine.d.ts +20 -0
  15. package/package.json +82 -81
  16. package/skills/logicprobe/SKILL.md +285 -268
  17. package/skills/logicprobe/references/__pycache__/verification-harness.cpython-312.pyc +0 -0
  18. package/skills/logicprobe/references/concurrency-risk-guide.md +54 -0
  19. package/skills/logicprobe/references/dsh-model-schema.md +145 -129
  20. package/skills/logicprobe/references/logic-verification-guide.md +463 -413
  21. package/skills/logicprobe/references/verification-harness.py +806 -582
  22. package/skills/logicprobe-datamodel/SKILL.md +124 -0
  23. package/skills/logicprobe-datamodel/references/__pycache__/data-model-harness.cpython-312.pyc +0 -0
  24. package/skills/logicprobe-datamodel/references/data-model-guide.md +62 -0
  25. package/skills/logicprobe-datamodel/references/data-model-harness.py +528 -0
  26. package/skills/logicprobe-datamodel/references/data-model-schema.md +128 -0
  27. package/src/concurrency-tool.ts +37 -0
  28. package/src/concurrency.ts +102 -0
  29. package/src/data-engine.ts +1001 -0
  30. package/src/data-tool.ts +65 -0
  31. package/src/engine.ts +234 -0
  32. package/src/index.ts +315 -301
  33. package/src/tool.ts +60 -60
@@ -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 executable state-machine checks. The skill's Python harness remains the fallback for non-dsh hosts.
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 the `logicprobe_verify` tool for executable state-machine verification.',
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
- engineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled verification engine accepts.' },
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, and event-before-state. 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.',
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;