dsh-logicprobe 0.9.0 → 1.0.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.
Files changed (63) hide show
  1. package/README.en-US.md +79 -14
  2. package/README.md +79 -14
  3. package/lib/baseline.js +142 -0
  4. package/lib/client.js +56 -1
  5. package/lib/compose-tool.js +1 -1
  6. package/lib/concurrency.js +14 -0
  7. package/lib/data-engine.js +42 -8
  8. package/lib/engine.js +379 -79
  9. package/lib/export-tool.js +27 -2
  10. package/lib/granularity.js +237 -0
  11. package/lib/index.js +63 -41
  12. package/lib/report-diff-tool.js +39 -0
  13. package/lib/structure-tool.js +64 -0
  14. package/lib/structure.js +648 -0
  15. package/lib/tool.js +3 -3
  16. package/lib/types/baseline.d.ts +69 -0
  17. package/lib/types/concurrency.d.ts +10 -0
  18. package/lib/types/data-engine.d.ts +13 -1
  19. package/lib/types/engine.d.ts +138 -1
  20. package/lib/types/granularity.d.ts +106 -0
  21. package/lib/types/index.d.ts +9 -8
  22. package/lib/types/report-diff-tool.d.ts +8 -0
  23. package/lib/types/structure-tool.d.ts +10 -0
  24. package/lib/types/structure.d.ts +156 -0
  25. package/lib/types/tool.d.ts +2 -2
  26. package/lib/types/uml.d.ts +89 -0
  27. package/lib/uml-tool.js +36 -7
  28. package/lib/uml.js +217 -23
  29. package/locale/en.json +6 -0
  30. package/locale/zh.json +6 -0
  31. package/package.json +16 -14
  32. package/skills/logicprobe/SKILL.md +84 -58
  33. package/skills/logicprobe/references/dsh-model-schema.md +81 -10
  34. package/skills/logicprobe/references/gap-routing-guide.md +37 -36
  35. package/skills/logicprobe/references/hash-spec.md +128 -0
  36. package/skills/logicprobe/references/logic-verification-guide.md +27 -250
  37. package/skills/logicprobe-concurrency/SKILL.md +43 -0
  38. package/skills/{logicprobe → logicprobe-concurrency}/references/concurrency-risk-guide.md +1 -1
  39. package/skills/logicprobe-datamodel/SKILL.md +5 -4
  40. package/skills/logicprobe-datamodel/references/data-model-guide.md +1 -1
  41. package/skills/logicprobe-datamodel/references/data-model-schema.md +1 -1
  42. package/skills/logicprobe-structure/SKILL.md +95 -0
  43. package/skills/logicprobe-structure/references/structure-review-guide.md +177 -0
  44. package/skills/logicprobe-uml/SKILL.md +55 -0
  45. package/skills/{logicprobe → logicprobe-uml}/references/uml-modeling-guide.md +120 -8
  46. package/src/baseline.ts +180 -0
  47. package/src/client.js +56 -1
  48. package/src/compose-tool.ts +47 -47
  49. package/src/concurrency.ts +23 -0
  50. package/src/data-engine.ts +51 -9
  51. package/src/engine.ts +441 -67
  52. package/src/export-tool.ts +73 -48
  53. package/src/granularity.ts +312 -0
  54. package/src/index.ts +63 -41
  55. package/src/report-diff-tool.ts +43 -0
  56. package/src/structure-tool.ts +68 -0
  57. package/src/structure.ts +724 -0
  58. package/src/tool.ts +3 -3
  59. package/src/uml-tool.ts +38 -8
  60. package/src/uml.ts +275 -24
  61. package/skills/logicprobe/references/logicprobe-engine.py +0 -4943
  62. package/skills/logicprobe/references/verification-harness.py +0 -959
  63. package/skills/logicprobe-datamodel/references/data-model-harness.py +0 -528
