dsh-logicprobe 0.8.1 → 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 (65) 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-tool.js +1 -1
  7. package/lib/concurrency.js +14 -0
  8. package/lib/data-engine.js +42 -8
  9. package/lib/engine.js +609 -118
  10. package/lib/export-tool.js +27 -2
  11. package/lib/granularity.js +237 -0
  12. package/lib/index.js +63 -32
  13. package/lib/report-diff-tool.js +39 -0
  14. package/lib/structure-tool.js +64 -0
  15. package/lib/structure.js +648 -0
  16. package/lib/tool.js +3 -3
  17. package/lib/types/baseline.d.ts +69 -0
  18. package/lib/types/concurrency.d.ts +10 -0
  19. package/lib/types/data-engine.d.ts +13 -1
  20. package/lib/types/engine.d.ts +139 -2
  21. package/lib/types/granularity.d.ts +106 -0
  22. package/lib/types/index.d.ts +9 -8
  23. package/lib/types/report-diff-tool.d.ts +8 -0
  24. package/lib/types/structure-tool.d.ts +10 -0
  25. package/lib/types/structure.d.ts +156 -0
  26. package/lib/types/tool.d.ts +2 -2
  27. package/lib/types/uml.d.ts +89 -0
  28. package/lib/uml-tool.js +36 -7
  29. package/lib/uml.js +217 -23
  30. package/locale/en.json +6 -0
  31. package/locale/zh.json +6 -0
  32. package/package.json +16 -14
  33. package/skills/logicprobe/SKILL.md +97 -58
  34. package/skills/logicprobe/references/dsh-model-schema.md +87 -15
  35. package/skills/logicprobe/references/gap-routing-guide.md +37 -36
  36. package/skills/logicprobe/references/hash-spec.md +128 -0
  37. package/skills/logicprobe/references/logic-verification-guide.md +29 -248
  38. package/skills/logicprobe-concurrency/SKILL.md +43 -0
  39. package/skills/{logicprobe → logicprobe-concurrency}/references/concurrency-risk-guide.md +1 -1
  40. package/skills/logicprobe-datamodel/SKILL.md +5 -4
  41. package/skills/logicprobe-datamodel/references/data-model-guide.md +1 -1
  42. package/skills/logicprobe-datamodel/references/data-model-schema.md +1 -1
  43. package/skills/logicprobe-structure/SKILL.md +95 -0
  44. package/skills/logicprobe-structure/references/structure-review-guide.md +177 -0
  45. package/skills/logicprobe-uml/SKILL.md +55 -0
  46. package/skills/{logicprobe → logicprobe-uml}/references/uml-modeling-guide.md +120 -8
  47. package/src/baseline.ts +180 -0
  48. package/src/client.js +56 -1
  49. package/src/compose-tool.ts +47 -47
  50. package/src/concurrency-tool.ts +2 -2
  51. package/src/concurrency.ts +23 -0
  52. package/src/data-engine.ts +51 -9
  53. package/src/engine.ts +656 -101
  54. package/src/export-tool.ts +73 -48
  55. package/src/granularity.ts +312 -0
  56. package/src/index.ts +63 -32
  57. package/src/report-diff-tool.ts +43 -0
  58. package/src/structure-tool.ts +68 -0
  59. package/src/structure.ts +724 -0
  60. package/src/tool.ts +4 -4
  61. package/src/uml-tool.ts +38 -8
  62. package/src/uml.ts +275 -24
  63. package/skills/logicprobe/references/logicprobe-engine.py +0 -4780
  64. package/skills/logicprobe/references/verification-harness.py +0 -959
  65. package/skills/logicprobe-datamodel/references/data-model-harness.py +0 -528
