dsh-logicprobe 0.7.0 → 0.8.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.
@@ -0,0 +1,166 @@
1
+ # UML Modelling Guide
2
+
3
+ This guide covers two jobs: drawing a code flow as UML, and reviewing that drawing. Load it when the task is "show me the flow" rather than "check this claim". Typical cases are reverse-engineering a handler, documenting a protocol, or auditing a diagram somebody else drew.
4
+
5
+ One rule runs through the whole guide. **A diagram is a model, and a model can be wrong.** A tidy flow chart is not evidence about the code. Worse, a diagram that disagrees with the model it came from will mislead every reader who trusts it.
6
+
7
+ ## When to use this
8
+
9
+ | Situation | View that answers it |
10
+ |---|---|
11
+ | "What are the states of this handler, and what moves between them?" | State machine |
12
+ | "What does this function actually do, including its error paths?" | Activity (flow) |
13
+ | "In what order do these messages arrive, and what is the state after each?" | Sequence |
14
+ | "Somebody gave me a diagram. Is it right?" | Any view. Feed it to the review and compare it against the code. |
15
+ | "This diagram and this model disagree." | Feed both to the review. The round-trip check names every difference. |
16
+
17
+ Do not use this guide for a behavioural claim about a specific machine. That is the job of `logicprobe_verify` (S1-S8 and A1-A14). The review here covers the modelling, not the behaviour. Every finding it produces names the engine check that settles the behavioural half.
18
+
19
+ ## The three views
20
+
21
+ One machine, three projections. They are not interchangeable. The review reports which view it saw, and the differences matter.
22
+
23
+ **State machine** (`stateDiagram-v2`, or a PlantUML state diagram). This view shows the whole topology: every state, every event-and-guard branch, and every terminal state as `--> [*]`. Model code flow into this view, and use it for review. It is the only view that round-trips without loss.
24
+
25
+ **Activity** (`flowchart TD` in Mermaid). This view draws the same machine as work rather than as states. Each edge carries `event [guard] / actions`. It suits a reader who thinks in steps. It holds the same information as the state view. PlantUML activity is **not** generated: its structured-flowchart syntax needs a while/if reconstruction for any graph with a merge or a cycle. A diagram that quietly reshapes the machine is exactly the failure this feature exists to prevent, so the tool refuses that combination.
26
+
27
+ **Sequence** (`sequenceDiagram`). This view shows one BFS trace: the messages in the order a walker meets them, with a note per state change. A trace is not a machine. Branches appear as separate guarded messages, paths the walk never took are absent, and the trace is capped. Use it to show a protocol exchange to a human. It cannot be the review's fidelity input, because `parse` refuses it and `review` reports the check as inapplicable (`UML019`).
28
+
29
+ ## Modelling a code flow from source
30
+
31
+ Extraction follows the same discipline as `logic-verification-guide.md`, applied to code instead of a plan.
32
+
33
+ 1. **Fix the boundary.** Decide which function, task, or module is the machine. In scope: state the code holds across calls, such as statics, fields, enums, and task state variables. Out of scope: the call stack inside one invocation, hardware behaviour, and scheduler preemption.
34
+ 2. **Name the states from the code.** A state exists if something survives a call boundary and is tested later. An enum, a `state` field, or a task-local variable that gates the next entry all qualify. A local variable inside one function does not.
35
+ 3. **Name the events from the code.** Every edge must trace to a call site, a message, a timer expiry, or an ISR. An edge with no call site is a guess. Put it in the review findings instead of the model.
36
+ 4. **Record guards and actions verbatim.** A guard must be the real condition, with the real variable and the real constant. A retry limit of 3 modelled as "a few" is a model of your assumption, not of the code.
37
+ 5. **Mark the terminals.** An absorbing state is `terminal: true`. Examples are power-off, fatal, and done. If nothing is terminal, say so. The review will flag it either way.
38
+ 6. **Write the narrative as you go.** The `narrative` block holds natural-language meanings for states and events, plus a scenario for each state-and-event pair. It is what lets a reader check the diagram against the code without re-deriving every symbol. Once the block is present, the schema requires all three parts and full coverage, so it cannot rot half-way.
39
+
40
+ ### Evidence rule
41
+
42
+ Every state, event, guard and action needs a citation. Use `file:line` for code and a section reference for a document. A UML model without citations is a drawing. It cannot be reviewed, only admired. Present the citations together with the model.
43
+
44
+ ### Confirming the model
45
+
46
+ The usual gate applies: show the extracted transition table or the diagram, and get confirmation before treating the model as fact. In `logicprobe interaction=auto`, skip the question. Instead, cite evidence for every element, round-trip the model (render, parse, compare), and mark the result `UNCONFIRMED`.
47
+
48
+ ## Rendering
49
+
50
+ In DSH:
51
+
52
+ ```json
53
+ { "action": "render", "model": { "...LogicModelV1..." }, "notation": "mermaid", "kind": "state" }
54
+ ```
55
+
56
+ `logicprobe_uml` with `action=render` returns the diagram source plus `warnings`. Read those warnings. They list every construct the notation could not carry verbatim: a state id that needed an alias, a label containing `[` or `/`, a trace that was capped.
57
+
58
+ Without the DSH tool, run the standalone engine:
59
+
60
+ ```bash
61
+ python skills/logicprobe/references/logicprobe-engine.py uml-render model.json --notation mermaid --diagram state
62
+ ```
63
+
64
+ You can also write the diagram by hand. The generator is a convenience, not a requirement. A hand-written diagram is parsed and reviewed exactly like a generated one.
65
+
66
+ ### Directives in generated diagrams
67
+
68
+ Generated text carries comment lines. Mermaid and PlantUML ignore them. The parser reads them:
69
+
70
+ ```text
71
+ %%logicprobe:uml v1 notation=mermaid diagram=state
72
+ %%logicprobe:init INIT
73
+ %%logicprobe:terminal FATAL
74
+ %%logicprobe:alias S_1 1st state
75
+ %%logicprobe:variable retry integer
76
+ ```
77
+
78
+ These lines exist because the notation cannot express everything the model knows. `[*]` marks an initial state, but a flowchart has no such marker. A state id may contain characters the notation cannot spell. A boolean assignment (`armed := 1`) looks exactly like an integer one. Pinning those facts in comments is what makes the round-trip check exact instead of approximate. PlantUML uses `'` instead of `%%`.
79
+
80
+ A hand-written diagram needs none of these directives. Adding `%%logicprobe:init` and `%%logicprobe:terminal` to a flowchart is how you say which node starts and which one ends.
81
+
82
+ ## Reviewing the modelling
83
+
84
+ ```json
85
+ { "action": "review", "model": { "...LogicModelV1..." } }
86
+ ```
87
+
88
+ Four input shapes, four different questions:
89
+
90
+ | Input | Question answered |
91
+ |---|---|
92
+ | `model` only | Is this machine well-modelled? The review checks its structure, then renders and re-parses it to prove the diagram carries it. |
93
+ | `diagram` only | What does this diagram actually say? The diagram is parsed into a model, and that model is reviewed. Fidelity to code is **unchecked** (`UML018`). |
94
+ | `model` + `diagram` | Does the diagram match the model? Every structural difference is a modelling defect (`UML017`). |
95
+ | `model` + `kind: "sequence"` | The trace is rendered and the structure is reviewed, but the fidelity check **does not apply** (`UML019`). A trace cannot be parsed back into a machine, so the review says so instead of pretending it verified the diagram. `maxSteps` caps the trace. |
96
+
97
+ ### Findings
98
+
99
+ | Code | Severity | What it means |
100
+ |---|---|---|
101
+ | `UML001_DIAGRAM_UNREADABLE` | error | The text is not readable as a Mermaid or PlantUML state or activity diagram. |
102
+ | `UML002_UNREACHABLE_STATE` | error | No transition can enter this state from init. The diagram draws flow nobody can reach. The check is structural and ignores guards; S1 is the guard-aware check. |
103
+ | `UML003_DEAD_END_STATE` | error | A non-terminal state has no outgoing transition. Either it is terminal, or the outgoing flow was never modelled. |
104
+ | `UML004_AMBIGUOUS_BRANCH` | error | Two unconditional arrows share one state and event. No reader and no implementation can resolve that. S4 is the authoritative check. |
105
+ | `UML005_OVERLAPPING_GUARD` | warning | The same guard text appears twice in one branch group. |
106
+ | `UML006_INEXHAUSTIVE_BRANCH` | warning or info | The branch group has only guarded branches and no default. The severity is `warning` when the guards do not look complementary. It is `info` when a complementary pair such as `k < 3` and `k >= 3` is present, which is probably exhaustive. Only S6 can settle it. |
107
+ | `UML007_UNUSED_EVENT` | warning | The event fires only from unreachable states. The diagram shows messages that never arrive. |
108
+ | `UML008_SELF_LOOP_NO_EXIT` | warning | An unguarded self-loop has no other exit. The diagram presents it as progress, but the flow never leaves. S3 reports absorbing cycles. |
109
+ | `UML009_DUPLICATE_TRANSITION` | warning | The same from, event, guard, actions and target row appears twice. |
110
+ | `UML010_UNUSED_VARIABLE` | warning | No guard reads this variable and no action writes it. It is a symbol with no source. |
111
+ | `UML011_UNBOUNDED_VARIABLE` | info | An integer variable has no min or max, so no range invariant can be checked and A5 has no declared domain. |
112
+ | `UML012_NO_TERMINAL` | warning | No state is terminal, so completion, failure and a stuck flow all look the same. |
113
+ | `UML013_NO_NARRATIVE` | info | The model carries no meanings, so a reader must re-derive every symbol from the source. |
114
+ | `UML014_UNDOCUMENTED_STATE` | info | These states render as their bare id, so the diagram cannot be read against the code. |
115
+ | `UML015_LABEL_DRIFT` | warning | A diagram label disagrees with the model narrative. One of the two is stale, and the review cannot tell which. |
116
+ | `UML016_DIAGRAM_PARSE_NOTES` | info | Notes collected while rendering or reading the diagram. They mark information the notation could not carry. |
117
+ | `UML017_ROUND_TRIP_MISMATCH` | error | The diagram does not carry the model. Transitions were lost or invented, or init, terminals or variables differ. The `roundTrip.diffs` array lists each one. |
118
+ | `UML018_FIDELITY_UNCHECKED` | info | Only a diagram was supplied, so nothing here proves it matches the code. |
119
+ | `UML019_ROUND_TRIP_SKIPPED` | warning | The fidelity check could not run, either because the view is not round-trippable or because it was switched off. |
120
+
121
+ `ok: true` means the review ran. It does not mean the model is good. Read the findings and the `summary` counts.
122
+
123
+ ### What the round trip proves
124
+
125
+ It proves the diagram is a faithful rendering of the model:
126
+
127
+ - the same initial state
128
+ - the same set of states and terminal states
129
+ - the same transitions, with the same guards and actions
130
+ - the same variables and kinds
131
+
132
+ It does **not** prove the model matches the code. Only a citation-per-element comparison does that, as described under "Evidence rule". It also does not prove the machine is correct. That is the job of `logicprobe_verify`.
133
+
134
+ A failing round trip means the notation lost something. In practice that is a real finding: an unlabelled arrow, a guard the parser could not read, or a diagram that was hand-edited away from its model.
135
+
136
+ ## Worked example
137
+
138
+ The code under review is a handshake that retries on timeout and gives up.
139
+
140
+ ```text
141
+ INIT --power_ready--> STARTING
142
+ STARTING --ack--> ACTIVE
143
+ STARTING --timeout [retry < 3] / retry := retry + 1--> ERROR
144
+ STARTING --timeout [retry >= 3]--> FATAL (terminal)
145
+ ERROR --cooldown--> STARTING
146
+ ```
147
+
148
+ Render it as a state machine, then review it. The review reports two modelling gaps:
149
+
150
+ ```text
151
+ UML006_INEXHAUSTIVE_BRANCH (info) guards look complementary on retry
152
+ UML013_NO_NARRATIVE (info) no state, event or scenario meanings
153
+ ```
154
+
155
+ Neither is a behaviour bug. Add the narrative so a reader can check the diagram against the code, then run `logicprobe_verify` for S1-S8 and A1-A14.
156
+
157
+ Now delete the `ERROR --cooldown--> STARTING` arrow and review again. `ACTIVE` and `ERROR` become dead ends (`UML003`), and `cooldown` becomes an event that only fires from an unreachable state (`UML007`). The diagram still looks plausible. That is the whole point.
158
+
159
+ ## Limits
160
+
161
+ - The review is **structural**. It evaluates no guard over any valuation. "Probably exhaustive" is a shape test, not a proof. S6 is the check that decides.
162
+ - Reachability ignores guards (`UML002`). A state reachable only under an unsatisfiable guard is a behaviour finding (S1 and S6), not a modelling one.
163
+ - Sequence diagrams are traces. They are neither round-tripped nor parsed. Reviewing one renders the trace, reports the structure findings, and marks the fidelity check inapplicable (`UML019`).
164
+ - PlantUML activity is refused by design, as described above.
165
+ - Renaming a state in the diagram does not rename it in the code. The review can only tell you the two disagree. Resolving it needs the source.
166
+ - The generated diagram is a faithful view of the **model**, never of the **program**. If the model is wrong, the diagram is wrong in exactly the same way.
package/src/client.js ADDED
@@ -0,0 +1,212 @@
1
+ /**
2
+ * logicprobe — browser half: the gate-injection switch on the dsh Web client's
3
+ * Plugins page.
4
+ *
5
+ * The Plugins page (`@deepseek-ai/dsh-client-ui-plugin-manager`) owns the
6
+ * sidebar **Plugins** entry and declares the slots a bundle's own configuration
7
+ * registers into. This module contributes one `plugins.bundle.config` entry,
8
+ * keyed by this package's npm name, so the switch renders on logicprobe's own
9
+ * page between its description and its rows.
10
+ *
11
+ * Why the switch writes through `configForms` rather than reaching for the
12
+ * profile file: dsh's settings service exposes only the Config fields declared
13
+ * `.volatile()`, and it rejects a write to any other path. `enabled` is such a
14
+ * field (see `src/index.ts`), so flipping the switch is an ordinary
15
+ * revision-fenced settings write that the running Host picks up in place — the
16
+ * injection there re-reads the reference on every model step.
17
+ *
18
+ * Shape: this is a prebuilt module-system bundle, not a source module. It calls
19
+ * `window.__ModuleLoader__.load({ id, factory })` with this package's resolved
20
+ * npm name, and `factory` returns the cordis plugin face. Only the client
21
+ * baseline is requested (`react` and
22
+ * `@deepseek-ai/dsh-client-ui-primitives`); every other capability arrives
23
+ * through cordis `inject`. `scripts/build-client.mjs` publishes this file
24
+ * verbatim as `lib/client.js`.
25
+ *
26
+ * @module dsh-logicprobe/client
27
+ */
28
+
29
+ window.__ModuleLoader__.load({
30
+ id: 'dsh-logicprobe',
31
+ factory: (require) => {
32
+ const React = require('react')
33
+ const { Button, Switch } = require('@deepseek-ai/dsh-client-ui-primitives')
34
+
35
+ /** Settings namespace: the Loader entry id this bundle's patch declares. */
36
+ const NS = 'logicprobe'
37
+ /** `plugins.bundle.config` key: the bundle's npm package name. */
38
+ const PACKAGE = 'dsh-logicprobe'
39
+ /** This page's dictionary namespace. */
40
+ const LOCALE_NS = 'logicprobe.plugins'
41
+ /** The Config field the switch writes inside the namespace's section. */
42
+ const FIELD = 'enabled'
43
+
44
+ /** English copy. */
45
+ const en = {
46
+ title: 'Gate injection',
47
+ label: 'Inject the gate text',
48
+ hint: 'Folds the claim-verification doctrine into the first model step of every session. Turning it off leaves the skills and the verification tools registered — only the injected text is dropped.',
49
+ overridden: 'Overridden',
50
+ reset: 'Reset to default',
51
+ readOnly: 'This deployment stores settings read-only.',
52
+ unavailable: 'This plugin is not loaded, so it cannot be configured right now.',
53
+ saveFailed: 'The deployment did not accept that value; the switch shows what is stored.',
54
+ }
55
+ /** Simplified Chinese copy. */
56
+ const zh = {
57
+ title: 'Gate 注入',
58
+ label: '注入 gate 文本',
59
+ hint: '把 claim 核查铁律折进每个会话的第一个模型步。关掉后 skills 与验证工具仍然注册,只是不再注入那段提示文本。',
60
+ overridden: '已覆盖',
61
+ reset: '恢复默认',
62
+ readOnly: '本部署的设置为只读。',
63
+ unavailable: '该插件当前未加载,暂时无法配置。',
64
+ saveFailed: '本部署没有接受这个值,开关显示的是已存下的状态。',
65
+ }
66
+
67
+ /** Required cordis services. */
68
+ const inject = ['slots', 'locale', 'configForms']
69
+
70
+ const GROUP = { display: 'flex', flexDirection: 'column', gap: '8px' }
71
+ const TITLE = { margin: 0, fontSize: '14px', fontWeight: '500', lineHeight: '22px' }
72
+ const ROW = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px' }
73
+ const LABEL = { fontSize: '13px', lineHeight: '20px' }
74
+ const NOTE = { margin: 0, fontSize: '12px', lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' }
75
+ const FAILED = { margin: 0, fontSize: '12px', lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' }
76
+
77
+ /**
78
+ * Whether a settings-layer value carries this field, which is what marks it
79
+ * overridden: an override equal to the default is still an override.
80
+ * @param layer - the raw user layer the form snapshot carries.
81
+ * @returns whether the layer holds the field.
82
+ */
83
+ function carries(layer) {
84
+ return layer !== null && typeof layer === 'object' && Object.prototype.hasOwnProperty.call(layer, FIELD)
85
+ }
86
+
87
+ /**
88
+ * Render the gate-injection switch, or the note saying why it cannot render.
89
+ * @param props - the page's `t` seat, the bound form snapshot hook, and the write actions.
90
+ * @returns the body of this bundle's configuration section.
91
+ */
92
+ function InjectionCard(props) {
93
+ const t = props.t
94
+ const state = props.useInjectionForm((snapshot) => snapshot)
95
+ const [pending, setPending] = React.useState(false)
96
+ const [failed, setFailed] = React.useState(false)
97
+
98
+ /** Run one settings write and report a refusal or a transport failure. */
99
+ const write = (run) => {
100
+ setPending(true)
101
+ setFailed(false)
102
+ Promise.resolve(run()).then(
103
+ (accepted) => {
104
+ setPending(false)
105
+ setFailed(accepted === false)
106
+ },
107
+ () => {
108
+ setPending(false)
109
+ setFailed(true)
110
+ },
111
+ )
112
+ }
113
+
114
+ if (state.status !== 'ready') {
115
+ return React.createElement('p', { style: NOTE }, t('unavailable'))
116
+ }
117
+
118
+ const section = state.value !== null && typeof state.value === 'object' ? state.value : {}
119
+ // The schema default is `true`; only an explicit false means off.
120
+ const checked = section[FIELD] !== false
121
+ const overridden = carries(state.user)
122
+ const locked = state.writable !== true || pending
123
+
124
+ const children = [
125
+ React.createElement('h4', { key: 'title', style: TITLE }, t('title')),
126
+ React.createElement('div', { key: 'row', style: ROW }, [
127
+ React.createElement('span', { key: 'label', style: LABEL }, t('label')),
128
+ React.createElement(Switch, {
129
+ key: 'switch',
130
+ checked,
131
+ disabled: locked,
132
+ label: t('label'),
133
+ onChange: (next) => write(() => props.setEnabled(next)),
134
+ }),
135
+ ]),
136
+ React.createElement('p', { key: 'hint', style: NOTE }, state.writable === true ? t('hint') : t('readOnly')),
137
+ ]
138
+
139
+ if (overridden) {
140
+ children.push(
141
+ React.createElement('div', { key: 'overridden', style: ROW }, [
142
+ React.createElement('span', { key: 'badge', style: NOTE }, t('overridden')),
143
+ React.createElement(
144
+ Button,
145
+ {
146
+ key: 'reset',
147
+ variant: 'outline',
148
+ size: 'sm',
149
+ disabled: locked,
150
+ onClick: () => write(() => props.resetEnabled()),
151
+ },
152
+ t('reset'),
153
+ ),
154
+ ]),
155
+ )
156
+ }
157
+
158
+ if (failed) {
159
+ children.push(React.createElement('p', { key: 'failed', style: FAILED, role: 'alert' }, t('saveFailed')))
160
+ }
161
+
162
+ return React.createElement('div', { style: GROUP }, children)
163
+ }
164
+
165
+ /**
166
+ * Mount the switch while the Host serves logicprobe's settings namespace.
167
+ * @param ctx - the browser plugin context.
168
+ */
169
+ function apply(ctx) {
170
+ ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), 'dsh-logicprobe: dictionaries')
171
+ // `whileServed` is the registration barrier that keeps this page alive only
172
+ // while the Host serves the namespace. Hosts predating it (measured: dsh
173
+ // 0.1.5-rc.3 and 0.1.6-alpha.2) have no live settings field to offer at all,
174
+ // so there is nothing to register — and calling it there would throw during
175
+ // this plugin's own activation, which the Web boot audit then reports as a
176
+ // failed client entry. Degrade to no page instead.
177
+ if (typeof ctx.configForms.whileServed !== 'function') return
178
+ // The page renders the section only for a bundle whose package name is in
179
+ // its configuration ledger, and the ledger follows this registration. The
180
+ // registration in turn waits for the namespace to be served, so a profile
181
+ // whose logicprobe row is switched off shows no trace of the switch.
182
+ ctx.effect(
183
+ () =>
184
+ ctx.configForms.whileServed([NS], () => {
185
+ const form = ctx.configForms.get(NS)
186
+ const source = {
187
+ getSnapshot: () => form.getSnapshot(),
188
+ subscribe: (listener) => form.subscribe(listener),
189
+ }
190
+ return ctx.slots.inject('plugins.bundle.config', () =>
191
+ ctx.slots.register(
192
+ {
193
+ name: 'plugins.bundle.config',
194
+ key: PACKAGE,
195
+ locale: LOCALE_NS,
196
+ inject: () => ({
197
+ hooks: { injectionForm: source },
198
+ setEnabled: (next) => form.set(FIELD, next),
199
+ resetEnabled: () => form.unset(FIELD),
200
+ }),
201
+ },
202
+ InjectionCard,
203
+ ),
204
+ )
205
+ }),
206
+ 'dsh-logicprobe: gate-injection switch',
207
+ )
208
+ }
209
+
210
+ return { inject, apply }
211
+ },
212
+ })
@@ -1,4 +1,5 @@
1
- import { defineTool, type JsonValue } from '@deepseek-ai/dsh-tools'
1
+ import { defineTool } from '@deepseek-ai/dsh-tools'
2
+ import type { JsonValue } from './json-value.js'
2
3
  import { runCompositionVerification } from './engine.js'
