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,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,33 +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, 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.
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** (dsh) — six model-invocable tools:
90
-
91
- - \`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.
92
- - \`logicprobe_datamodel_verify\` — data models and schema migrations: DS/DA checks, migration coverage, copy consistency, rollback symmetry, DD1-DD4 data regression.
93
- - \`logicprobe_compose_verify\` — two or more machines checked together (rendezvous handshake semantics): C1 composition deadlock, C2 rendezvous that never fires.
94
- - \`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.
95
- - \`logicprobe_export\` — emit external-checker input from a verified model: UPPAAL, TLA+, PRISM, SPIN.
96
- - \`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).
97
-
98
- Python harnesses in the skill references remain the fallback for non-dsh hosts.
99
-
100
- **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).
101
- </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
+ `
102
114
 
103
115
  /** A schemastery field that may or may not carry `.volatile()`. */
104
116
  interface LiveField {
@@ -220,8 +232,8 @@ function resolveInteraction(config: Config, session: Session): 'ask' | 'auto' {
220
232
  function modeContextText(config: Config, session: Session): string {
221
233
  const interaction = resolveInteraction(config, session)
222
234
  const lines = [
223
- '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).',
224
- '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`.',
225
237
  interaction === 'auto'
226
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.'
227
239
  : 'logicprobe interaction=ask: show the extracted transition table and get user confirmation before running verification.',
@@ -245,11 +257,11 @@ interface SystemPromptLike {
245
257
  * lets the model read this plugin's runtime status without guessing. Mirrors
246
258
  * the registration pattern of the official dsh-tool-cordis host providers.
247
259
  */
248
- 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 {
249
261
  return {
250
262
  manifest: {
251
263
  id: 'logicprobe',
252
- 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.',
253
265
  methods: [
254
266
  {
255
267
  name: 'status',
@@ -272,10 +284,12 @@ function inspectProvider(config: Config, isToolRegistered: () => boolean, isData
272
284
  composeToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_compose_verify tool is registered on ctx.tools.' },
273
285
  exportToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_export tool is registered on ctx.tools.' },
274
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.' },
275
289
  engineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled state-machine verification engine accepts.' },
276
290
  dataEngineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled data-model verification engine accepts.' },
277
291
  },
278
- 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'],
279
293
  additionalProperties: false,
280
294
  },
281
295
  },
@@ -293,6 +307,8 @@ function inspectProvider(config: Config, isToolRegistered: () => boolean, isData
293
307
  composeToolRegistered: isComposeToolRegistered(),
294
308
  exportToolRegistered: isExportToolRegistered(),
295
309
  umlToolRegistered: isUmlToolRegistered(),
310
+ structureToolRegistered: isStructureToolRegistered(),
311
+ reportDiffToolRegistered: isReportDiffToolRegistered(),
296
312
  engineSchemaVersion: ENGINE_SCHEMA_VERSION,
297
313
  dataEngineSchemaVersion: DATA_ENGINE_SCHEMA_VERSION,
298
314
  }
@@ -313,13 +329,15 @@ export function apply(ctx: Context, config: Config): void {
313
329
  let composeToolRegistered = false
314
330
  let exportToolRegistered = false
315
331
  let umlToolRegistered = false
332
+ let structureToolRegistered = false
333
+ let reportDiffToolRegistered = false
316
334
  let modeContextRegistered = false
317
335
  const registerProvider = (): void => {
318
336
  if (providerRegistered) return
319
337
  const inspect = ctx.get('cordisInspect')
320
338
  if (inspect === undefined) return
321
339
  try {
322
- 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')
323
341
  providerRegistered = true
324
342
  } catch (err) {
325
343
  console.warn('[logicprobe] inspect provider registration failed', err)
@@ -336,12 +354,16 @@ export function apply(ctx: Context, config: Config): void {
336
354
  ctx.effect(() => tools.register(logicProbeComposeTool), 'logicprobe: compose tool')
337
355
  ctx.effect(() => tools.register(logicProbeExportTool), 'logicprobe: export tool')
338
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')
339
359
  toolRegistered = true
340
360
  dataToolRegistered = true
341
361
  concurrencyToolRegistered = true
342
362
  composeToolRegistered = true
343
363
  exportToolRegistered = true
344
364
  umlToolRegistered = true
365
+ structureToolRegistered = true
366
+ reportDiffToolRegistered = true
345
367
  } catch (err) {
346
368
  console.warn('[logicprobe] logicprobe_verify/logicprobe_datamodel_verify tool registration failed', err)
347
369
  }