@@ -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
- return { ok: true, format: result.format, primary: result.primary, ...(result.extras === undefined ? {} : { extras: result.extras }), warnings: result.warnings };
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
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
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 (claim-verification doctrine, 1% Rule,
4
- * Red Flags, proactive suggestion) into the first model step of every agent
5
- * session, mirroring the SessionStart hook the Claude Code plugin installs.
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`: behavior rules
20
- * (1% Rule / Red Flags / proactive suggestion) stay in sync, while
21
- * presentation is adapted to dsh's native skill catalog — the trigger list
22
- * lives in the skill description, not duplicated in the gate. Deployments
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 = `<EXTREMELY_IMPORTANT>
50
- Plugin logicprobe is active. Documents are not truth — code is. Verify every verifiable claim before accepting or acting on any design.
51
-
52
- **1% Rule**: If there is even a 1% chance the logicprobe skill applies — reviewing design documents, architecture specs, technical proposals, or refactoring plans that make claims about API names, file locations, enum values, mechanism feasibility, state machines, protocol logic, data models, schema migrations, data invariants, behavioral guarantees ("always"/"never"/"guaranteed"), worst-case path cost or declared budgets, probabilistic reachability ("≥90% of runs reach SAFE"), deadlines ("must leave within 2 ticks"), cross-machine handshakes, or concurrency guarantees ("thread-safe"/"lock-free"/"ISR-safe") — load it with the skill tool before responding. The cost of loading is trivial compared to the cost of a false claim.
53
-
54
- **Red Flags** — if you think any of these, STOP. You are rationalizing:
55
-
56
- | You think | Reality |
57
- |-----------|---------|
58
- | "This plan is too simple to verify" | The skill auto-classifies depth (LIGHTWEIGHT / STANDARD / ESCALATED). You don't decide. |
59
- | "I already know the file paths are correct" | Organic verification leaves no audit trail. Run Phase 0, append the "## Plan Verification" block. |
60
- | "I'll verify while implementing" | Verification happens before implementation, not during. |
61
- | "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
62
-
63
- **Native verification path** (dsh) — six model-invocable tools:
64
-
65
- - \`logicprobe_verify\` — state machines: S1-S8 structural + A1-A14 adversarial, including A12 worst-case path cost against a declared \`budget\`, A13 probabilistic reachability over a DTMC, and A14 discrete-tick deadlines. Pass \`beforeModel\` + \`stateMapping\` to add the D1-D4 before/after regression.
66
- - \`logicprobe_datamodel_verify\` — data models and schema migrations: DS/DA checks, migration coverage, copy consistency, rollback symmetry, DD1-DD4 data regression.
67
- - \`logicprobe_compose_verify\` — two or more machines checked together (rendezvous handshake semantics): C1 composition deadlock, C2 rendezvous that never fires.
68
- - \`logicprobe_concurrency_scan\` — mine concurrency claims (thread-safe, lock-free, race condition, mutex, ISR-safe) and route them to dedicated verification; logicprobe does not prove concurrency safety.
69
- - \`logicprobe_export\` — emit external-checker input from a verified model: UPPAAL, TLA+, PRISM, SPIN.
70
- - \`logicprobe_uml\` — model a code flow as UML (render), read a diagram back into a model (parse), or audit the modelling (review: UML001-UML019 structural defects, documentation gaps, diagram-versus-model round-trip fidelity).
71
-
72
- Python harnesses in the skill references remain the fallback for non-dsh hosts.
73
-
74
- **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).
75
- </EXTREMELY_IMPORTANT>`;
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: native tools — `logicprobe_verify` (state machines: S1-S8 + A1-A14, incl. A12 budget/worst-case path cost, A13 probabilistic reachability, A14 deadlines; `beforeModel` adds D1-D4 regression), `logicprobe_datamodel_verify` (data models and schema migrations: DS/DA, migration coverage, copy consistency, rollback symmetry, DD1-DD4), `logicprobe_compose_verify` (two or more machines: C1 composition deadlock, C2 rendezvous never fires), `logicprobe_concurrency_scan` (mine concurrency claims — it does not prove concurrency safety), `logicprobe_export` (UPPAAL, TLA+, PRISM, SPIN input from a verified model).',
159
- '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).',
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 — folds the claim-verification doctrine (1% Rule / Red Flags / proactive suggestion) into the first model step of every agent session and registers the logicprobe_verify tool.',
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
+ });