3
4
 
4
5
  export const LOGICPROBE_COMPOSE_TOOL_NAME = 'logicprobe_compose_verify'
@@ -1,4 +1,5 @@
1
- import { defineTool, type JsonValue } from '@deepseek-ai/dsh-tools'
1
+ import { defineTool } from '@deepseek-ai/dsh-tools'
2
+ import type { JsonValue } from './json-value.js'
2
3
  import { runConcurrencyScan } from './concurrency.js'
3
4
 
4
5
  export const LOGICPROBE_CONCURRENCY_SCAN_TOOL_NAME = 'logicprobe_concurrency_scan'
package/src/data-tool.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { defineTool, type JsonValue } from '@deepseek-ai/dsh-tools'
1
+ import { defineTool } from '@deepseek-ai/dsh-tools'
2
+ import type { JsonValue } from './json-value.js'
2
3
  import { runDataVerification, DATA_ENGINE_SCHEMA_VERSION } from './data-engine.js'
3
4
 
4
5
  export const LOGICPROBE_DATAMODEL_VERIFY_TOOL_NAME = 'logicprobe_datamodel_verify'
@@ -1,4 +1,5 @@
1
- import { defineTool, type JsonValue } from '@deepseek-ai/dsh-tools'
1
+ import { defineTool } from '@deepseek-ai/dsh-tools'
2
+ import type { JsonValue } from './json-value.js'
2
3
  import { exportModel, type ExportFormat } from './exporters.js'