@@ -1,48 +1,73 @@
1
- import { defineTool } from '@deepseek-ai/dsh-tools'
2
- import type { JsonValue } from './json-value.js'
3
- import { exportModel, type ExportFormat } from './exporters.js'
4
-
5
- export const LOGICPROBE_EXPORT_TOOL_NAME = 'logicprobe_export'
6
-
7
- /**
8
- * DSH tool wrapping the external-tool exporters: turn a LogicModelV1 machine into
9
- * native input for the tool logicprobe routes to (UPPAAL / TLA+ / PRISM / SPIN).
10
- * v1 translates the core machine; unrepresentable invariants become warnings.
11
- */
12
- export const logicProbeExportTool = defineTool({
13
- name: LOGICPROBE_EXPORT_TOOL_NAME,
14
- description:
15
- 'Export a LogicModelV1 state machine into native input for an external verification tool (logicprobe routing). format is one of uppaal (.xta + queries), tla (TLC spec + safety property), prism (.pm + .pctl), spin (Promela + ltl). Booleans are exported as 0/1 integers; invariants the target cannot express are returned as warnings, never silently dropped. Returns { ok, format, primary, extras?, warnings? } or { ok: false, error } when the model is invalid or the PRISM enumeration is too large.',
16
- parameters: {
17
- model: {
18
- type: 'json',
19
- required: true,
20
- description: 'LogicModelV1 state-machine model to export.',
21
- },
22
- format: {
23
- type: 'string',
24
- required: true,
25
- enum: ['uppaal', 'tla', 'prism', 'spin'],
26
- description: 'Target tool format.',
27
- },
28
- },
29
- output: {
30
- schema: {
31
- type: 'json',
32
- description: 'Export result with generated file content.',
33
- },
34
- render(_args, value) {
35
- return [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }]
36
- },
37
- },
38
- timeoutMs: 10_000,
39
- isConcurrencySafe: () => true,
40
- async execute(args) {
41
- try {
42
- const result = exportModel(args.model, args.format as ExportFormat)
43
- return { ok: true, format: result.format, primary: result.primary, ...(result.extras === undefined ? {} : { extras: result.extras }), warnings: result.warnings } as unknown as JsonValue
44
- } catch (error) {
45
- return { ok: false, error: error instanceof Error ? error.message : String(error) } as unknown as JsonValue
46
- }
47
- },
48
- })
1
+ import { defineTool } from '@deepseek-ai/dsh-tools'
2
+ import type { JsonValue } from './json-value.js'
3
+ import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, modelHash, refusalVerdict, validateModel, verdictOf } from './engine.js'
4
+ import { exportModel, type ExportFormat } from './exporters.js'
5
+
6
+ /** The model hash of an export's input, when the input validates (the success path always does). */
7
+ function exportHashes(model: unknown): { hashSpec: typeof DEFAULT_HASH_SPEC; modelHash?: string } {
8
+ const validation = validateModel(model)
9
+ return validation.ok
10
+ ? { hashSpec: DEFAULT_HASH_SPEC, modelHash: modelHash(validation.model) }
11
+ : { hashSpec: DEFAULT_HASH_SPEC }
12
+ }
13
+
14
+ export const LOGICPROBE_EXPORT_TOOL_NAME = 'logicprobe_export'
15
+
16
+ /**
17
+ * DSH tool wrapping the external-tool exporters: turn a LogicModelV1 machine into
18
+ * native input for the tool logicprobe routes to (UPPAAL / TLA+ / PRISM / SPIN).
19
+ * v1 translates the core machine; unrepresentable invariants become warnings.
20
+ */
21
+ export const logicProbeExportTool = defineTool({
22
+ name: LOGICPROBE_EXPORT_TOOL_NAME,
23
+ description:
24
+ 'Export a LogicModelV1 state machine into native input for an external verification tool (logicprobe routing). format is one of uppaal (.xta + queries), tla (TLC spec + safety property), prism (.pm + .pctl), spin (Promela + ltl). Booleans are exported as 0/1 integers; invariants the target cannot express are returned as warnings, never silently dropped. Returns { ok, format, primary, extras?, warnings? } or { ok: false, error } when the model is invalid or the PRISM enumeration is too large.',
25
+ parameters: {
26
+ model: {
27
+ type: 'json',
28
+ required: true,
29
+ description: 'LogicModelV1 state-machine model to export.',
30
+ },
31
+ format: {
32
+ type: 'string',
33
+ required: true,
34
+ enum: ['uppaal', 'tla', 'prism', 'spin'],
35
+ description: 'Target tool format.',
36
+ },
37
+ },
38
+ output: {
39
+ schema: {
40
+ type: 'json',
41
+ description: 'Export result with generated file content.',
42
+ },
43
+ render(_args, value) {
44
+ return [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }]
45
+ },
46
+ },
47
+ timeoutMs: 10_000,
48
+ isConcurrencySafe: () => true,
49
+ async execute(args) {
50
+ try {
51
+ const result = exportModel(args.model, args.format as ExportFormat)
52
+ const warnings = result.warnings ?? []
53
+ const nextSteps: string[] = []
54
+ if (warnings.length > 0) 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.')
55
+ 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.')
56
+ return {
57
+ ok: true,
58
+ ran: true,
59
+ ...verdictOf(0, warnings.length),
60
+ schema: REPORT_SCHEMAS.export,
61
+ format: result.format,
62
+ primary: result.primary,
63
+ ...(result.extras === undefined ? {} : { extras: result.extras }),
64
+ hashes: exportHashes(args.model),
65
+ warnings,
66
+ nextSteps,
67
+ } as unknown as JsonValue
68
+ } catch (error) {
69
+ const message = error instanceof Error ? error.message : String(error)
70
+ return { ok: false, ran: false, ...refusalVerdict(message), schema: REPORT_SCHEMAS.export, errorCode: 'MODEL_INVALID', error: message } as unknown as JsonValue
71
+ }
72
+ },
73
+ })
@@ -0,0 +1,312 @@
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
+
22
+ import { createHash } from 'node:crypto'
23
+ import { REPORT_SCHEMAS, canonicalJson, verdictOfFindings, type Finding, type ReportSchema, type Verdict, type VerdictSummary } from './engine.js'
24
+ import { parseStructure, reviewStructure, type StructureGraph, type StructureNotation } from './structure.js'
25
+
26
+ export interface GranularityDiagram {
27
+ /** Name of this level, e.g. `L2/motion` or `repo`. Referenced by a child's `parent`. */
28
+ name: string
29
+ /** Diagram source text. */
30
+ diagram: string
31
+ /** Name of the diagram this one refines; absent for a root. */
32
+ parent?: string
33
+ }
34
+
35
+ export interface GranularityOptions {
36
+ diagrams: GranularityDiagram[]
37
+ notation?: 'auto' | StructureNotation
38
+ /** Optional dependency matrix, applied to every diagram. */
39
+ matrix?: unknown
40
+ }
41
+
42
+ export interface GranularityEdgeRef {
43
+ from: string
44
+ to: string
45
+ line?: number
46
+ }
47
+
48
+ export interface GranularityPairSummary {
49
+ parent: string
50
+ child: string
51
+ parentNodes: number
52
+ childNodes: number
53
+ parentEdges: number
54
+ childEdges: number
55
+ /** Child edges whose endpoints both exist in the parent and which the parent also draws. */
56
+ inheritedEdges: number
57
+ /** Child edges with at least one endpoint the parent does not have (the refinement's own detail). */
58
+ newEdges: number
59
+ /** Parent edges with no direct child counterpart, but a child path connects the same ends. */
60
+ expandedEdges: GranularityEdgeRef[]
61
+ /** Parent edges with no direct counterpart and no connecting path — the refinement lost them. */
62
+ missingEdges: GranularityEdgeRef[]
63
+ /** Child edges between two parent nodes that the parent does not draw — invented dependencies. */
64
+ inventedEdges: GranularityEdgeRef[]
65
+ }
66
+
67
+ export interface GranularityDiagramSummary {
68
+ name: string
69
+ parent?: string
70
+ notation: StructureNotation
71
+ nodes: number
72
+ edges: number
73
+ verdict: Verdict
74
+ findings: number
75
+ }
76
+
77
+ export interface GranularityReport {
78
+ ok: boolean
79
+ ran: boolean
80
+ verdict: Verdict
81
+ verdictReason: string
82
+ schema: ReportSchema
83
+ diagrams: GranularityDiagramSummary[]
84
+ pairs: GranularityPairSummary[]
85
+ findings: Finding[]
86
+ summary: {
87
+ diagrams: number
88
+ roots: number
89
+ pairs: number
90
+ nodes: number
91
+ edges: number
92
+ inventedEdges: number
93
+ missingEdges: number
94
+ expandedEdges: number
95
+ errors: number
96
+ warnings: number
97
+ }
98
+ hashes: { diagrams: Array<{ name: string; hash: string }> }
99
+ warnings: string[]
100
+ nextSteps: string[]
101
+ }
102
+
103
+ /** Reachability in one diagram, ignoring edge direction? No: a dependency path is directed. */
104
+ function reaches(graph: StructureGraph, from: string, to: string): boolean {
105
+ const adjacency = new Map<string, string[]>()
106
+ for (const node of graph.nodes) adjacency.set(node.id, [])
107
+ for (const edge of graph.edges) adjacency.get(edge.from)?.push(edge.to)
108
+ const seen = new Set<string>([from])
109
+ const queue: string[] = [from]
110
+ while (queue.length > 0) {
111
+ const current = queue.shift() as string
112
+ for (const next of adjacency.get(current) ?? []) {
113
+ if (next === to) return true
114
+ if (seen.has(next)) continue
115
+ seen.add(next)
116
+ queue.push(next)
117
+ }
118
+ }
119
+ return false
120
+ }
121
+
122
+ /** The parent relation must be a forest: report the first cycle it contains, if any. */
123
+ function parentCycle(diagrams: GranularityDiagram[]): string[] | undefined {
124
+ const byName = new Map(diagrams.map((entry) => [entry.name, entry]))
125
+ for (const start of diagrams) {
126
+ const path: string[] = [start.name]
127
+ const seen = new Set<string>([start.name])
128
+ let cursor = start.parent
129
+ while (cursor !== undefined) {
130
+ if (seen.has(cursor)) {
131
+ path.push(cursor)
132
+ return path
133
+ }
134
+ seen.add(cursor)
135
+ path.push(cursor)
136
+ cursor = byName.get(cursor)?.parent
137
+ }
138
+ }
139
+ return undefined
140
+ }
141
+
142
+ /**
143
+ * Review a set of structure diagrams with declared parent relations. Per-diagram
144
+ * structural checks run first; the cross-diagram refinement checks follow.
145
+ */
146
+ export function reviewGranularity(options: GranularityOptions): GranularityReport {
147
+ const diagrams = options.diagrams
148
+ const findings: Finding[] = []
149
+ const warnings: string[] = []
150
+ const diagramSummaries: GranularityDiagramSummary[] = []
151
+ const graphs = new Map<string, StructureGraph>()
152
+
153
+ if (diagrams.length === 0) {
154
+ findings.push({ code: 'UML031_NO_DIAGRAMS', severity: 'error', message: 'no diagram was supplied, so there are no granularity levels to compare.' })
155
+ }
156
+
157
+ const cycle = parentCycle(diagrams)
158
+ if (cycle !== undefined) {
159
+ findings.push({
160
+ code: 'UML030_PARENT_CYCLE',
161
+ severity: 'error',
162
+ 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.',
163
+ evidence: { cycle },
164
+ })
165
+ }
166
+
167
+ const knownNames = new Set(diagrams.map((entry) => entry.name))
168
+ for (const entry of diagrams) {
169
+ if (entry.parent !== undefined && !knownNames.has(entry.parent)) {
170
+ findings.push({
171
+ code: 'UML028_UNKNOWN_PARENT',
172
+ severity: 'error',
173
+ message: 'diagram "' + entry.name + '" declares parent "' + entry.parent + '", which is not part of this set.',
174
+ file: entry.name,
175
+ evidence: { diagram: entry.name, parent: entry.parent, known: [...knownNames].sort() },
176
+ })
177
+ }
178
+ const report = reviewStructure({
179
+ diagram: entry.diagram,
180
+ notation: options.notation ?? 'auto',
181
+ ...(options.matrix === undefined ? {} : { matrix: options.matrix }),
182
+ })
183
+ graphs.set(entry.name, report.graph)
184
+ // Tag every finding with the diagram it came from: with several diagrams in one
185
+ // report, an untagged finding is unactionable.
186
+ for (const finding of report.findings) {
187
+ findings.push({ ...finding, file: entry.name, ...(finding.evidence === undefined ? {} : {}) })
188
+ }
189
+ warnings.push(...report.warnings.map((warning) => entry.name + ': ' + warning))
190
+ diagramSummaries.push({
191
+ name: entry.name,
192
+ ...(entry.parent === undefined ? {} : { parent: entry.parent }),
193
+ notation: report.notation,
194
+ nodes: report.summary.nodes,
195
+ edges: report.summary.edges,
196
+ verdict: report.verdict,
197
+ findings: report.findings.length,
198
+ })
199
+ }
200
+
201
+ const pairs: GranularityPairSummary[] = []
202
+ for (const child of diagrams) {
203
+ if (child.parent === undefined) continue
204
+ const parent = diagrams.find((entry) => entry.name === child.parent)
205
+ const parentGraph = graphs.get(child.parent)
206
+ const childGraph = graphs.get(child.name)
207
+ if (parent === undefined || parentGraph === undefined || childGraph === undefined) continue
208
+ const parentNodes = new Set(parentGraph.nodes.map((node) => node.id))
209
+ const childNodes = new Set(childGraph.nodes.map((node) => node.id))
210
+ const parentEdges = new Set(parentGraph.edges.map((edge) => edge.from + '\u0000' + edge.to))
211
+ const childEdges = new Set(childGraph.edges.map((edge) => edge.from + '\u0000' + edge.to))
212
+
213
+ let inheritedEdges = 0
214
+ let newEdges = 0
215
+ const inventedEdges: GranularityEdgeRef[] = []
216
+ for (const edge of childGraph.edges) {
217
+ const hasFrom = parentNodes.has(edge.from)
218
+ const hasTo = parentNodes.has(edge.to)
219
+ if (!hasFrom || !hasTo) { newEdges += 1; continue }
220
+ if (parentEdges.has(edge.from + '\u0000' + edge.to)) { inheritedEdges += 1; continue }
221
+ inventedEdges.push({ from: edge.from, to: edge.to, line: edge.line })
222
+ }
223
+
224
+ const expandedEdges: GranularityEdgeRef[] = []
225
+ const missingEdges: GranularityEdgeRef[] = []
226
+ for (const edge of parentGraph.edges) {
227
+ const key = edge.from + '\u0000' + edge.to
228
+ if (childEdges.has(key)) continue
229
+ // The child only has to answer for a parent edge whose ends it also carries; a child
230
+ // may legitimately cover a subtree of the parent.
231
+ if (!childNodes.has(edge.from) || !childNodes.has(edge.to)) continue
232
+ if (reaches(childGraph, edge.from, edge.to)) expandedEdges.push({ from: edge.from, to: edge.to, line: edge.line })
233
+ else missingEdges.push({ from: edge.from, to: edge.to, line: edge.line })
234
+ }
235
+
236
+ if (inventedEdges.length > 0) {
237
+ findings.push({
238
+ code: 'UML029_REFINEMENT_VIOLATION',
239
+ severity: 'error',
240
+ 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.',
241
+ file: child.name,
242
+ evidence: { kind: 'invented-edge', parent: parent.name, edges: inventedEdges },
243
+ })
244
+ }
245
+ if (missingEdges.length > 0) {
246
+ findings.push({
247
+ code: 'UML029_REFINEMENT_VIOLATION',
248
+ severity: 'error',
249
+ 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(', ') + '.',
250
+ file: child.name,
251
+ evidence: { kind: 'unexpanded-edge', parent: parent.name, edges: missingEdges },
252
+ })
253
+ }
254
+ pairs.push({
255
+ parent: parent.name,
256
+ child: child.name,
257
+ parentNodes: parentNodes.size,
258
+ childNodes: childNodes.size,
259
+ parentEdges: parentGraph.edges.length,
260
+ childEdges: childGraph.edges.length,
261
+ inheritedEdges,
262
+ newEdges,
263
+ expandedEdges,
264
+ missingEdges,
265
+ inventedEdges,
266
+ })
267
+ }
268
+
269
+ const errors = findings.filter((finding) => finding.severity === 'error').length
270
+ const warningCount = findings.filter((finding) => finding.severity === 'warning').length
271
+ const invented = pairs.reduce((sum, pair) => sum + pair.inventedEdges.length, 0)
272
+ const missing = pairs.reduce((sum, pair) => sum + pair.missingEdges.length, 0)
273
+ const expanded = pairs.reduce((sum, pair) => sum + pair.expandedEdges.length, 0)
274
+
275
+ const nextSteps: string[] = []
276
+ if (errors > 0) 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.')
277
+ if (pairs.length === 0 && diagrams.length > 0) nextSteps.push('Give the levels a `parent` so the refinement can be checked; a set of unrelated diagrams has no consistency property to verify.')
278
+ if (expanded > 0) 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.')
279
+ 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.')
280
+
281
+ return {
282
+ ok: true,
283
+ ran: true,
284
+ ...verdictOfFindings(findings),
285
+ schema: REPORT_SCHEMAS.granularity,
286
+ diagrams: diagramSummaries,
287
+ pairs,
288
+ findings,
289
+ summary: {
290
+ diagrams: diagrams.length,
291
+ roots: diagrams.filter((entry) => entry.parent === undefined).length,
292
+ pairs: pairs.length,
293
+ nodes: diagrams.reduce((sum, entry) => sum + (graphs.get(entry.name)?.nodes.length ?? 0), 0),
294
+ edges: diagrams.reduce((sum, entry) => sum + (graphs.get(entry.name)?.edges.length ?? 0), 0),
295
+ inventedEdges: invented,
296
+ missingEdges: missing,
297
+ expandedEdges: expanded,
298
+ errors,
299
+ warnings: warningCount,
300
+ },
301
+ hashes: { diagrams: diagrams.map((entry) => ({ name: entry.name, hash: createHash('sha256').update(entry.diagram).digest('hex') })) },
302
+ warnings,
303
+ nextSteps,
304
+ }
305
+ }
306
+
307
+ /** Deterministic JSON of a granularity report's pair table, for a stable summary line. */
308
+ export function granularityDigest(pairs: GranularityPairSummary[]): string {
309
+ return canonicalJson(pairs)
310
+ }
311
+
312
+ export type { VerdictSummary }
package/src/index.ts 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
  */
