dsh-model-everything 0.2.0 → 0.3.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 +81 -73
- package/README.md +67 -68
- package/lib/automaton-tool.js +69 -0
- package/lib/automaton.js +327 -0
- package/lib/compose-tool.js +6 -1
- package/lib/concurrency-tool.js +6 -1
- package/lib/data-tool.js +6 -1
- package/lib/design-tool.js +7 -2
- package/lib/edit-tool.js +111 -0
- package/lib/editor.js +293 -0
- package/lib/engine.js +7 -0
- package/lib/export-tool.js +6 -1
- package/lib/index.js +4 -0
- package/lib/presentation.js +81 -0
- package/lib/report-diff-tool.js +6 -1
- package/lib/report-view.js +62 -10
- package/lib/routing.js +85 -0
- package/lib/scan.js +547 -0
- package/lib/structure-tool.js +34 -1
- package/lib/structure.js +28 -3
- package/lib/tool.js +6 -0
- package/lib/types/automaton-tool.d.ts +2 -0
- package/lib/types/automaton.d.ts +57 -0
- package/lib/types/edit-tool.d.ts +2 -0
- package/lib/types/editor.d.ts +46 -0
- package/lib/types/engine.d.ts +1 -0
- package/lib/types/presentation.d.ts +59 -0
- package/lib/types/routing.d.ts +31 -0
- package/lib/types/scan.d.ts +37 -0
- package/lib/types/structure.d.ts +16 -0
- package/lib/uml-tool.js +16 -6
- package/package.json +4 -3
- package/skills/model-everything/SKILL.md +6 -2
- package/skills/model-everything/references/automata.md +102 -0
- package/src/automaton-tool.ts +73 -0
- package/src/automaton.ts +375 -0
- package/src/compose-tool.ts +12 -6
- package/src/concurrency-tool.ts +7 -1
- package/src/data-tool.ts +7 -1
- package/src/design-tool.ts +74 -68
- package/src/edit-tool.ts +119 -0
- package/src/editor.ts +297 -0
- package/src/engine.ts +7 -0
- package/src/export-tool.ts +87 -81
- package/src/index.ts +4 -0
- package/src/presentation.ts +125 -0
- package/src/report-diff-tool.ts +7 -1
- package/src/report-view.ts +53 -2
- package/src/routing.ts +96 -0
- package/src/scan.ts +564 -0
- package/src/structure-tool.ts +33 -1
- package/src/structure.ts +37 -3
- package/src/tool.ts +6 -0
- package/src/uml-tool.ts +240 -230
package/src/export-tool.ts
CHANGED
|
@@ -1,81 +1,87 @@
|
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
warnings,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 { reportCard, reportResultView } from './presentation.js'
|
|
5
|
+
import { ExportError, exportModel, type ExportFormat } from './exporters.js'
|
|
6
|
+
import { renderReportText } from './report-view.js'
|
|
7
|
+
|
|
8
|
+
/** The model hash of an export's input, when the input validates (the success path always does). */
|
|
9
|
+
function exportHashes(model: unknown): { hashSpec: typeof DEFAULT_HASH_SPEC; modelHash?: string } {
|
|
10
|
+
const validation = validateModel(model)
|
|
11
|
+
return validation.ok
|
|
12
|
+
? { hashSpec: DEFAULT_HASH_SPEC, modelHash: modelHash(validation.model) }
|
|
13
|
+
: { hashSpec: DEFAULT_HASH_SPEC }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const EXPORT_TOOL_NAME = 'model_export'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* DSH tool wrapping the external-tool exporters: turn a LogicModelV1 machine into
|
|
20
|
+
* native input for the tool this plugin routes to (UPPAAL / TLA+ / PRISM / SPIN).
|
|
21
|
+
* v1 translates the core machine; unrepresentable invariants become warnings.
|
|
22
|
+
*/
|
|
23
|
+
export const modelExportTool = defineTool({
|
|
24
|
+
name: EXPORT_TOOL_NAME,
|
|
25
|
+
description:
|
|
26
|
+
'Translate a LogicModelV1 into the input language of an external checker - UPPAAL, TLA+, PRISM or SPIN - for a property this plugin does not decide itself (symbolic model checking, unbounded induction, expected reward). format is one of uppaal (.xta + queries), tla (TLC spec + safety property), prism (.pm + .pctl), spin (Promela + ltl). This tool does not verify anything: for the checks it can decide use `model_verify`. 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.',
|
|
27
|
+
parameters: {
|
|
28
|
+
model: {
|
|
29
|
+
type: 'json',
|
|
30
|
+
required: true,
|
|
31
|
+
description: 'LogicModelV1 state-machine model to export.',
|
|
32
|
+
},
|
|
33
|
+
format: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
required: true,
|
|
36
|
+
enum: ['uppaal', 'tla', 'prism', 'spin'],
|
|
37
|
+
description: 'Target tool format.',
|
|
38
|
+
},
|
|
39
|
+
reportView: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
enum: ['digest', 'full'],
|
|
42
|
+
description: 'digest (default) prints the generated file verbatim; full prints the structured result as JSON.',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
output: {
|
|
46
|
+
schema: {
|
|
47
|
+
type: 'json',
|
|
48
|
+
description: 'Export result with generated file content.',
|
|
49
|
+
},
|
|
50
|
+
render(args, value) {
|
|
51
|
+
return [{ type: 'text' as const, text: renderReportText(value, { full: args.reportView === 'full' }) }]
|
|
52
|
+
},
|
|
53
|
+
// The card a UI shows, separated from the text the model reads.
|
|
54
|
+
presentationMeta(_args, value) {
|
|
55
|
+
return reportCard(value, EXPORT_TOOL_NAME) as unknown as JsonValue
|
|
56
|
+
},
|
|
57
|
+
}, presentResult: reportResultView(EXPORT_TOOL_NAME),
|
|
58
|
+
|
|
59
|
+
timeoutMs: 10_000,
|
|
60
|
+
isConcurrencySafe: () => true,
|
|
61
|
+
async execute(args) {
|
|
62
|
+
try {
|
|
63
|
+
const result = exportModel(args.model, args.format as ExportFormat)
|
|
64
|
+
const warnings = result.warnings ?? []
|
|
65
|
+
const nextSteps: string[] = []
|
|
66
|
+
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.')
|
|
67
|
+
nextSteps.push('Run the exported file through its own tool (verify/compile) before trusting the translation; this plugin guarantees the encoding, not the target checker\'s verdict.')
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
ran: true,
|
|
71
|
+
...verdictOf(0, warnings.length),
|
|
72
|
+
schema: REPORT_SCHEMAS.export,
|
|
73
|
+
format: result.format,
|
|
74
|
+
primary: result.primary,
|
|
75
|
+
...(result.extras === undefined ? {} : { extras: result.extras }),
|
|
76
|
+
hashes: exportHashes(args.model),
|
|
77
|
+
warnings,
|
|
78
|
+
nextSteps,
|
|
79
|
+
} as unknown as JsonValue
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
82
|
+
// The exporter's own refusals carry their code: a limit of the encoding is not a defect in the model.
|
|
83
|
+
const errorCode = error instanceof ExportError ? error.code : 'MODEL_INVALID'
|
|
84
|
+
return { ok: false, ran: false, ...refusalVerdict(message), schema: REPORT_SCHEMAS.export, errorCode, error: message } as unknown as JsonValue
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,8 @@ import { umlRenderTool, umlParseTool, umlReviewTool, umlExplainLabelsTool } from
|
|
|
39
39
|
import { umlStructureTool } from './structure-tool.js'
|
|
40
40
|
import { designTool } from './design-tool.js'
|
|
41
41
|
import { reportDiffTool } from './report-diff-tool.js'
|
|
42
|
+
import { modelEditTool } from './edit-tool.js'
|
|
43
|
+
import { modelAutomatonTool } from './automaton-tool.js'
|
|
42
44
|
import { ENGINE_SCHEMA_VERSION } from './engine.js'
|
|
43
45
|
|
|
44
46
|
// DSH 0.1.7-alpha.1 (session format v4) retires the shared
|
|
@@ -245,6 +247,8 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
245
247
|
ctx.effect(() => tools.register(umlStructureTool), 'dsh-model-everything: structure tool')
|
|
246
248
|
ctx.effect(() => tools.register(designTool), 'dsh-model-everything: design review tool')
|
|
247
249
|
ctx.effect(() => tools.register(reportDiffTool), 'dsh-model-everything: report diff tool')
|
|
250
|
+
ctx.effect(() => tools.register(modelEditTool), 'dsh-model-everything: model edit tool')
|
|
251
|
+
ctx.effect(() => tools.register(modelAutomatonTool), 'dsh-model-everything: automaton tool')
|
|
248
252
|
toolRegistered = true
|
|
249
253
|
dataToolRegistered = true
|
|
250
254
|
concurrencyToolRegistered = true
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a UI shows for a report, separated from what the model reads.
|
|
3
|
+
*
|
|
4
|
+
* A report is written for a model: it names every check, every finding and the next step, because the
|
|
5
|
+
* model has to act on all of it. A reader wants four things instead - did it run, what does it decide,
|
|
6
|
+
* what is the first thing wrong, what to do next - and wants the rest folded away.
|
|
7
|
+
*
|
|
8
|
+
* Two host hooks carry that split:
|
|
9
|
+
*
|
|
10
|
+
* - `output.presentationMeta` projects a small JSON payload the host persists on the result; a client
|
|
11
|
+
* card narrows it without parsing prose.
|
|
12
|
+
* - `presentResult` returns the card body from that payload, so a UI that supports it shows the summary
|
|
13
|
+
* and a UI that does not falls back to the raw text. The model-facing content never changes.
|
|
14
|
+
*
|
|
15
|
+
* Both are pure and replayable: they read the persisted value, never the filesystem.
|
|
16
|
+
*/
|
|
17
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
18
|
+
import { routeFor } from './routing.js'
|
|
19
|
+
import type { ToolResultView } from '@deepseek-ai/dsh-tools'
|
|
20
|
+
|
|
21
|
+
/** One finding, as a card needs it: a code to name it and a sentence to read. */
|
|
22
|
+
export interface CardFinding {
|
|
23
|
+
code: string
|
|
24
|
+
severity: string
|
|
25
|
+
message: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The payload a client card narrows. Small on purpose: a card is a summary, not a second report. */
|
|
29
|
+
export interface ReportCard {
|
|
30
|
+
verdict?: string
|
|
31
|
+
verdictReason?: string
|
|
32
|
+
errors: number
|
|
33
|
+
warnings: number
|
|
34
|
+
checksRun?: number
|
|
35
|
+
first: CardFinding | null
|
|
36
|
+
/** The one step a reader should take, already a sentence. */
|
|
37
|
+
next: string | null
|
|
38
|
+
/** Which family produced this, so a card can pick an icon and a colour without parsing codes. */
|
|
39
|
+
family: string
|
|
40
|
+
/** What this family owns, and where the case just outside it goes. One table, not a second prose copy. */
|
|
41
|
+
route?: { owns: string; instead: string }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const SHORT = 160
|
|
45
|
+
|
|
46
|
+
function shorten(text: string, limit = SHORT): string {
|
|
47
|
+
const flat = text.replace(/\s+/g, ' ').trim()
|
|
48
|
+
return flat.length <= limit ? flat : flat.slice(0, limit - 1).trimEnd() + '…'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
52
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function array(value: unknown): unknown[] {
|
|
56
|
+
return Array.isArray(value) ? value : []
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Project a report into the card payload.
|
|
61
|
+
*
|
|
62
|
+
* Read defensively: this runs on replay too, and a payload that once had a shape may come back with a
|
|
63
|
+
* field missing. A card that throws would take the whole session view with it.
|
|
64
|
+
*/
|
|
65
|
+
export function reportCard(value: unknown, family: string): ReportCard {
|
|
66
|
+
const report = record(value) ?? {}
|
|
67
|
+
const findings = array(report.findings)
|
|
68
|
+
.map((entry) => record(entry))
|
|
69
|
+
.filter((entry): entry is Record<string, unknown> => entry !== undefined)
|
|
70
|
+
const severity = (entry: Record<string, unknown>): string => String(entry.severity ?? 'info')
|
|
71
|
+
const errors = findings.filter((entry) => severity(entry) === 'error')
|
|
72
|
+
const warnings = findings.filter((entry) => severity(entry) === 'warning')
|
|
73
|
+
const summary = record(report.summary)
|
|
74
|
+
const checksRun = typeof summary?.checksRun === 'number'
|
|
75
|
+
? summary.checksRun
|
|
76
|
+
: array(report.checks).length > 0
|
|
77
|
+
? array(report.checks).length
|
|
78
|
+
: undefined
|
|
79
|
+
const first = errors[0] ?? warnings[0]
|
|
80
|
+
const next = array(report.nextSteps).map((step) => String(step))[0]
|
|
81
|
+
return {
|
|
82
|
+
...(typeof report.verdict === 'string' ? { verdict: report.verdict } : {}),
|
|
83
|
+
...(typeof report.verdictReason === 'string' ? { verdictReason: shorten(report.verdictReason) } : {}),
|
|
84
|
+
errors: errors.length,
|
|
85
|
+
warnings: warnings.length,
|
|
86
|
+
...(checksRun === undefined ? {} : { checksRun }),
|
|
87
|
+
first: first === undefined
|
|
88
|
+
? null
|
|
89
|
+
: { code: String(first.code ?? 'UNKNOWN'), severity: severity(first), message: shorten(String(first.message ?? '')) },
|
|
90
|
+
next: next === undefined ? null : shorten(next),
|
|
91
|
+
family,
|
|
92
|
+
...(routeFor(family) === undefined
|
|
93
|
+
? {}
|
|
94
|
+
: { route: { owns: routeFor(family)?.owns ?? '', instead: routeFor(family)?.notFor.tool ?? '' } }),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The card body: four lines, in the order a reader asks the questions. */
|
|
99
|
+
export function cardBlocks(card: ReportCard): ContentBlock[] {
|
|
100
|
+
const lines: string[] = []
|
|
101
|
+
const head = card.verdict === undefined ? card.family : card.verdict.toUpperCase()
|
|
102
|
+
const counts = card.checksRun === undefined
|
|
103
|
+
? `${String(card.errors)} error(s), ${String(card.warnings)} warning(s)`
|
|
104
|
+
: `${String(card.checksRun)} checks · ${String(card.errors)} error(s) · ${String(card.warnings)} warning(s)`
|
|
105
|
+
lines.push(head + ' — ' + counts)
|
|
106
|
+
if (card.verdictReason !== undefined) lines.push(card.verdictReason)
|
|
107
|
+
if (card.first !== null) {
|
|
108
|
+
lines.push((card.first.severity === 'error' ? '✗ ' : '⚠ ') + card.first.code + ': ' + card.first.message)
|
|
109
|
+
}
|
|
110
|
+
if (card.next !== null) lines.push('next: ' + card.next)
|
|
111
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* `presentResult` for a report tool: render the card from the persisted payload, and fall back to the
|
|
116
|
+
* raw content by returning undefined when there is none.
|
|
117
|
+
*/
|
|
118
|
+
export function reportResultView(family: string) {
|
|
119
|
+
return (_args: unknown, result: { content: ContentBlock[]; meta?: unknown }): ToolResultView | undefined => {
|
|
120
|
+
const card = record(result.meta) as unknown as ReportCard | undefined
|
|
121
|
+
if (card === undefined || card === null) return undefined
|
|
122
|
+
if (card.verdict === undefined && card.next === null && card.first === null) return undefined
|
|
123
|
+
return { card: 'generic', title: family + ' ' + (card.verdict ?? '') , content: cardBlocks(card) }
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/report-diff-tool.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
2
2
|
import type { JsonValue } from './json-value.js'
|
|
3
3
|
import { diffReports } from './baseline.js'
|
|
4
4
|
import { refusalVerdict } from './engine.js'
|
|
5
|
+
import { reportCard, reportResultView } from './presentation.js'
|
|
5
6
|
import { renderReportText } from './report-view.js'
|
|
6
7
|
|
|
7
8
|
export const REPORT_DIFF_TOOL_NAME = 'report_diff'
|
|
@@ -54,7 +55,12 @@ export const reportDiffTool = defineTool({
|
|
|
54
55
|
render(args, value) {
|
|
55
56
|
return [{ type: 'text' as const, text: renderReportText(value, { full: args.reportView === 'full' }) }]
|
|
56
57
|
},
|
|
57
|
-
|
|
58
|
+
// The card a UI shows, separated from the text the model reads.
|
|
59
|
+
presentationMeta(_args, value) {
|
|
60
|
+
return reportCard(value, REPORT_DIFF_TOOL_NAME) as unknown as JsonValue
|
|
61
|
+
},
|
|
62
|
+
}, presentResult: reportResultView(REPORT_DIFF_TOOL_NAME),
|
|
63
|
+
|
|
58
64
|
timeoutMs: 10_000,
|
|
59
65
|
isConcurrencySafe: () => true,
|
|
60
66
|
async execute(args) {
|
package/src/report-view.ts
CHANGED
|
@@ -199,6 +199,29 @@ function headerLines(report: Record_, options: Required<ReportViewOptions>): str
|
|
|
199
199
|
ok === undefined || verdict === undefined ? '' : `ok=${String(ok)}`,
|
|
200
200
|
].filter((bit) => bit !== '')
|
|
201
201
|
lines.push(bits.join(' '))
|
|
202
|
+
// A graph report answers "which module is the hub" in one line: counting incoming edges off the graph by
|
|
203
|
+
// hand is where a correct graph still produced a wrong answer, in both conditions of the round that asked.
|
|
204
|
+
const degreeSummary = record(report.summary)
|
|
205
|
+
const hubs = array(degreeSummary?.hubs).map((entry) => record(entry)).filter((entry) => entry !== undefined)
|
|
206
|
+
if (hubs.length > 0) {
|
|
207
|
+
const counts = degreeSummary?.nodes !== undefined && degreeSummary?.edges !== undefined ? ` ${String(degreeSummary.nodes)} modules, ${String(degreeSummary.edges)} edges` : ''
|
|
208
|
+
lines.push(`GRAPH${counts}`)
|
|
209
|
+
lines.push(' ' + hubs.map((entry) => `${String(entry.id)} (${String(entry.dependents)} dependents)`).join(', '))
|
|
210
|
+
const leaves = array(degreeSummary?.leaves).map((leaf) => String(leaf))
|
|
211
|
+
if (leaves.length > 0) lines.push(' imports nothing: ' + leaves.join(', '))
|
|
212
|
+
}
|
|
213
|
+
// A scan has to say what it covered, or a model that cannot see the boundary recounts the tree by hand
|
|
214
|
+
// and spends more than the tool saved. The numbers are the boundary: files read, references resolved.
|
|
215
|
+
const scan = record(report.scan)
|
|
216
|
+
if (scan !== undefined) {
|
|
217
|
+
const files = String(scan.files ?? '?')
|
|
218
|
+
const unresolved = Number(scan.unresolved ?? 0)
|
|
219
|
+
const languages = array(scan.languages).map((entry) => String(entry))
|
|
220
|
+
lines.push(`SCAN ${files} files read, ${String(unresolved)} unresolved${languages.length === 0 ? '' : ', ' + languages.join(', ')}`)
|
|
221
|
+
lines.push(unresolved === 0
|
|
222
|
+
? ' this is the whole tree: every relative reference resolved, so a recount by hand cannot find more'
|
|
223
|
+
: ' the unresolved lines above are outside this tree; count nothing twice')
|
|
224
|
+
}
|
|
202
225
|
// A refusal carries no model to hash, and the field is an empty string rather than absent, so it rendered
|
|
203
226
|
// as `modelHash=…` - which reads as a truncated hash rather than as nothing to hash. An empty value means
|
|
204
227
|
// the same thing as a missing one here.
|
|
@@ -259,6 +282,29 @@ function footerLines(report: Record_): string[] {
|
|
|
259
282
|
}
|
|
260
283
|
|
|
261
284
|
/** A `checks[]` report (verify, compose, datamodel): silence pass, expand the rest. */
|
|
285
|
+
/**
|
|
286
|
+
* An edit result: what changed in the model, then what changed in the findings.
|
|
287
|
+
*
|
|
288
|
+
* The two belong together because the second is the reason the first happened. The delta is
|
|
289
|
+
* rendered by the delta renderer rather than restated here, so an edit and a `report_diff` of the
|
|
290
|
+
* same two reports cannot drift apart.
|
|
291
|
+
*/
|
|
292
|
+
function renderEditReport(report: Record_, options: Required<ReportViewOptions>): string[] {
|
|
293
|
+
const lines = headerLines(report, options)
|
|
294
|
+
const changes = array(report.changes).map((value) => record(value) ?? {})
|
|
295
|
+
const added = changes.filter((change) => text(change.kind) === 'added').length
|
|
296
|
+
const removed = changes.filter((change) => text(change.kind) === 'removed').length
|
|
297
|
+
const updated = changes.filter((change) => text(change.kind) === 'updated').length
|
|
298
|
+
lines.push(`CHANGES ${String(added)} added, ${String(removed)} removed, ${String(updated)} updated`)
|
|
299
|
+
for (const change of changes) {
|
|
300
|
+
const detail = text(change.detail)
|
|
301
|
+
lines.push(` ${text(change.kind) ?? '?'} ${text(change.what) ?? '?'}${detail === undefined || detail === '' ? '' : ` — ${detail}`}`)
|
|
302
|
+
}
|
|
303
|
+
const delta = record(report.reportDelta)
|
|
304
|
+
if (delta !== undefined) lines.push('', ...renderDeltaReport(delta, options))
|
|
305
|
+
return [...lines, ...wrapFooter(report)]
|
|
306
|
+
}
|
|
307
|
+
|
|
262
308
|
function renderChecksReport(report: Record_, options: Required<ReportViewOptions>): string[] {
|
|
263
309
|
const checks = array(report.checks).map((value) => record(value) ?? {})
|
|
264
310
|
const byStatus = (status: string): string[] =>
|
|
@@ -286,7 +332,8 @@ function renderChecksReport(report: Record_, options: Required<ReportViewOptions
|
|
|
286
332
|
.filter(([, ids]) => (ids as string[]).length > 0)
|
|
287
333
|
.map(([status, ids]) => `${String(status)} ${(ids as string[]).join(' ')}`)
|
|
288
334
|
.join(' | ')
|
|
289
|
-
const lines = [...headerLines(report, options)
|
|
335
|
+
const lines = [...headerLines(report, options)]
|
|
336
|
+
lines.push(`CHECKS ${inventory}`)
|
|
290
337
|
const requested = options.check
|
|
291
338
|
const ids = checks.map((check) => text(check.id)).filter((id): id is string => id !== undefined)
|
|
292
339
|
const selected = requested === undefined ? checks : checks.filter((check) => text(check.id) === requested)
|
|
@@ -470,7 +517,11 @@ export function renderReportText(value: unknown, options: ReportViewOptions = {}
|
|
|
470
517
|
// which printed the whole object as JSON.
|
|
471
518
|
const isDelta = Array.isArray(report.added) || Array.isArray(report.removed) || Array.isArray(report.changed) || typeof report.currentVerdict === 'string'
|
|
472
519
|
const isPayload = typeof report.diagram === 'string' || typeof report.primary === 'string'
|
|
473
|
-
|
|
520
|
+
// An edit result names its changes; the findings it brings along are a delta, which already has a renderer.
|
|
521
|
+
const isEdit = Array.isArray(report.changes)
|
|
522
|
+
const lines = isEdit
|
|
523
|
+
? renderEditReport(report, resolved)
|
|
524
|
+
: hasChecks
|
|
474
525
|
? renderChecksReport(report, resolved)
|
|
475
526
|
: isDelta
|
|
476
527
|
? renderDeltaReport(report, resolved)
|
package/src/routing.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tool owns which question, in one table.
|
|
3
|
+
*
|
|
4
|
+
* Two consumers read this, and they used to read two different things: the result a model gets
|
|
5
|
+
* (`nextSteps` naming a sibling) and the toolset map a human reads (hand-written prose). That is how the
|
|
6
|
+
* map came to say "about ten thousand characters" while the descriptions measured twenty-nine: a fact
|
|
7
|
+
* stated twice drifts, and nothing fails when it does.
|
|
8
|
+
*
|
|
9
|
+
* So the table states the routing once. `routeFor` hands it to the card a UI draws; the toolset map
|
|
10
|
+
* generator reads the same table and refuses to write a map for a tool that is missing from it.
|
|
11
|
+
*
|
|
12
|
+
* What it is not: a decision. The tool a request should use is chosen by the model from the descriptions.
|
|
13
|
+
* This table says what each tool owns and which neighbour takes the case just outside that line, so a
|
|
14
|
+
* report can redirect without a reader having to remember the roster.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface Route {
|
|
18
|
+
/** What this tool decides, in the words its report already uses. */
|
|
19
|
+
owns: string
|
|
20
|
+
/** The case just outside the line, and the tool that owns it. */
|
|
21
|
+
notFor: { when: string; tool: string }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const ROUTES: Record<string, Route> = {
|
|
25
|
+
model_verify: {
|
|
26
|
+
owns: 'one machine: reachable states, invariants, deadlock, and a before/after regression',
|
|
27
|
+
notFor: { when: 'two or more machines must handshake or share a location', tool: 'model_compose_verify' },
|
|
28
|
+
},
|
|
29
|
+
model_compose_verify: {
|
|
30
|
+
owns: 'several machines run together: rendezvous, shared locations, lost updates',
|
|
31
|
+
notFor: { when: 'the question is about one machine on its own', tool: 'model_verify' },
|
|
32
|
+
},
|
|
33
|
+
model_automaton_verify: {
|
|
34
|
+
owns: 'a finite-word language, and its relation to another automaton',
|
|
35
|
+
notFor: { when: 'the question is reachability or deadlock inside one state machine', tool: 'model_verify' },
|
|
36
|
+
},
|
|
37
|
+
model_edit: {
|
|
38
|
+
owns: 'one editing round on a model file, answered with the structural diff and the change in findings',
|
|
39
|
+
notFor: { when: 'the edit is done and the model has to be decided again', tool: 'model_verify' },
|
|
40
|
+
},
|
|
41
|
+
model_datamodel_verify: {
|
|
42
|
+
owns: 'entities, keys, migrations, and whether the data survived them',
|
|
43
|
+
notFor: { when: 'the question is about behaviour over time rather than stored state', tool: 'model_verify' },
|
|
44
|
+
},
|
|
45
|
+
model_design_review: {
|
|
46
|
+
owns: "a design's own declarations: layering, responsibility, substitutability, interface width",
|
|
47
|
+
notFor: { when: 'the question is a fact about the graph - does this edge exist', tool: 'uml_structure_verify' },
|
|
48
|
+
},
|
|
49
|
+
model_interleaving_scan: {
|
|
50
|
+
owns: 'mining a document or plan for concurrency claims, and routing each to the analysis that decides it',
|
|
51
|
+
notFor: { when: 'the protocol itself has to be decided', tool: 'model_compose_verify' },
|
|
52
|
+
},
|
|
53
|
+
model_export: {
|
|
54
|
+
owns: 'a model translated into UPPAAL, TLA+, PRISM or SPIN for a property this plugin does not decide',
|
|
55
|
+
notFor: { when: 'the property can be decided here', tool: 'model_verify' },
|
|
56
|
+
},
|
|
57
|
+
report_diff: {
|
|
58
|
+
owns: 'what changed between two reports of the same family',
|
|
59
|
+
notFor: { when: 'the current report does not exist yet', tool: 'model_verify' },
|
|
60
|
+
},
|
|
61
|
+
uml_render: {
|
|
62
|
+
owns: 'drawing a model in the notation that can carry its family',
|
|
63
|
+
notFor: { when: 'the question is whether the modelling is sound', tool: 'uml_review' },
|
|
64
|
+
},
|
|
65
|
+
uml_parse: {
|
|
66
|
+
owns: 'reading a diagram back into the model its family declares',
|
|
67
|
+
notFor: { when: 'the question is whether the diagram says something sound', tool: 'uml_review' },
|
|
68
|
+
},
|
|
69
|
+
uml_review: {
|
|
70
|
+
owns: 'auditing the modelling: reachability, ambiguity, round-trip fidelity, family-specific defects',
|
|
71
|
+
notFor: { when: 'the claim is about behaviour rather than the drawing', tool: 'model_verify' },
|
|
72
|
+
},
|
|
73
|
+
uml_explain_labels: {
|
|
74
|
+
owns: 'the label spellings a notation accepts and the directives that pin the rest',
|
|
75
|
+
notFor: { when: 'a diagram has to be read rather than explained', tool: 'uml_parse' },
|
|
76
|
+
},
|
|
77
|
+
uml_structure_verify: {
|
|
78
|
+
owns: 'a dependency graph as a graph - cycles, layers, dangling edges - from a drawing or a source tree',
|
|
79
|
+
notFor: { when: 'the question is a rule the design declared for itself, not an edge that exists', tool: 'model_design_review' },
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The route for a tool, or undefined when the tool has none. */
|
|
84
|
+
export function routeFor(tool: string): Route | undefined {
|
|
85
|
+
return ROUTES[tool]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Every tool a route names, for a caller that has to check the table against the registry. */
|
|
89
|
+
export function routedTools(): string[] {
|
|
90
|
+
return Object.keys(ROUTES)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The tools named as the owner of an adjacent case. */
|
|
94
|
+
export function siblingTools(): string[] {
|
|
95
|
+
return [...new Set(Object.values(ROUTES).map((route) => route.notFor.tool))].sort()
|
|
96
|
+
}
|