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.
- package/README.en-US.md +79 -14
- package/README.md +79 -14
- package/lib/baseline.js +142 -0
- package/lib/client.js +56 -1
- package/lib/compose-tool.js +1 -1
- package/lib/concurrency-tool.js +1 -1
- package/lib/concurrency.js +14 -0
- package/lib/data-engine.js +42 -8
- package/lib/engine.js +609 -118
- package/lib/export-tool.js +27 -2
- package/lib/granularity.js +237 -0
- package/lib/index.js +63 -32
- package/lib/report-diff-tool.js +39 -0
- package/lib/structure-tool.js +64 -0
- package/lib/structure.js +648 -0
- package/lib/tool.js +3 -3
- package/lib/types/baseline.d.ts +69 -0
- package/lib/types/concurrency.d.ts +10 -0
- package/lib/types/data-engine.d.ts +13 -1
- package/lib/types/engine.d.ts +139 -2
- package/lib/types/granularity.d.ts +106 -0
- package/lib/types/index.d.ts +9 -8
- package/lib/types/report-diff-tool.d.ts +8 -0
- package/lib/types/structure-tool.d.ts +10 -0
- package/lib/types/structure.d.ts +156 -0
- package/lib/types/tool.d.ts +2 -2
- package/lib/types/uml.d.ts +89 -0
- package/lib/uml-tool.js +36 -7
- package/lib/uml.js +217 -23
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +16 -14
- package/skills/logicprobe/SKILL.md +97 -58
- package/skills/logicprobe/references/dsh-model-schema.md +87 -15
- package/skills/logicprobe/references/gap-routing-guide.md +37 -36
- package/skills/logicprobe/references/hash-spec.md +128 -0
- package/skills/logicprobe/references/logic-verification-guide.md +29 -248
- package/skills/logicprobe-concurrency/SKILL.md +43 -0
- package/skills/{logicprobe → logicprobe-concurrency}/references/concurrency-risk-guide.md +1 -1
- package/skills/logicprobe-datamodel/SKILL.md +5 -4
- package/skills/logicprobe-datamodel/references/data-model-guide.md +1 -1
- package/skills/logicprobe-datamodel/references/data-model-schema.md +1 -1
- package/skills/logicprobe-structure/SKILL.md +95 -0
- package/skills/logicprobe-structure/references/structure-review-guide.md +177 -0
- package/skills/logicprobe-uml/SKILL.md +55 -0
- package/skills/{logicprobe → logicprobe-uml}/references/uml-modeling-guide.md +120 -8
- package/src/baseline.ts +180 -0
- package/src/client.js +56 -1
- package/src/compose-tool.ts +47 -47
- package/src/concurrency-tool.ts +2 -2
- package/src/concurrency.ts +23 -0
- package/src/data-engine.ts +51 -9
- package/src/engine.ts +656 -101
- package/src/export-tool.ts +73 -48
- package/src/granularity.ts +312 -0
- package/src/index.ts +63 -32
- package/src/report-diff-tool.ts +43 -0
- package/src/structure-tool.ts +68 -0
- package/src/structure.ts +724 -0
- package/src/tool.ts +4 -4
- package/src/uml-tool.ts +38 -8
- package/src/uml.ts +275 -24
- package/skills/logicprobe/references/logicprobe-engine.py +0 -4780
- package/skills/logicprobe/references/verification-harness.py +0 -959
- package/skills/logicprobe-datamodel/references/data-model-harness.py +0 -528
package/src/export-tool.ts
CHANGED
|
@@ -1,48 +1,73 @@
|
|
|
1
|
-
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
|
-
import type { JsonValue } from './json-value.js'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 (
|
|
4
|
-
* Red Flags,
|
|
5
|
-
*
|
|
3
|
+
* Injects the session-start gate text (what this plugin checks, the 1% Rule,
|
|
4
|
+
* the Red Flags, the routing to the skills, the tool list, how to read a report)
|
|
5
|
+
* into the first model step of every agent session, mirroring the SessionStart
|
|
6
|
+
* hook the Claude Code plugin installs.
|
|
6
7
|
* The skill ships in this package's `skills/` directory and is registered at
|
|
7
8
|
* apply time into dsh's `ctx.skills` registry through the standard filesystem
|
|
8
9
|
* provider, so it appears in every session catalog without a manual copy step.
|
|
@@ -16,11 +17,11 @@
|
|
|
16
17
|
* reminders (skill catalog, AGENTS.md, gate plugins) simply defer this message
|
|
17
18
|
* to the first step after their promotion, and the history guard re-injects it
|
|
18
19
|
* there. The default gate text is the dsh-native adaptation of
|
|
19
|
-
* `hooks/session-start-content.md
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* override via Config.
|
|
20
|
+
* `hooks/session-start-content.md`. Both are a guide to calling the tools: what
|
|
21
|
+
* this plugin checks, the 1% Rule and the Red Flags that raise the chance of a
|
|
22
|
+
* call, the routing to the five skills, the tool list, and how to read a report.
|
|
23
|
+
* They stay in sync. Method, doctrine and per-domain detail live in the skills
|
|
24
|
+
* and their references, not here. Deployments override via Config.
|
|
24
25
|
*
|
|
25
26
|
* @module logicprobe-dsh
|
|
26
27
|
*/
|
|
@@ -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 =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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:
|
|
215
|
-
'logicprobe:
|
|
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
|
|
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
|
}
|