@@ -40,6 +41,8 @@ import { logicProbeConcurrencyScanTool } from './concurrency-tool.js'
40
41
  import { logicProbeComposeTool } from './compose-tool.js'
41
42
  import { logicProbeExportTool } from './export-tool.js'
42
43
  import { logicProbeUmlTool } from './uml-tool.js'
44
+ import { logicProbeStructureTool } from './structure-tool.js'
45
+ import { logicProbeReportDiffTool } from './report-diff-tool.js'
43
46
  import { ENGINE_SCHEMA_VERSION } from './engine.js'
44
47
 
45
48
  // DSH 0.1.7-alpha.1 (session format v4) retires the shared
@@ -72,24 +75,42 @@ const GATE_SOURCE_KIND: 'plugin:logicprobe' = 'plugin:logicprobe'
72
75
 
73
76
  export type InteractionMode = 'ask' | 'auto' | 'follow-approval'
74
77
 
75
- const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
76
- Plugin logicprobe is active. Documents are not truth — code is. Verify every verifiable claim before accepting or acting on any design.
77
-
78
- **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, or behavioral guarantees ("always"/"never"/"guaranteed") — load it with the skill tool before responding. The cost of loading is trivial compared to the cost of a false claim.
79
-
80
- **Red Flags** — if you think any of these, STOP. You are rationalizing:
81
-
82
- | You think | Reality |
83
- |-----------|---------|
84
- | "This plan is too simple to verify" | The skill auto-classifies depth (LIGHTWEIGHT / STANDARD / ESCALATED). You don't decide. |
85
- | "I already know the file paths are correct" | Organic verification leaves no audit trail. Run Phase 0, append the "## Plan Verification" block. |
86
- | "I'll verify while implementing" | Verification happens before implementation, not during. |
87
- | "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
88
-
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.
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).
92
- </EXTREMELY_IMPORTANT>`
78
+ const DEFAULT_GATE_CONTENT = `
79
+ <EXTREMELY_IMPORTANT>
80
+ Plugin logicprobe is active. It checks claims about designs and code: facts against the source, behaviour as an executable model.
81
+
82
+ **1% Rule**: if any claim about this code deserves a check, load the matching skill first. Loading is cheap. A false claim is not.
83
+
84
+ **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.
85
+
86
+ **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).
87
+
88
+ **Tools** (dsh):
89
+
90
+ - \`logicprobe_verify\` — state machines: structural checks, adversarial probes, budgets, deadlines, before/after regression.
91
+ - \`logicprobe_datamodel_verify\` — data models and migrations: structure, migration coverage, copy consistency, rollback, before/after data.
92
+ - \`logicprobe_compose_verify\` — two or more machines: deadlock, and a rendezvous that never fires.
93
+ - \`logicprobe_concurrency_scan\` — finds concurrency claims and routes them. It proves nothing about concurrency.
94
+ - \`logicprobe_export\` — UPPAAL, TLA+, PRISM and SPIN input from a verified model.
95
+ - \`logicprobe_uml\` — render, parse or review a UML diagram.
96
+ - \`logicprobe_structure_verify\` — dependency review of a structure diagram, at one level or several.
97
+ - \`logicprobe_report_diff\` — baseline against current: what a change added, removed or changed.
98
+
99
+ **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.
100
+
101
+ **Red Flags** — if you think any of these, stop:
102
+
103
+ | You think | Reality |
104
+ |-----------|---------|
105
+ | "Too simple to check" | The check is cheap. The silent skip is not. |
106
+ | "I read the code, so I know" | Knowing is not evidence. Cite \`file:line\`, or run the model, or call the claim unverified. |
107
+ | "The spec or the diagram says so" | Both are claims about the code. Reconcile them with a source-side scan. |
108
+ | "I'll check while implementing" | Check before the decision, not after. |
109
+ | "Reasoning is enough" | Reasoning is not a run. One counter-example kills a universal claim. |
110
+ | "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. |
111
+
112
+ </EXTREMELY_IMPORTANT>
113
+ `
93
114
 
