dsh-logicprobe 0.6.8 → 0.7.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/src/index.ts CHANGED
@@ -26,7 +26,7 @@
26
26
  */
27
27
 
28
28
  import { fileURLToPath } from 'node:url'
29
- import type { Context } from '@deepseek-ai/cordis'
29
+ import type { Context, Volatile } from '@deepseek-ai/cordis'
30
30
  import z from '@deepseek-ai/schemastery'
31
31
  import { createUserMessage } from '@deepseek-ai/dsh-llm'
32
32
  import type { ContextFormed } from '@deepseek-ai/dsh-llm'
@@ -90,18 +90,63 @@ Plugin logicprobe is active. Documents are not truth — code is. Verify every v
90
90
  **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).
91
91
  </EXTREMELY_IMPORTANT>`
92
92
 
93
+ /** A schemastery field that may or may not carry `.volatile()`. */
94
+ interface LiveField {
95
+ volatile?: () => unknown
96
+ }
97
+
98
+ /**
99
+ * Declare a field as live where this host's schemastery can — `.volatile()`
100
+ * arrived in 3.18.3 — and leave it an ordinary field where it cannot.
101
+ *
102
+ * The fallback is load-bearing, not defensive padding. `Config` below is built
103
+ * while this module is still being evaluated, so an unconditional `.volatile()`
104
+ * on a host shipping schemastery 3.18.2 (measured: dsh 0.1.5-rc.2 and
105
+ * 0.1.5-rc.3) throws during import; the loader entry then fails and takes the
106
+ * WHOLE plugin tree — and the host's boot — down with it. Degrading costs only
107
+ * the Plugins-page switch, because the settings service projects nothing but
108
+ * fields under a `.volatile()` node; skills, tools and the gate injection are
109
+ * untouched. The returned schema keeps the plain field's static type; the
110
+ * `Config` interface below carries the union the host actually hands over.
111
+ */
112
+ function live<T>(field: T): T {
113
+ const probe = field as T & LiveField
114
+ return typeof probe.volatile === 'function' ? (probe.volatile() as T) : field
115
+ }
116
+
93
117
  export interface Config {
94
- enabled: boolean
118
+ /**
119
+ * The injection switch the Web client's Plugins page edits live: a `Volatile`
120
+ * reference on a host whose schemastery supports one, an ordinary boolean on a
121
+ * host that predates `.volatile()`. Read it through {@link injectionEnabled},
122
+ * which accepts both shapes.
123
+ */
124
+ enabled: Volatile<boolean> | boolean
95
125
  gateContent: string
96
126
  interaction: InteractionMode
97
127
  }
98
128
 
99
129
  export const Config = z.object({
100
- enabled: z.boolean().default(true),
130
+ // Live so the Web Plugins page can flip the gate injection inside a running
131
+ // session: dsh's settings service projects ONLY fields under a `.volatile()`
132
+ // node and rejects writes to every other path. The price is that the injection
133
+ // reads the reference per step instead of deciding once at mount, which is also
134
+ // what lets a toggle take effect without remounting the row.
135
+ enabled: live(z.boolean().default(true)),
101
136
  gateContent: z.string().default(DEFAULT_GATE_CONTENT),
102
137
  interaction: z.union(['ask', 'auto', 'follow-approval']).default('follow-approval'),
103
138
  })
104
139
 
140
+ /**
141
+ * Read the injection switch as a boolean, whichever shape this host produced.
142
+ * @param config - the resolved plugin configuration.
143
+ * @returns whether the gate may be injected.
144
+ */
145
+ function injectionEnabled(config: Config): boolean {
146
+ const value = config.enabled
147
+ return typeof value === 'boolean' ? value : value.get()
148
+ }
149
+
105
150
  function gateMessage(text: string): UserMessage {
106
151
  return createUserMessage({
107
152
  content: [{ type: 'text', text }],
@@ -227,7 +272,7 @@ function inspectProvider(config: Config, isToolRegistered: () => boolean, isData
227
272
  query: async (method) => {
228
273
  if (method === 'status') {
229
274
  return {
230
- enabled: config.enabled,
275
+ enabled: injectionEnabled(config),
231
276
  gateContentLength: config.gateContent.length,
232
277
  interaction: config.interaction,
233
278
  toolRegistered: isToolRegistered(),
@@ -324,7 +369,12 @@ export function apply(ctx: Context, config: Config): void {
324
369
  customSkillDirs: [SKILLS_DIR],
325
370
  })
326
371
  })
327
- if (!config.enabled) return
372
+ // Injection listens unconditionally, including while the switch is off: the
373
+ // switch is volatile, so `apply` runs once and the value behind it can turn on
374
+ // later from the Web Plugins page. Returning early on a false value here would
375
+ // freeze that decision for the lifetime of the mount, and turning the switch
376
+ // back on could never take effect without a profile restart.
377
+ //
328
378
  // Inject the gate once per session on the FIRST model step that runs,
329
379
  // instead of at session-start: session-start injection lands in the agent's
330
380
  // inbox, which a blank-session preset switch (agentPreset.select ->
@@ -340,6 +390,7 @@ export function apply(ctx: Context, config: Config): void {
340
390
  const decision = await next()
341
391
  if (decision.kind === 'reject') return decision
342
392
  registerIntegrations()
393
+ if (!injectionEnabled(config)) return decision
343
394
  if (gateInHistory(agent.session)) return decision
344
395
  return {
345
396
  kind: 'enter',
package/src/tool.ts CHANGED
@@ -1,61 +1,61 @@
1
- import { defineTool, type JsonValue } from '@deepseek-ai/dsh-tools'
2
- import { runVerification } from './engine.js'
3
-
4
- export const LOGICPROBE_VERIFY_TOOL_NAME = 'logicprobe_verify'
5
-
6
- /**
7
- * Model-visible DSH tool wrapping the bundled TypeScript verification engine.
8
- * The model passes a LogicModelV1 object; the engine validates it and returns
9
- * the 22-check report (S1-S8 structural, A1-A14), or a 26-check
10
- * report when beforeModel is supplied (D1 behavioral preservation, D2
11
- * invariant continuity, D3 regression delta, D4 deadlock/liveness regression).
12
- * This is the dsh-native replacement for hand-filling the Python template
13
- * shipped in the skill references.
14
- */
15
- export const logicProbeVerifyTool = defineTool({
16
- name: LOGICPROBE_VERIFY_TOOL_NAME,
17
- description:
18
- '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.',
19
- parameters: {
20
- model: {
21
- type: 'json',
22
- required: true,
23
- description: 'LogicModelV1 state machine model to verify.',
24
- },
25
- maxStates: {
26
- type: 'integer',
27
- description: 'Maximum runtime states to explore. Default 10000.',
28
- },
29
- maxPermutationEvents: {
30
- type: 'integer',
31
- description: 'Maximum event count for A3 order permutation. Default 5.',
32
- },
33
- beforeModel: {
34
- type: 'json',
35
- description: 'Optional BEFORE LogicModelV1 state machine model for refactoring/migration regression comparison.',
36
- },
37
- stateMapping: {
38
- type: 'json',
39
- description: 'Optional object mapping BEFORE state ids to AFTER state ids. Omit for identity mapping (same state names).',
40
- },
41
- },
42
- output: {
43
- schema: {
44
- type: 'json',
45
- description: 'logicprobe verification report with summary and per-check findings.',
46
- },
47
- render(_args, value) {
48
- return [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }]
49
- },
50
- },
51
- timeoutMs: 10_000,
52
- isConcurrencySafe: () => true,
53
- async execute(args) {
54
- return runVerification(args.model, {
55
- maxStates: args.maxStates,
56
- maxPermutationEvents: args.maxPermutationEvents,
57
- beforeModel: args.beforeModel,
58
- stateMapping: args.stateMapping as Record<string, string> | undefined,
59
- }) as unknown as JsonValue
60
- },
61
- })
1
+ import { defineTool, type JsonValue } from '@deepseek-ai/dsh-tools'
2
+ import { runVerification } from './engine.js'
3
+
4
+ export const LOGICPROBE_VERIFY_TOOL_NAME = 'logicprobe_verify'
5
+
6
+ /**
7
+ * Model-visible DSH tool wrapping the bundled TypeScript verification engine.
8
+ * The model passes a LogicModelV1 object; the engine validates it and returns
9
+ * the 22-check report (S1-S8 structural, A1-A14), or a 26-check
10
+ * report when beforeModel is supplied (D1 behavioral preservation, D2
11
+ * invariant continuity, D3 regression delta, D4 deadlock/liveness regression).
12
+ * This is the dsh-native replacement for hand-filling the Python template
13
+ * shipped in the skill references.
14
+ */
15
+ export const logicProbeVerifyTool = defineTool({
16
+ name: LOGICPROBE_VERIFY_TOOL_NAME,
17
+ description:
18
+ '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.',
19
+ parameters: {
20
+ model: {
21
+ type: 'json',
22
+ required: true,
23
+ description: 'LogicModelV1 state machine model to verify.',
24
+ },
25
+ maxStates: {
26
+ type: 'integer',
27
+ description: 'Maximum runtime states to explore. Default 10000.',
28
+ },
29
+ maxPermutationEvents: {
30
+ type: 'integer',
31
+ description: 'Maximum event count for A3 order permutation. Default 5.',
32
+ },
33
+ beforeModel: {
34
+ type: 'json',
35
+ description: 'Optional BEFORE LogicModelV1 state machine model for refactoring/migration regression comparison.',
36
+ },
37
+ stateMapping: {
38
+ type: 'json',
39
+ description: 'Optional object mapping BEFORE state ids to AFTER state ids. Omit for identity mapping (same state names).',
40
+ },
41
+ },
42
+ output: {
43
+ schema: {
44
+ type: 'json',
45
+ description: 'logicprobe verification report with summary and per-check findings.',
46
+ },
47
+ render(_args, value) {
48
+ return [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }]
49
+ },
50
+ },
51
+ timeoutMs: 10_000,
52
+ isConcurrencySafe: () => true,
53
+ async execute(args) {
54
+ return runVerification(args.model, {
55
+ maxStates: args.maxStates,
56
+ maxPermutationEvents: args.maxPermutationEvents,
57
+ beforeModel: args.beforeModel,
58
+ stateMapping: args.stateMapping as Record<string, string> | undefined,
59
+ }) as unknown as JsonValue
60
+ },
61
+ })