3
4
 
4
5
  export const LOGICPROBE_EXPORT_TOOL_NAME = 'logicprobe_export'
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'
@@ -39,6 +39,7 @@ import { logicProbeDataModelVerifyTool, DATA_ENGINE_SCHEMA_VERSION } from './dat
39
39
  import { logicProbeConcurrencyScanTool } from './concurrency-tool.js'
40
40
  import { logicProbeComposeTool } from './compose-tool.js'
41
41
  import { logicProbeExportTool } from './export-tool.js'
42
+ import { logicProbeUmlTool } from './uml-tool.js'
42
43
  import { ENGINE_SCHEMA_VERSION } from './engine.js'
43
44
 
44
45
  // DSH 0.1.7-alpha.1 (session format v4) retires the shared
@@ -85,23 +86,68 @@ Plugin logicprobe is active. Documents are not truth — code is. Verify every v
85
86
  | "I'll verify while implementing" | Verification happens before implementation, not during. |
86
87
  | "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
87
88
 
88
- **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.
89
+ **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). To model a code flow as UML, or to audit such a modelling, use \`logicprobe_uml\` (render | parse | review). Python harnesses remain the fallback for non-dsh hosts.
89
90
 
90
91
  **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
92
  </EXTREMELY_IMPORTANT>`
