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.
- package/README.en-US.md +90 -56
- package/README.md +77 -44
- package/lib/client.js +212 -0
- package/lib/index.js +53 -8
- package/lib/json-value.js +18 -0
- package/lib/types/index.d.ts +17 -11
- package/lib/types/json-value.d.ts +21 -0
- package/lib/types/uml-tool.d.ts +14 -0
- package/lib/types/uml.d.ts +167 -0
- package/lib/uml-tool.js +104 -0
- package/lib/uml.js +1544 -0
- package/package.json +29 -16
- package/skills/logicprobe/SKILL.md +36 -1
- package/skills/logicprobe/references/logicprobe-engine.py +1586 -2
- package/skills/logicprobe/references/uml-modeling-guide.md +166 -0
- package/src/client.js +212 -0
- package/src/compose-tool.ts +2 -1
- package/src/concurrency-tool.ts +2 -1
- package/src/data-tool.ts +2 -1
- package/src/export-tool.ts +2 -1
- package/src/index.ts +67 -9
- package/src/json-value.ts +20 -0
- package/src/tool.ts +2 -1
- package/src/uml-tool.ts +106 -0
- package/src/uml.ts +1461 -0
- package/skills/logicprobe/references/__pycache__/logicprobe-engine.cpython-310.pyc +0 -0
|
@@ -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
|
+
})
|
package/src/compose-tool.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { defineTool
|
|
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'
|
package/src/concurrency-tool.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { defineTool
|
|
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
|
|
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'
|
package/src/export-tool.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { defineTool
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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'
|