dsh-logicprobe 0.9.0 → 1.1.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 +128 -66
- package/README.md +128 -66
- package/lib/baseline.js +142 -0
- package/lib/client.js +56 -1
- package/lib/compose-tool.js +1 -1
- package/lib/concurrency.js +14 -0
- package/lib/data-engine.js +42 -8
- package/lib/engine.js +3151 -2801
- package/lib/export-tool.js +27 -2
- package/lib/granularity.js +237 -0
- package/lib/index.js +63 -41
- package/lib/report-diff-tool.js +39 -0
- package/lib/structure-tool.js +64 -0
- package/lib/structure.js +648 -0
- package/lib/tool.js +3 -3
- package/lib/types/baseline.d.ts +69 -0
- package/lib/types/concurrency.d.ts +10 -0
- package/lib/types/data-engine.d.ts +13 -1
- package/lib/types/engine.d.ts +419 -277
- package/lib/types/granularity.d.ts +106 -0
- package/lib/types/index.d.ts +9 -8
- package/lib/types/report-diff-tool.d.ts +8 -0
- package/lib/types/structure-tool.d.ts +10 -0
- package/lib/types/structure.d.ts +156 -0
- package/lib/types/tool.d.ts +2 -2
- package/lib/types/uml.d.ts +89 -0
- package/lib/uml-tool.js +146 -104
- package/lib/uml.js +1748 -1544
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +16 -14
- package/skills/logicprobe/SKILL.md +84 -58
- package/skills/logicprobe/references/dsh-model-schema.md +102 -10
- package/skills/logicprobe/references/gap-routing-guide.md +37 -36
- package/skills/logicprobe/references/hash-spec.md +153 -0
- package/skills/logicprobe/references/logic-verification-guide.md +27 -250
- package/skills/logicprobe-concurrency/SKILL.md +43 -0
- package/skills/{logicprobe → logicprobe-concurrency}/references/concurrency-risk-guide.md +1 -1
- package/skills/logicprobe-datamodel/SKILL.md +5 -4
- package/skills/logicprobe-datamodel/references/data-model-guide.md +1 -1
- package/skills/logicprobe-datamodel/references/data-model-schema.md +1 -1
- package/skills/logicprobe-structure/SKILL.md +95 -0
- package/skills/logicprobe-structure/references/structure-review-guide.md +177 -0
- package/skills/logicprobe-uml/SKILL.md +57 -0
- package/skills/{logicprobe → logicprobe-uml}/references/uml-modeling-guide.md +121 -8
- package/src/baseline.ts +180 -0
- package/src/client.js +56 -1
- package/src/compose-tool.ts +47 -47
- package/src/concurrency.ts +23 -0
- package/src/data-engine.ts +51 -9
- package/src/engine.ts +503 -72
- package/src/export-tool.ts +73 -48
- package/src/granularity.ts +312 -0
- package/src/index.ts +63 -41
- package/src/report-diff-tool.ts +43 -0
- package/src/structure-tool.ts +68 -0
- package/src/structure.ts +724 -0
- package/src/tool.ts +3 -3
- package/src/uml-tool.ts +53 -10
- package/src/uml.ts +288 -24
- package/skills/logicprobe/references/logicprobe-engine.py +0 -4943
- package/skills/logicprobe/references/verification-harness.py +0 -959
- package/skills/logicprobe-datamodel/references/data-model-harness.py +0 -528
package/lib/export-tool.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, modelHash, refusalVerdict, validateModel, verdictOf } from './engine.js';
|
|
2
3
|
import { exportModel } from './exporters.js';
|
|
4
|
+
/** The model hash of an export's input, when the input validates (the success path always does). */
|
|
5
|
+
function exportHashes(model) {
|
|
6
|
+
const validation = validateModel(model);
|
|
7
|
+
return validation.ok
|
|
8
|
+
? { hashSpec: DEFAULT_HASH_SPEC, modelHash: modelHash(validation.model) }
|
|
9
|
+
: { hashSpec: DEFAULT_HASH_SPEC };
|
|
10
|
+
}
|
|
3
11
|
export const LOGICPROBE_EXPORT_TOOL_NAME = 'logicprobe_export';
|
|
4
12
|
/**
|
|
5
13
|
* DSH tool wrapping the external-tool exporters: turn a LogicModelV1 machine into
|
|
@@ -36,10 +44,27 @@ export const logicProbeExportTool = defineTool({
|
|
|
36
44
|
async execute(args) {
|
|
37
45
|
try {
|
|
38
46
|
const result = exportModel(args.model, args.format);
|
|
39
|
-
|
|
47
|
+
const warnings = result.warnings ?? [];
|
|
48
|
+
const nextSteps = [];
|
|
49
|
+
if (warnings.length > 0)
|
|
50
|
+
nextSteps.push('The target notation cannot express some invariants; the warnings name each one — verify them in the target tool rather than assuming they carried over.');
|
|
51
|
+
nextSteps.push('Run the exported file through its own tool (verify/compile) before trusting the translation; logicprobe guarantees the encoding, not the target checker\'s verdict.');
|
|
52
|
+
return {
|
|
53
|
+
ok: true,
|
|
54
|
+
ran: true,
|
|
55
|
+
...verdictOf(0, warnings.length),
|
|
56
|
+
schema: REPORT_SCHEMAS.export,
|
|
57
|
+
format: result.format,
|
|
58
|
+
primary: result.primary,
|
|
59
|
+
...(result.extras === undefined ? {} : { extras: result.extras }),
|
|
60
|
+
hashes: exportHashes(args.model),
|
|
61
|
+
warnings,
|
|
62
|
+
nextSteps,
|
|
63
|
+
};
|
|
40
64
|
}
|
|
41
65
|
catch (error) {
|
|
42
|
-
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
return { ok: false, ran: false, ...refusalVerdict(message), schema: REPORT_SCHEMAS.export, errorCode: 'MODEL_INVALID', error: message };
|
|
43
68
|
}
|
|
44
69
|
},
|
|
45
70
|
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-granularity structure review: one architecture, several levels of detail.
|
|
3
|
+
*
|
|
4
|
+
* A module diagram and the repository-wide diagram it belongs to are two views of one
|
|
5
|
+
* fact, and nothing keeps them in step — the child grows a dependency the parent never
|
|
6
|
+
* had, or the parent keeps an arrow the child quietly stopped drawing. This module
|
|
7
|
+
* checks that a child diagram is a **refinement** of its declared parent:
|
|
8
|
+
*
|
|
9
|
+
* - every dependency the child draws between two nodes the parent also has must exist in
|
|
10
|
+
* the parent (nothing appears out of nowhere);
|
|
11
|
+
* - an edge the parent draws and the child does not must be *expanded* in the child, i.e.
|
|
12
|
+
* a child path must connect the same two ends;
|
|
13
|
+
* - the declared parent relation must be a forest, not a cycle.
|
|
14
|
+
*
|
|
15
|
+
* Every per-diagram structural check still runs, and its findings are tagged with the
|
|
16
|
+
* diagram they came from (`file`), so one report can carry several diagrams without the
|
|
17
|
+
* findings becoming anonymous.
|
|
18
|
+
*
|
|
19
|
+
* @module logicprobe-granularity
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { REPORT_SCHEMAS, canonicalJson, verdictOfFindings } from './engine.js';
|
|
23
|
+
import { parseStructure, reviewStructure } from './structure.js';
|
|
24
|
+
/** Reachability in one diagram, ignoring edge direction? No: a dependency path is directed. */
|
|
25
|
+
function reaches(graph, from, to) {
|
|
26
|
+
const adjacency = new Map();
|
|
27
|
+
for (const node of graph.nodes)
|
|
28
|
+
adjacency.set(node.id, []);
|
|
29
|
+
for (const edge of graph.edges)
|
|
30
|
+
adjacency.get(edge.from)?.push(edge.to);
|
|
31
|
+
const seen = new Set([from]);
|
|
32
|
+
const queue = [from];
|
|
33
|
+
while (queue.length > 0) {
|
|
34
|
+
const current = queue.shift();
|
|
35
|
+
for (const next of adjacency.get(current) ?? []) {
|
|
36
|
+
if (next === to)
|
|
37
|
+
return true;
|
|
38
|
+
if (seen.has(next))
|
|
39
|
+
continue;
|
|
40
|
+
seen.add(next);
|
|
41
|
+
queue.push(next);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
/** The parent relation must be a forest: report the first cycle it contains, if any. */
|
|
47
|
+
function parentCycle(diagrams) {
|
|
48
|
+
const byName = new Map(diagrams.map((entry) => [entry.name, entry]));
|
|
49
|
+
for (const start of diagrams) {
|
|
50
|
+
const path = [start.name];
|
|
51
|
+
const seen = new Set([start.name]);
|
|
52
|
+
let cursor = start.parent;
|
|
53
|
+
while (cursor !== undefined) {
|
|
54
|
+
if (seen.has(cursor)) {
|
|
55
|
+
path.push(cursor);
|
|
56
|
+
return path;
|
|
57
|
+
}
|
|
58
|
+
seen.add(cursor);
|
|
59
|
+
path.push(cursor);
|
|
60
|
+
cursor = byName.get(cursor)?.parent;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Review a set of structure diagrams with declared parent relations. Per-diagram
|
|
67
|
+
* structural checks run first; the cross-diagram refinement checks follow.
|
|
68
|
+
*/
|
|
69
|
+
export function reviewGranularity(options) {
|
|
70
|
+
const diagrams = options.diagrams;
|
|
71
|
+
const findings = [];
|
|
72
|
+
const warnings = [];
|
|
73
|
+
const diagramSummaries = [];
|
|
74
|
+
const graphs = new Map();
|
|
75
|
+
if (diagrams.length === 0) {
|
|
76
|
+
findings.push({ code: 'UML031_NO_DIAGRAMS', severity: 'error', message: 'no diagram was supplied, so there are no granularity levels to compare.' });
|
|
77
|
+
}
|
|
78
|
+
const cycle = parentCycle(diagrams);
|
|
79
|
+
if (cycle !== undefined) {
|
|
80
|
+
findings.push({
|
|
81
|
+
code: 'UML030_PARENT_CYCLE',
|
|
82
|
+
severity: 'error',
|
|
83
|
+
message: 'the declared parent relation has a cycle: ' + cycle.join(' → ') + '. A refinement hierarchy must be a forest, or "which level owns this dependency" has no answer.',
|
|
84
|
+
evidence: { cycle },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const knownNames = new Set(diagrams.map((entry) => entry.name));
|
|
88
|
+
for (const entry of diagrams) {
|
|
89
|
+
if (entry.parent !== undefined && !knownNames.has(entry.parent)) {
|
|
90
|
+
findings.push({
|
|
91
|
+
code: 'UML028_UNKNOWN_PARENT',
|
|
92
|
+
severity: 'error',
|
|
93
|
+
message: 'diagram "' + entry.name + '" declares parent "' + entry.parent + '", which is not part of this set.',
|
|
94
|
+
file: entry.name,
|
|
95
|
+
evidence: { diagram: entry.name, parent: entry.parent, known: [...knownNames].sort() },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const report = reviewStructure({
|
|
99
|
+
diagram: entry.diagram,
|
|
100
|
+
notation: options.notation ?? 'auto',
|
|
101
|
+
...(options.matrix === undefined ? {} : { matrix: options.matrix }),
|
|
102
|
+
});
|
|
103
|
+
graphs.set(entry.name, report.graph);
|
|
104
|
+
// Tag every finding with the diagram it came from: with several diagrams in one
|
|
105
|
+
// report, an untagged finding is unactionable.
|
|
106
|
+
for (const finding of report.findings) {
|
|
107
|
+
findings.push({ ...finding, file: entry.name, ...(finding.evidence === undefined ? {} : {}) });
|
|
108
|
+
}
|
|
109
|
+
warnings.push(...report.warnings.map((warning) => entry.name + ': ' + warning));
|
|
110
|
+
diagramSummaries.push({
|
|
111
|
+
name: entry.name,
|
|
112
|
+
...(entry.parent === undefined ? {} : { parent: entry.parent }),
|
|
113
|
+
notation: report.notation,
|
|
114
|
+
nodes: report.summary.nodes,
|
|
115
|
+
edges: report.summary.edges,
|
|
116
|
+
verdict: report.verdict,
|
|
117
|
+
findings: report.findings.length,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const pairs = [];
|
|
121
|
+
for (const child of diagrams) {
|
|
122
|
+
if (child.parent === undefined)
|
|
123
|
+
continue;
|
|
124
|
+
const parent = diagrams.find((entry) => entry.name === child.parent);
|
|
125
|
+
const parentGraph = graphs.get(child.parent);
|
|
126
|
+
const childGraph = graphs.get(child.name);
|
|
127
|
+
if (parent === undefined || parentGraph === undefined || childGraph === undefined)
|
|
128
|
+
continue;
|
|
129
|
+
const parentNodes = new Set(parentGraph.nodes.map((node) => node.id));
|
|
130
|
+
const childNodes = new Set(childGraph.nodes.map((node) => node.id));
|
|
131
|
+
const parentEdges = new Set(parentGraph.edges.map((edge) => edge.from + '\u0000' + edge.to));
|
|
132
|
+
const childEdges = new Set(childGraph.edges.map((edge) => edge.from + '\u0000' + edge.to));
|
|
133
|
+
let inheritedEdges = 0;
|
|
134
|
+
let newEdges = 0;
|
|
135
|
+
const inventedEdges = [];
|
|
136
|
+
for (const edge of childGraph.edges) {
|
|
137
|
+
const hasFrom = parentNodes.has(edge.from);
|
|
138
|
+
const hasTo = parentNodes.has(edge.to);
|
|
139
|
+
if (!hasFrom || !hasTo) {
|
|
140
|
+
newEdges += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (parentEdges.has(edge.from + '\u0000' + edge.to)) {
|
|
144
|
+
inheritedEdges += 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
inventedEdges.push({ from: edge.from, to: edge.to, line: edge.line });
|
|
148
|
+
}
|
|
149
|
+
const expandedEdges = [];
|
|
150
|
+
const missingEdges = [];
|
|
151
|
+
for (const edge of parentGraph.edges) {
|
|
152
|
+
const key = edge.from + '\u0000' + edge.to;
|
|
153
|
+
if (childEdges.has(key))
|
|
154
|
+
continue;
|
|
155
|
+
// The child only has to answer for a parent edge whose ends it also carries; a child
|
|
156
|
+
// may legitimately cover a subtree of the parent.
|
|
157
|
+
if (!childNodes.has(edge.from) || !childNodes.has(edge.to))
|
|
158
|
+
continue;
|
|
159
|
+
if (reaches(childGraph, edge.from, edge.to))
|
|
160
|
+
expandedEdges.push({ from: edge.from, to: edge.to, line: edge.line });
|
|
161
|
+
else
|
|
162
|
+
missingEdges.push({ from: edge.from, to: edge.to, line: edge.line });
|
|
163
|
+
}
|
|
164
|
+
if (inventedEdges.length > 0) {
|
|
165
|
+
findings.push({
|
|
166
|
+
code: 'UML029_REFINEMENT_VIOLATION',
|
|
167
|
+
severity: 'error',
|
|
168
|
+
message: 'diagram "' + child.name + '" draws ' + String(inventedEdges.length) + ' dependency(ies) between nodes its parent "' + parent.name + '" also has, but the parent does not: ' + inventedEdges.map((edge) => edge.from + ' → ' + edge.to).join(', ') + '. A refinement may add detail below the parent level, never a dependency at the parent level.',
|
|
169
|
+
file: child.name,
|
|
170
|
+
evidence: { kind: 'invented-edge', parent: parent.name, edges: inventedEdges },
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (missingEdges.length > 0) {
|
|
174
|
+
findings.push({
|
|
175
|
+
code: 'UML029_REFINEMENT_VIOLATION',
|
|
176
|
+
severity: 'error',
|
|
177
|
+
message: 'diagram "' + child.name + '" drops ' + String(missingEdges.length) + ' dependency(ies) its parent "' + parent.name + '" draws, without expanding them into a path: ' + missingEdges.map((edge) => edge.from + ' → ' + edge.to).join(', ') + '.',
|
|
178
|
+
file: child.name,
|
|
179
|
+
evidence: { kind: 'unexpanded-edge', parent: parent.name, edges: missingEdges },
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
pairs.push({
|
|
183
|
+
parent: parent.name,
|
|
184
|
+
child: child.name,
|
|
185
|
+
parentNodes: parentNodes.size,
|
|
186
|
+
childNodes: childNodes.size,
|
|
187
|
+
parentEdges: parentGraph.edges.length,
|
|
188
|
+
childEdges: childGraph.edges.length,
|
|
189
|
+
inheritedEdges,
|
|
190
|
+
newEdges,
|
|
191
|
+
expandedEdges,
|
|
192
|
+
missingEdges,
|
|
193
|
+
inventedEdges,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const errors = findings.filter((finding) => finding.severity === 'error').length;
|
|
197
|
+
const warningCount = findings.filter((finding) => finding.severity === 'warning').length;
|
|
198
|
+
const invented = pairs.reduce((sum, pair) => sum + pair.inventedEdges.length, 0);
|
|
199
|
+
const missing = pairs.reduce((sum, pair) => sum + pair.missingEdges.length, 0);
|
|
200
|
+
const expanded = pairs.reduce((sum, pair) => sum + pair.expandedEdges.length, 0);
|
|
201
|
+
const nextSteps = [];
|
|
202
|
+
if (errors > 0)
|
|
203
|
+
nextSteps.push('Fix the refinement violations first: a level that invents or drops a parent-level dependency makes the hierarchy unusable as a rule source.');
|
|
204
|
+
if (pairs.length === 0 && diagrams.length > 0)
|
|
205
|
+
nextSteps.push('Give the levels a `parent` so the refinement can be checked; a set of unrelated diagrams has no consistency property to verify.');
|
|
206
|
+
if (expanded > 0)
|
|
207
|
+
nextSteps.push(String(expanded) + ' parent edge(s) are drawn as a path in the child — that is a legitimate expansion, and it is listed per pair so a reviewer can confirm each one.');
|
|
208
|
+
nextSteps.push('Reconcile the levels with the source-side scan: the diagram pair is consistent or not, and neither says anything about the code until it is scanned.');
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
ran: true,
|
|
212
|
+
...verdictOfFindings(findings),
|
|
213
|
+
schema: REPORT_SCHEMAS.granularity,
|
|
214
|
+
diagrams: diagramSummaries,
|
|
215
|
+
pairs,
|
|
216
|
+
findings,
|
|
217
|
+
summary: {
|
|
218
|
+
diagrams: diagrams.length,
|
|
219
|
+
roots: diagrams.filter((entry) => entry.parent === undefined).length,
|
|
220
|
+
pairs: pairs.length,
|
|
221
|
+
nodes: diagrams.reduce((sum, entry) => sum + (graphs.get(entry.name)?.nodes.length ?? 0), 0),
|
|
222
|
+
edges: diagrams.reduce((sum, entry) => sum + (graphs.get(entry.name)?.edges.length ?? 0), 0),
|
|
223
|
+
inventedEdges: invented,
|
|
224
|
+
missingEdges: missing,
|
|
225
|
+
expandedEdges: expanded,
|
|
226
|
+
errors,
|
|
227
|
+
warnings: warningCount,
|
|
228
|
+
},
|
|
229
|
+
hashes: { diagrams: diagrams.map((entry) => ({ name: entry.name, hash: createHash('sha256').update(entry.diagram).digest('hex') })) },
|
|
230
|
+
warnings,
|
|
231
|
+
nextSteps,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** Deterministic JSON of a granularity report's pair table, for a stable summary line. */
|
|
235
|
+
export function granularityDigest(pairs) {
|
|
236
|
+
return canonicalJson(pairs);
|
|
237
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* logicprobe — DeepSeek Harness native plugin for the Logic Probe toolbox.
|
|
3
|
-
* Injects the session-start gate text (
|
|
4
|
-
* Red Flags,
|
|
5
|
-
*
|
|
3
|
+
* Injects the session-start gate text (what this plugin checks, the 1% Rule,
|
|
4
|
+
* the Red Flags, the routing to the skills, the tool list, how to read a report)
|
|
5
|
+
* into the first model step of every agent session, mirroring the SessionStart
|
|
6
|
+
* hook the Claude Code plugin installs.
|
|
6
7
|
* The skill ships in this package's `skills/` directory and is registered at
|
|
7
8
|
* apply time into dsh's `ctx.skills` registry through the standard filesystem
|
|
8
9
|
* provider, so it appears in every session catalog without a manual copy step.
|
|
@@ -16,11 +17,11 @@
|
|
|
16
17
|
* reminders (skill catalog, AGENTS.md, gate plugins) simply defer this message
|
|
17
18
|
* to the first step after their promotion, and the history guard re-injects it
|
|
18
19
|
* there. The default gate text is the dsh-native adaptation of
|
|
19
|
-
* `hooks/session-start-content.md
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* override via Config.
|
|
20
|
+
* `hooks/session-start-content.md`. Both are a guide to calling the tools: what
|
|
21
|
+
* this plugin checks, the 1% Rule and the Red Flags that raise the chance of a
|
|
22
|
+
* call, the routing to the five skills, the tool list, and how to read a report.
|
|
23
|
+
* They stay in sync. Method, doctrine and per-domain detail live in the skills
|
|
24
|
+
* and their references, not here. Deployments override via Config.
|
|
24
25
|
*
|
|
25
26
|
* @module logicprobe-dsh
|
|
26
27
|
*/
|
|
@@ -34,6 +35,8 @@ import { logicProbeConcurrencyScanTool } from './concurrency-tool.js';
|
|
|
34
35
|
import { logicProbeComposeTool } from './compose-tool.js';
|
|
35
36
|
import { logicProbeExportTool } from './export-tool.js';
|
|
36
37
|
import { logicProbeUmlTool } from './uml-tool.js';
|
|
38
|
+
import { logicProbeStructureTool } from './structure-tool.js';
|
|
39
|
+
import { logicProbeReportDiffTool } from './report-diff-tool.js';
|
|
37
40
|
import { ENGINE_SCHEMA_VERSION } from './engine.js';
|
|
38
41
|
export const name = 'logicprobe';
|
|
39
42
|
// Skills are contributed through the registry service, which dsh-base always
|
|
@@ -46,33 +49,42 @@ const SKILLS_DIR = fileURLToPath(new URL('../skills', import.meta.url));
|
|
|
46
49
|
const GATE_PLUGIN_ID = 'logicprobe';
|
|
47
50
|
/** Producer-owned message source kind declared in `MessageSourceMap` above. */
|
|
48
51
|
const GATE_SOURCE_KIND = 'plugin:logicprobe';
|
|
49
|
-
const DEFAULT_GATE_CONTENT =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
- \`
|
|
66
|
-
- \`
|
|
67
|
-
- \`
|
|
68
|
-
- \`
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
52
|
+
const DEFAULT_GATE_CONTENT = `
|
|
53
|
+
<EXTREMELY_IMPORTANT>
|
|
54
|
+
Plugin logicprobe is active. It checks claims about designs and code: facts against the source, behaviour as an executable model.
|
|
55
|
+
|
|
56
|
+
**1% Rule**: if any claim about this code deserves a check, load the matching skill first. Loading is cheap. A false claim is not.
|
|
57
|
+
|
|
58
|
+
**Unsure whether a check applies?** Load \`logicprobe\` first. It is the entry point: it owns the doctrine, the model schema, the depth rules and the routing to its siblings.
|
|
59
|
+
|
|
60
|
+
**Load the skill that matches the claim**: \`logicprobe\` (entry point; claim and behaviour checks), \`logicprobe-uml\` (diagrams), \`logicprobe-structure\` (architecture, dependencies), \`logicprobe-concurrency\` (concurrency claims), \`logicprobe-datamodel\` (data models, migrations).
|
|
61
|
+
|
|
62
|
+
**Tools** (dsh):
|
|
63
|
+
|
|
64
|
+
- \`logicprobe_verify\` — state machines: structural checks, adversarial probes, budgets, deadlines, before/after regression.
|
|
65
|
+
- \`logicprobe_datamodel_verify\` — data models and migrations: structure, migration coverage, copy consistency, rollback, before/after data.
|
|
66
|
+
- \`logicprobe_compose_verify\` — two or more machines: deadlock, and a rendezvous that never fires.
|
|
67
|
+
- \`logicprobe_concurrency_scan\` — finds concurrency claims and routes them. It proves nothing about concurrency.
|
|
68
|
+
- \`logicprobe_export\` — UPPAAL, TLA+, PRISM and SPIN input from a verified model.
|
|
69
|
+
- \`logicprobe_uml\` — render, parse or review a UML diagram.
|
|
70
|
+
- \`logicprobe_structure_verify\` — dependency review of a structure diagram, at one level or several.
|
|
71
|
+
- \`logicprobe_report_diff\` — baseline against current: what a change added, removed or changed.
|
|
72
|
+
|
|
73
|
+
**Read a report**: gate on \`verdict\` (\`pass\`, \`pass_with_findings\`, \`fail\`), never on \`ok\`. Reports carry \`verdictReason\`, \`hashes\` and \`nextSteps\`. Keys starting with \`_\` are notes: ignored, and left out of the model hash.
|
|
74
|
+
|
|
75
|
+
**Red Flags** — if you think any of these, stop:
|
|
76
|
+
|
|
77
|
+
| You think | Reality |
|
|
78
|
+
|-----------|---------|
|
|
79
|
+
| "Too simple to check" | The check is cheap. The silent skip is not. |
|
|
80
|
+
| "I read the code, so I know" | Knowing is not evidence. Cite \`file:line\`, or run the model, or call the claim unverified. |
|
|
81
|
+
| "The spec or the diagram says so" | Both are claims about the code. Reconcile them with a source-side scan. |
|
|
82
|
+
| "I'll check while implementing" | Check before the decision, not after. |
|
|
83
|
+
| "Reasoning is enough" | Reasoning is not a run. One counter-example kills a universal claim. |
|
|
84
|
+
| "The report passed, so the code is fine" | The report covers the model you gave it. Reconcile it, and say what you did not check. |
|
|
85
|
+
|
|
86
|
+
</EXTREMELY_IMPORTANT>
|
|
87
|
+
`;
|
|
76
88
|
/**
|
|
77
89
|
* Declare a field as live where this host's schemastery can — `.volatile()`
|
|
78
90
|
* arrived in 3.18.3 — and leave it an ordinary field where it cannot.
|
|
@@ -155,8 +167,8 @@ function resolveInteraction(config, session) {
|
|
|
155
167
|
function modeContextText(config, session) {
|
|
156
168
|
const interaction = resolveInteraction(config, session);
|
|
157
169
|
const lines = [
|
|
158
|
-
'logicprobe:
|
|
159
|
-
'logicprobe:
|
|
170
|
+
'logicprobe: tools — `logicprobe_verify` (state machines), `logicprobe_datamodel_verify` (data models, migrations), `logicprobe_compose_verify` (two or more machines), `logicprobe_concurrency_scan` (finds concurrency claims, proves nothing), `logicprobe_export` (UPPAAL, TLA+, PRISM, SPIN), `logicprobe_uml` (render, parse, review UML), `logicprobe_structure_verify` (dependency diagrams, one or several levels), `logicprobe_report_diff` (baseline vs current).',
|
|
171
|
+
'logicprobe: gate on `verdict`, never on `ok`. Reports carry `verdictReason`, `hashes` and `nextSteps`.',
|
|
160
172
|
interaction === 'auto'
|
|
161
173
|
? '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.'
|
|
162
174
|
: 'logicprobe interaction=ask: show the extracted transition table and get user confirmation before running verification.',
|
|
@@ -171,11 +183,11 @@ function modeContextText(config, session) {
|
|
|
171
183
|
* lets the model read this plugin's runtime status without guessing. Mirrors
|
|
172
184
|
* the registration pattern of the official dsh-tool-cordis host providers.
|
|
173
185
|
*/
|
|
174
|
-
function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcurrencyToolRegistered, isComposeToolRegistered, isExportToolRegistered, isUmlToolRegistered) {
|
|
186
|
+
function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcurrencyToolRegistered, isComposeToolRegistered, isExportToolRegistered, isUmlToolRegistered, isStructureToolRegistered, isReportDiffToolRegistered) {
|
|
175
187
|
return {
|
|
176
188
|
manifest: {
|
|
177
189
|
id: 'logicprobe',
|
|
178
|
-
description: 'Session-start gate injection and native verification tooling for the Logic Probe toolbox
|
|
190
|
+
description: 'Session-start gate injection and native verification tooling for the Logic Probe toolbox. The gate is a guide to calling the tools: what the plugin checks, the 1% Rule and Red Flags, the routing to the five skills, the tool list, and how to read a report. Doctrine and per-domain detail live in the skills.',
|
|
179
191
|
methods: [
|
|
180
192
|
{
|
|
181
193
|
name: 'status',
|
|
@@ -198,10 +210,12 @@ function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcu
|
|
|
198
210
|
composeToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_compose_verify tool is registered on ctx.tools.' },
|
|
199
211
|
exportToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_export tool is registered on ctx.tools.' },
|
|
200
212
|
umlToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_uml tool (UML modelling + modelling review) is registered on ctx.tools.' },
|
|
213
|
+
structureToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_structure_verify tool (structure-diagram / dependency review) is registered on ctx.tools.' },
|
|
214
|
+
reportDiffToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_report_diff tool (baseline report comparison) is registered on ctx.tools.' },
|
|
201
215
|
engineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled state-machine verification engine accepts.' },
|
|
202
216
|
dataEngineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled data-model verification engine accepts.' },
|
|
203
217
|
},
|
|
204
|
-
required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'composeToolRegistered', 'exportToolRegistered', 'umlToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
|
|
218
|
+
required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'composeToolRegistered', 'exportToolRegistered', 'umlToolRegistered', 'structureToolRegistered', 'reportDiffToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
|
|
205
219
|
additionalProperties: false,
|
|
206
220
|
},
|
|
207
221
|
},
|
|
@@ -219,6 +233,8 @@ function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcu
|
|
|
219
233
|
composeToolRegistered: isComposeToolRegistered(),
|
|
220
234
|
exportToolRegistered: isExportToolRegistered(),
|
|
221
235
|
umlToolRegistered: isUmlToolRegistered(),
|
|
236
|
+
structureToolRegistered: isStructureToolRegistered(),
|
|
237
|
+
reportDiffToolRegistered: isReportDiffToolRegistered(),
|
|
222
238
|
engineSchemaVersion: ENGINE_SCHEMA_VERSION,
|
|
223
239
|
dataEngineSchemaVersion: DATA_ENGINE_SCHEMA_VERSION,
|
|
224
240
|
};
|
|
@@ -238,6 +254,8 @@ export function apply(ctx, config) {
|
|
|
238
254
|
let composeToolRegistered = false;
|
|
239
255
|
let exportToolRegistered = false;
|
|
240
256
|
let umlToolRegistered = false;
|
|
257
|
+
let structureToolRegistered = false;
|
|
258
|
+
let reportDiffToolRegistered = false;
|
|
241
259
|
let modeContextRegistered = false;
|
|
242
260
|
const registerProvider = () => {
|
|
243
261
|
if (providerRegistered)
|
|
@@ -246,7 +264,7 @@ export function apply(ctx, config) {
|
|
|
246
264
|
if (inspect === undefined)
|
|
247
265
|
return;
|
|
248
266
|
try {
|
|
249
|
-
ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered, () => composeToolRegistered, () => exportToolRegistered, () => umlToolRegistered)), 'logicprobe: inspect provider');
|
|
267
|
+
ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered, () => composeToolRegistered, () => exportToolRegistered, () => umlToolRegistered, () => structureToolRegistered, () => reportDiffToolRegistered)), 'logicprobe: inspect provider');
|
|
250
268
|
providerRegistered = true;
|
|
251
269
|
}
|
|
252
270
|
catch (err) {
|
|
@@ -266,12 +284,16 @@ export function apply(ctx, config) {
|
|
|
266
284
|
ctx.effect(() => tools.register(logicProbeComposeTool), 'logicprobe: compose tool');
|
|
267
285
|
ctx.effect(() => tools.register(logicProbeExportTool), 'logicprobe: export tool');
|
|
268
286
|
ctx.effect(() => tools.register(logicProbeUmlTool), 'logicprobe: uml tool');
|
|
287
|
+
ctx.effect(() => tools.register(logicProbeStructureTool), 'logicprobe: structure tool');
|
|
288
|
+
ctx.effect(() => tools.register(logicProbeReportDiffTool), 'logicprobe: report diff tool');
|
|
269
289
|
toolRegistered = true;
|
|
270
290
|
dataToolRegistered = true;
|
|
271
291
|
concurrencyToolRegistered = true;
|
|
272
292
|
composeToolRegistered = true;
|
|
273
293
|
exportToolRegistered = true;
|
|
274
294
|
umlToolRegistered = true;
|
|
295
|
+
structureToolRegistered = true;
|
|
296
|
+
reportDiffToolRegistered = true;
|
|
275
297
|
}
|
|
276
298
|
catch (err) {
|
|
277
299
|
console.warn('[logicprobe] logicprobe_verify/logicprobe_datamodel_verify tool registration failed', err);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { diffReports } from './baseline.js';
|
|
3
|
+
export const LOGICPROBE_REPORT_DIFF_TOOL_NAME = 'logicprobe_report_diff';
|
|
4
|
+
/**
|
|
5
|
+
* Model-visible DSH tool that compares two reports of the same family — the accepted
|
|
6
|
+
* "violations must not increase" acceptance criterion, computed instead of eyeballed.
|
|
7
|
+
* A finding's identity is its check + code + machine-readable locator, so a reworded
|
|
8
|
+
* message reads as `changed`, and two runs over the same input add nothing.
|
|
9
|
+
*/
|
|
10
|
+
export const logicProbeReportDiffTool = defineTool({
|
|
11
|
+
name: LOGICPROBE_REPORT_DIFF_TOOL_NAME,
|
|
12
|
+
description: 'Compare two logicprobe reports of the same family: `baseline` (earlier) against `current` (newer). Use it to accept a slice by "no new findings". Findings match on a stable identity: check id, code, and a locator built from `evidence` and `path`. Reworded prose counts as `changed`, not as new. Returns added, removed and changed findings, plus `summary`. The `verdict` is about the delta: a newly added error finding fails it. `currentVerdict` keeps the run\'s own verdict, so a clean delta over a failing report still shows as failing. Two runs over the same input add nothing.',
|
|
13
|
+
parameters: {
|
|
14
|
+
baseline: {
|
|
15
|
+
type: 'json',
|
|
16
|
+
required: true,
|
|
17
|
+
description: 'The earlier report (any family: verify, compose, datamodel, uml review, structure, concurrency).',
|
|
18
|
+
},
|
|
19
|
+
current: {
|
|
20
|
+
type: 'json',
|
|
21
|
+
required: true,
|
|
22
|
+
description: 'The current report to compare against the baseline.',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
output: {
|
|
26
|
+
schema: {
|
|
27
|
+
type: 'json',
|
|
28
|
+
description: 'Baseline diff report: added/removed/changed findings, delta verdict, current verdict, summary and next steps.',
|
|
29
|
+
},
|
|
30
|
+
render(_args, value) {
|
|
31
|
+
return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
timeoutMs: 10_000,
|
|
35
|
+
isConcurrencySafe: () => true,
|
|
36
|
+
async execute(args) {
|
|
37
|
+
return diffReports(args.baseline, args.current);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { REPORT_SCHEMAS } from './engine.js';
|
|
3
|
+
import { reviewStructure } from './structure.js';
|
|
4
|
+
import { reviewGranularity } from './granularity.js';
|
|
5
|
+
export const LOGICPROBE_STRUCTURE_TOOL_NAME = 'logicprobe_structure_verify';
|
|
6
|
+
/**
|
|
7
|
+
* Model-visible DSH tool that audits a *structure* diagram — components, packages,
|
|
8
|
+
* classes, deployment nodes and the dependency arrows between them — against an
|
|
9
|
+
* optional dependency matrix. A structure diagram is not a state machine: this tool
|
|
10
|
+
* reads the graph the diagram actually draws, instead of refusing it (which is what
|
|
11
|
+
* `logicprobe_uml action=parse` does) or pretending the arrows are state transitions
|
|
12
|
+
* (which is what a false `ok` used to hide).
|
|
13
|
+
*/
|
|
14
|
+
export const logicProbeStructureTool = defineTool({
|
|
15
|
+
name: LOGICPROBE_STRUCTURE_TOOL_NAME,
|
|
16
|
+
description: 'Audit a structure diagram as a dependency graph. Takes PlantUML component, package, class or deployment text, or a Mermaid class diagram. Reports UML020 isolated node, UML021 dangling reference, UML022 cycle (shortest path), UML023 disallowed edge and UML024 layer violation against a matrix, UML025 missing required edge, and UML026 when no node is declared. The matrix is {rules: [{id, source, allow?, deny?, require?}], layers: [{name, members}], default: "allow"|"deny"}. Ids and members accept globs (`*`, `?`). Deny beats allow, and layers run top-first, so downward edges pass and upward ones are UML024. With a matrix, every edge appears in `edgeVerdicts` with the rules it matched. Pass `diagrams` instead of `diagram` to check several levels at once: each child must refine its parent. This audits the diagram, not the code: reconcile it with a source-side scan. Read `verdict`, not `ok`. For state or activity diagrams use `logicprobe_uml` and `logicprobe_verify`.',
|
|
17
|
+
parameters: {
|
|
18
|
+
diagram: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Structure-diagram source text: PlantUML component/package/class/deployment, or a Mermaid class diagram. Omit when `diagrams` is given.',
|
|
21
|
+
},
|
|
22
|
+
diagrams: {
|
|
23
|
+
type: 'json',
|
|
24
|
+
description: 'Multi-granularity mode: an array of {name, diagram, parent?} levels. Each diagram is checked on its own AND against its declared parent (UML028 unknown parent, UML029 refinement violation, UML030 parent cycle), with per-pair node/edge counts.',
|
|
25
|
+
},
|
|
26
|
+
notation: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
enum: ['auto', 'plantuml', 'mermaid'],
|
|
29
|
+
description: 'Diagram language. Default auto (detected from the text).',
|
|
30
|
+
},
|
|
31
|
+
matrix: {
|
|
32
|
+
type: 'json',
|
|
33
|
+
description: 'Optional dependency matrix: {rules: [{id, source, allow?, deny?, require?}], layers: [{name, members}], default: "allow"|"deny"}. Every edge is judged against it and the matched rule ids are reported.',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
output: {
|
|
37
|
+
schema: {
|
|
38
|
+
type: 'json',
|
|
39
|
+
description: 'Structure review report: graph (nodes/edges), findings UML020-UML026, per-edge matrix verdicts, summary and next steps.',
|
|
40
|
+
},
|
|
41
|
+
render(_args, value) {
|
|
42
|
+
return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
timeoutMs: 10_000,
|
|
46
|
+
isConcurrencySafe: () => true,
|
|
47
|
+
async execute(args) {
|
|
48
|
+
if (args.diagrams !== undefined) {
|
|
49
|
+
return reviewGranularity({
|
|
50
|
+
diagrams: args.diagrams,
|
|
51
|
+
notation: (args.notation ?? 'auto'),
|
|
52
|
+
...(args.matrix === undefined ? {} : { matrix: args.matrix }),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (args.diagram === undefined) {
|
|
56
|
+
return { ok: false, ran: false, verdict: 'fail', verdictReason: 'the tool did not run: pass `diagram` (one structure diagram) or `diagrams` (several levels with parents)', schema: REPORT_SCHEMAS.structure, errorCode: 'UML_INPUT', error: 'structure review needs `diagram` or `diagrams`' };
|
|
57
|
+
}
|
|
58
|
+
return reviewStructure({
|
|
59
|
+
diagram: args.diagram,
|
|
60
|
+
notation: (args.notation ?? 'auto'),
|
|
61
|
+
...(args.matrix === undefined ? {} : { matrix: args.matrix }),
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
});
|