92
93
 
94
+ /** A schemastery field that may or may not carry `.volatile()`. */
95
+ interface LiveField {
96
+ volatile?: () => unknown
97
+ }
98
+
99
+ /**
100
+ * Declare a field as live where this host's schemastery can — `.volatile()`
101
+ * arrived in 3.18.3 — and leave it an ordinary field where it cannot.
102
+ *
103
+ * The fallback is load-bearing, not defensive padding. `Config` below is built
104
+ * while this module is still being evaluated, so an unconditional `.volatile()`
105
+ * on a host shipping schemastery 3.18.2 (measured: dsh 0.1.5-rc.2 and
106
+ * 0.1.5-rc.3) throws during import; the loader entry then fails and takes the
107
+ * WHOLE plugin tree — and the host's boot — down with it. Degrading costs only
108
+ * the Plugins-page switch, because the settings service projects nothing but
109
+ * fields under a `.volatile()` node; skills, tools and the gate injection are
110
+ * untouched. The returned schema keeps the plain field's static type; the
111
+ * `Config` interface below carries the union the host actually hands over.
112
+ */
113
+ function live<T>(field: T): T {
114
+ const probe = field as T & LiveField
115
+ return typeof probe.volatile === 'function' ? (probe.volatile() as T) : field
116
+ }
117
+
93
118
  export interface Config {
94
- enabled: boolean
119
+ /**
120
+ * The injection switch the Web client's Plugins page edits live: a `Volatile`
121
+ * reference on a host whose schemastery supports one, an ordinary boolean on a
122
+ * host that predates `.volatile()`. Read it through {@link injectionEnabled},
123
+ * which accepts both shapes.
124
+ */
125
+ enabled: Volatile<boolean> | boolean
95
126
  gateContent: string
96
127
  interaction: InteractionMode
97
128
  }