94
115
  /** A schemastery field that may or may not carry `.volatile()`. */
95
116
  interface LiveField {
@@ -211,8 +232,8 @@ function resolveInteraction(config: Config, session: Session): 'ask' | 'auto' {
211
232
  function modeContextText(config: Config, session: Session): string {
212
233
  const interaction = resolveInteraction(config, session)
213
234
  const lines = [
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).',
235
+ '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).',
236
+ 'logicprobe: gate on `verdict`, never on `ok`. Reports carry `verdictReason`, `hashes` and `nextSteps`.',
216
237
  interaction === 'auto'
217
238
  ? '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.'
218
239
  : 'logicprobe interaction=ask: show the extracted transition table and get user confirmation before running verification.',
@@ -236,11 +257,11 @@ interface SystemPromptLike {
236
257
  * lets the model read this plugin's runtime status without guessing. Mirrors
237
258
  * the registration pattern of the official dsh-tool-cordis host providers.
238
259
  */
239
- function inspectProvider(config: Config, isToolRegistered: () => boolean, isDataToolRegistered: () => boolean, isConcurrencyToolRegistered: () => boolean, isComposeToolRegistered: () => boolean, isExportToolRegistered: () => boolean, isUmlToolRegistered: () => boolean): HostCordisInspectProviderRegistration {
260
+ function inspectProvider(config: Config, isToolRegistered: () => boolean, isDataToolRegistered: () => boolean, isConcurrencyToolRegistered: () => boolean, isComposeToolRegistered: () => boolean, isExportToolRegistered: () => boolean, isUmlToolRegistered: () => boolean, isStructureToolRegistered: () => boolean, isReportDiffToolRegistered: () => boolean): HostCordisInspectProviderRegistration {
240
261
  return {
241
262
  manifest: {
242
263
  id: 'logicprobe',
243
- 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.',
264
+ 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.',
244
265
  methods: [
245
266
  {
246
267
  name: 'status',
@@ -263,10 +284,12 @@ function inspectProvider(config: Config, isToolRegistered: () => boolean, isData
263
284
  composeToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_compose_verify tool is registered on ctx.tools.' },
264
285
  exportToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_export tool is registered on ctx.tools.' },
265
286
  umlToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_uml tool (UML modelling + modelling review) is registered on ctx.tools.' },
287
+ structureToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_structure_verify tool (structure-diagram / dependency review) is registered on ctx.tools.' },
288
+ reportDiffToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_report_diff tool (baseline report comparison) is registered on ctx.tools.' },
266
289
  engineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled state-machine verification engine accepts.' },
267
290
  dataEngineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled data-model verification engine accepts.' },
268
291
  },
269
- required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'composeToolRegistered', 'exportToolRegistered', 'umlToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
292
+ required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'composeToolRegistered', 'exportToolRegistered', 'umlToolRegistered', 'structureToolRegistered', 'reportDiffToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
270
293
  additionalProperties: false,
271
294
  },
272
295
  },
@@ -284,6 +307,8 @@ function inspectProvider(config: Config, isToolRegistered: () => boolean, isData
284
307
  composeToolRegistered: isComposeToolRegistered(),
285
308
  exportToolRegistered: isExportToolRegistered(),
286
309
  umlToolRegistered: isUmlToolRegistered(),
310
+ structureToolRegistered: isStructureToolRegistered(),
311
+ reportDiffToolRegistered: isReportDiffToolRegistered(),
287
312
  engineSchemaVersion: ENGINE_SCHEMA_VERSION,
288
313
  dataEngineSchemaVersion: DATA_ENGINE_SCHEMA_VERSION,
289
314
  }
@@ -304,13 +329,15 @@ export function apply(ctx: Context, config: Config): void {
304
329
  let composeToolRegistered = false
305
330
  let exportToolRegistered = false
306
331
  let umlToolRegistered = false
332
+ let structureToolRegistered = false
333
+ let reportDiffToolRegistered = false
307
334
  let modeContextRegistered = false
308
335
  const registerProvider = (): void => {
309
336
  if (providerRegistered) return
310
337
  const inspect = ctx.get('cordisInspect')
311
338
  if (inspect === undefined) return
312
339
  try {
313
- ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered, () => composeToolRegistered, () => exportToolRegistered, () => umlToolRegistered)), 'logicprobe: inspect provider')
340
+ ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered, () => composeToolRegistered, () => exportToolRegistered, () => umlToolRegistered, () => structureToolRegistered, () => reportDiffToolRegistered)), 'logicprobe: inspect provider')
314
341
  providerRegistered = true
315
342
  } catch (err) {
316
343
  console.warn('[logicprobe] inspect provider registration failed', err)
@@ -327,12 +354,16 @@ export function apply(ctx: Context, config: Config): void {
327
354
  ctx.effect(() => tools.register(logicProbeComposeTool), 'logicprobe: compose tool')
328
355
  ctx.effect(() => tools.register(logicProbeExportTool), 'logicprobe: export tool')
329
356
  ctx.effect(() => tools.register(logicProbeUmlTool), 'logicprobe: uml tool')
357
+ ctx.effect(() => tools.register(logicProbeStructureTool), 'logicprobe: structure tool')
358
+ ctx.effect(() => tools.register(logicProbeReportDiffTool), 'logicprobe: report diff tool')
330
359
  toolRegistered = true
331
360
  dataToolRegistered = true
332
361
  concurrencyToolRegistered = true
333
362
  composeToolRegistered = true
334
363
  exportToolRegistered = true
335
364
  umlToolRegistered = true
365
+ structureToolRegistered = true
366
+ reportDiffToolRegistered = true
336
367
  } catch (err) {
337
368
  console.warn('[logicprobe] logicprobe_verify/logicprobe_datamodel_verify tool registration failed', err)
338
369
  }