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 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
- function invariantHolds(invariant, runtime) {
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
- const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
47
- Plugin logicprobe is active. Documents are not truth — code is. Verify every verifiable claim before accepting or acting on any design.
48
-
49
- **1% Rule**: If there is even a 1% chance the logicprobe skill applies — reviewing design documents, architecture specs, technical proposals, or refactoring plans that make claims about API names, file locations, enum values, mechanism feasibility, state machines, protocol logic, data models, schema migrations, data invariants, or behavioral guarantees ("always"/"never"/"guaranteed") — load it with the skill tool before responding. The cost of loading is trivial compared to the cost of a false claim.
50
-
51
- **Red Flags** — if you think any of these, STOP. You are rationalizing:
52
-
53
- | You think | Reality |
54
- |-----------|---------|
55
- | "This plan is too simple to verify" | The skill auto-classifies depth (LIGHTWEIGHT / STANDARD / ESCALATED). You don't decide. |
56
- | "I already know the file paths are correct" | Organic verification leaves no audit trail. Run Phase 0, append the "## Plan Verification" block. |
57
- | "I'll verify while implementing" | Verification happens before implementation, not during. |
58
- | "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
59
-
60
- **Native verification path**: In dsh, prefer the \`logicprobe_verify\` tool for state-machine checks and \`logicprobe_datamodel_verify\` for data-model/schema migration checks. Both support before/after regression and common domain constraints (idempotency, monotonic, sequence, leads-to, atomicity). Python harnesses remain the fallback for non-dsh hosts.
61
-
62
- **Proactive suggestion**: When a user asks code-level behavioral questions — "could this state machine deadlock", "is this retry limit safe", "check this timing sequence for bugs", "is this migration non-breaking", "does this copy cover all required fields" — suggest logicprobe as an optional verification pass (do not auto-escalate).
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: 'plugin', plugin: GATE_PLUGIN_ID },
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
- return source?.kind === 'plugin' && source.plugin === GATE_PLUGIN_ID;
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',
@@ -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;
@@ -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.6.7",
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.5.0",
93
+ "@types/node": "^26.6.2",
89
94
  "typescript": "^7.0.2"
90
95
  },
91
96
  "author": {