98
129
 
99
130
  export const Config = z.object({
100
- enabled: z.boolean().default(true),
131
+ // Live so the Web Plugins page can flip the gate injection inside a running
132
+ // session: dsh's settings service projects ONLY fields under a `.volatile()`
133
+ // node and rejects writes to every other path. The price is that the injection
134
+ // reads the reference per step instead of deciding once at mount, which is also
135
+ // what lets a toggle take effect without remounting the row.
136
+ enabled: live(z.boolean().default(true)),
101
137
  gateContent: z.string().default(DEFAULT_GATE_CONTENT),
102
138
  interaction: z.union(['ask', 'auto', 'follow-approval']).default('follow-approval'),
103
139
  })
104
140
 
141
+ /**
142
+ * Read the injection switch as a boolean, whichever shape this host produced.
143
+ * @param config - the resolved plugin configuration.
144
+ * @returns whether the gate may be injected.
145
+ */
146
+ function injectionEnabled(config: Config): boolean {
147
+ const value = config.enabled
148
+ return typeof value === 'boolean' ? value : value.get()
149
+ }
150
+
105
151
  function gateMessage(text: string): UserMessage {
106
152
  return createUserMessage({
107
153
  content: [{ type: 'text', text }],
@@ -166,6 +212,7 @@ function modeContextText(config: Config, session: Session): string {
166
212
  const interaction = resolveInteraction(config, session)
167
213
  const lines = [
168
214
  'logicprobe: use `logicprobe_verify` for state machines and `logicprobe_datamodel_verify` for data models; both cover before/after regression and common domain constraints.',
215
+ 'logicprobe: use `logicprobe_uml` to model a code flow as UML (render), to read a UML diagram back into a model (parse), or to audit the modelling (review: structural defects, documentation gaps, diagram-vs-model round-trip fidelity).',
169
216
  interaction === 'auto'
170
217
  ? '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.'
171
218
  : 'logicprobe interaction=ask: show the extracted transition table and get user confirmation before running verification.',
@@ -189,7 +236,7 @@ interface SystemPromptLike {
189
236
  * lets the model read this plugin's runtime status without guessing. Mirrors
190
237
  * the registration pattern of the official dsh-tool-cordis host providers.
191
238
  */
192
- function inspectProvider(config: Config, isToolRegistered: () => boolean, isDataToolRegistered: () => boolean, isConcurrencyToolRegistered: () => boolean, isComposeToolRegistered: () => boolean, isExportToolRegistered: () => boolean): HostCordisInspectProviderRegistration {
239
+ function inspectProvider(config: Config, isToolRegistered: () => boolean, isDataToolRegistered: () => boolean, isConcurrencyToolRegistered: () => boolean, isComposeToolRegistered: () => boolean, isExportToolRegistered: () => boolean, isUmlToolRegistered: () => boolean): HostCordisInspectProviderRegistration {
193
240
  return {
194
241
  manifest: {
195
242
  id: 'logicprobe',
@@ -215,10 +262,11 @@ function inspectProvider(config: Config, isToolRegistered: () => boolean, isData
215
262
  concurrencyToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_concurrency_scan tool is registered on ctx.tools.' },
216
263
  composeToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_compose_verify tool is registered on ctx.tools.' },
217
264
  exportToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_export tool is registered on ctx.tools.' },
265
+ umlToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_uml tool (UML modelling + modelling review) is registered on ctx.tools.' },
218
266
  engineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled state-machine verification engine accepts.' },
219
267
  dataEngineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled data-model verification engine accepts.' },
220
268
  },
221
- required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'composeToolRegistered', 'exportToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
269
+ required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'composeToolRegistered', 'exportToolRegistered', 'umlToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
222
270
  additionalProperties: false,
223
271
  },
224
272
  },
@@ -227,7 +275,7 @@ function inspectProvider(config: Config, isToolRegistered: () => boolean, isData
227
275
  query: async (method) => {
228
276
  if (method === 'status') {
229
277
  return {
230
- enabled: config.enabled,
278
+ enabled: injectionEnabled(config),
231
279
  gateContentLength: config.gateContent.length,
232
280
  interaction: config.interaction,
233
281
  toolRegistered: isToolRegistered(),
@@ -235,6 +283,7 @@ function inspectProvider(config: Config, isToolRegistered: () => boolean, isData
235
283
  concurrencyToolRegistered: isConcurrencyToolRegistered(),
236
284
  composeToolRegistered: isComposeToolRegistered(),
237
285
  exportToolRegistered: isExportToolRegistered(),
286
+ umlToolRegistered: isUmlToolRegistered(),
238
287
  engineSchemaVersion: ENGINE_SCHEMA_VERSION,
239
288
  dataEngineSchemaVersion: DATA_ENGINE_SCHEMA_VERSION,
240
289
  }
@@ -254,13 +303,14 @@ export function apply(ctx: Context, config: Config): void {
254
303
  let concurrencyToolRegistered = false
255
304
  let composeToolRegistered = false
256
305
  let exportToolRegistered = false
306
+ let umlToolRegistered = false
257
307
  let modeContextRegistered = false
258
308
  const registerProvider = (): void => {
259
309
  if (providerRegistered) return
260
310
  const inspect = ctx.get('cordisInspect')
261
311
  if (inspect === undefined) return
262
312
  try {
263
- ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered, () => composeToolRegistered, () => exportToolRegistered)), 'logicprobe: inspect provider')
313
+ ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered, () => composeToolRegistered, () => exportToolRegistered, () => umlToolRegistered)), 'logicprobe: inspect provider')
264
314
  providerRegistered = true
265
315
  } catch (err) {
266
316
  console.warn('[logicprobe] inspect provider registration failed', err)
@@ -276,11 +326,13 @@ export function apply(ctx: Context, config: Config): void {
276
326
  ctx.effect(() => tools.register(logicProbeConcurrencyScanTool), 'logicprobe: concurrency scan tool')
277
327
  ctx.effect(() => tools.register(logicProbeComposeTool), 'logicprobe: compose tool')
278
328
  ctx.effect(() => tools.register(logicProbeExportTool), 'logicprobe: export tool')
329
+ ctx.effect(() => tools.register(logicProbeUmlTool), 'logicprobe: uml tool')
279
330
  toolRegistered = true
280
331
  dataToolRegistered = true
281
332
  concurrencyToolRegistered = true
282
333
  composeToolRegistered = true
283
334
  exportToolRegistered = true
335
+ umlToolRegistered = true
284
336
  } catch (err) {
285
337
  console.warn('[logicprobe] logicprobe_verify/logicprobe_datamodel_verify tool registration failed', err)
286
338
  }
@@ -324,7 +376,12 @@ export function apply(ctx: Context, config: Config): void {
324
376
  customSkillDirs: [SKILLS_DIR],
325
377
  })
326
378
  })
327
- if (!config.enabled) return
379
+ // Injection listens unconditionally, including while the switch is off: the
380
+ // switch is volatile, so `apply` runs once and the value behind it can turn on
381
+ // later from the Web Plugins page. Returning early on a false value here would
382
+ // freeze that decision for the lifetime of the mount, and turning the switch
383
+ // back on could never take effect without a profile restart.
384
+ //
328
385
  // Inject the gate once per session on the FIRST model step that runs,
329
386
  // instead of at session-start: session-start injection lands in the agent's
330
387
  // inbox, which a blank-session preset switch (agentPreset.select ->
@@ -340,6 +397,7 @@ export function apply(ctx: Context, config: Config): void {
340
397
  const decision = await next()
341
398
  if (decision.kind === 'reject') return decision
342
399
  registerIntegrations()
400
+ if (!injectionEnabled(config)) return decision
343
401
  if (gateInHistory(agent.session)) return decision
344
402
  return {
345
403
  kind: 'enter',
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Structural mirror of the JSON value union the dsh tool registry accepts.
3
+ *
4
+ * `@deepseek-ai/dsh-tools` exported this type through the 0.1.x line and stopped
5
+ * exporting it in 0.2.x, where the same type now lives in
6
+ * `@deepseek-ai/dsh-util-values`. This package declares peer support for both
7
+ * lines, so it cannot import the type from either one without breaking the other
8
+ * build. Declaring the one-line union here keeps `npm run typecheck` green against
9
+ * both, and it costs nothing at runtime: every use is a type-only cast on a value
10
+ * this package already produced.
11
+ *
12
+ * Keep it structurally identical to the official type. A cast through `unknown` to
13
+ * this alias is only a way to satisfy `defineTool`'s return-type constraint; if the
14
+ * official union ever widens, this alias must widen with it.
15
+ *
16
+ * @module logicprobe-json-value
17
+ */
18
+
19
+ /** JSON-serializable value: null, boolean, number, string, or an array/object of those. */
20
+ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
package/src/tool.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { defineTool, type JsonValue } from '@deepseek-ai/dsh-tools'
1
+ import { defineTool } from '@deepseek-ai/dsh-tools'
2
+ import type { JsonValue } from './json-value.js'
2
3
  import { runVerification } from './engine.js'
3
4
 
4
5
  export const LOGICPROBE_VERIFY_TOOL_NAME = 'logicprobe_verify'