dsh-model-everything 0.1.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 +102 -81
- package/README.md +88 -76
- package/lib/automaton-tool.js +69 -0
- package/lib/automaton.js +327 -0
- package/lib/compose-tool.js +11 -2
- package/lib/concurrency-tool.js +6 -1
- package/lib/data-engine.js +1 -1
- package/lib/data-tool.js +18 -5
- package/lib/design-tool.js +21 -8
- package/lib/edit-tool.js +111 -0
- package/lib/editor.js +293 -0
- package/lib/engine.js +8 -1
- package/lib/export-tool.js +6 -1
- package/lib/index.js +4 -0
- package/lib/model-file.js +48 -0
- package/lib/presentation.js +81 -0
- package/lib/report-diff-tool.js +6 -1
- package/lib/report-view.js +92 -15
- 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 +19 -4
- 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/model-file.d.ts +7 -0
- package/lib/types/presentation.d.ts +59 -0
- package/lib/types/report-view.d.ts +8 -1
- 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 +8 -3
- package/skills/model-everything/SKILL.md +51 -79
- package/skills/model-everything/references/automata.md +102 -0
- package/skills/model-everything-interleaving/SKILL.zh-CN.md +60 -60
- package/skills/model-everything-report-reading/SKILL.md +137 -137
- package/src/automaton-tool.ts +73 -0
- package/src/automaton.ts +375 -0
- package/src/compose-tool.ts +13 -3
- package/src/concurrency-tool.ts +7 -1
- package/src/data-engine.ts +1 -1
- package/src/data-tool.ts +19 -5
- package/src/design-tool.ts +74 -60
- package/src/edit-tool.ts +119 -0
- package/src/editor.ts +297 -0
- package/src/engine.ts +8 -1
- package/src/export-tool.ts +87 -81
- package/src/index.ts +4 -0
- package/src/model-file.ts +49 -0
- package/src/presentation.ts +125 -0
- package/src/report-diff-tool.ts +7 -1
- package/src/report-view.ts +534 -451
- 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 +19 -4
- package/src/uml-tool.ts +240 -230
package/lib/report-view.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const DEFAULTS = { maxFindingsPerCheck: 24, maxEvidenceChars: 160, includePassing: false, full: false };
|
|
1
|
+
const DEFAULTS = { maxFindingsPerCheck: 24, maxEvidenceChars: 160, includePassing: false, full: false, check: undefined };
|
|
2
2
|
const record = (value) => typeof value === 'object' && value !== null && !Array.isArray(value) ? value : undefined;
|
|
3
3
|
const array = (value) => (Array.isArray(value) ? value : []);
|
|
4
4
|
const text = (value) => (typeof value === 'string' ? value : undefined);
|
|
@@ -93,8 +93,20 @@ function findingLines(findings, options, seenFacts) {
|
|
|
93
93
|
group.events.push(event);
|
|
94
94
|
stateGroups.set(state, group);
|
|
95
95
|
}
|
|
96
|
+
// The same event set on every state is one fact, not one per state: a 26-state machine whose
|
|
97
|
+
// states all ignore the same four events printed 24 near-identical lines, a quarter of the body.
|
|
98
|
+
// States that share a set are counted; a handful still name themselves, because there the names
|
|
99
|
+
// are what the reader acts on.
|
|
100
|
+
const byEventSet = new Map();
|
|
96
101
|
for (const [state, group] of stateGroups) {
|
|
97
|
-
|
|
102
|
+
const key = `${group.severity}|${group.code}|${group.events.join(',')}`;
|
|
103
|
+
const bucket = byEventSet.get(key) ?? { severity: group.severity, code: group.code, events: group.events, states: [] };
|
|
104
|
+
bucket.states.push(state);
|
|
105
|
+
byEventSet.set(key, bucket);
|
|
106
|
+
}
|
|
107
|
+
for (const group of byEventSet.values()) {
|
|
108
|
+
const who = group.states.length > 3 ? `${String(group.states.length)} states` : group.states.join(', ');
|
|
109
|
+
lines.push(` ${who} (${String(group.events.length)}) [${group.severity}/${group.code}]: ${group.events.join(', ')}`);
|
|
98
110
|
}
|
|
99
111
|
const rendered = collapse(singles.map((finding) => findingLine(finding, options)));
|
|
100
112
|
const budget = options.maxFindingsPerCheck;
|
|
@@ -126,6 +138,30 @@ function headerLines(report, options) {
|
|
|
126
138
|
ok === undefined || verdict === undefined ? '' : `ok=${String(ok)}`,
|
|
127
139
|
].filter((bit) => bit !== '');
|
|
128
140
|
lines.push(bits.join(' '));
|
|
141
|
+
// A graph report answers "which module is the hub" in one line: counting incoming edges off the graph by
|
|
142
|
+
// hand is where a correct graph still produced a wrong answer, in both conditions of the round that asked.
|
|
143
|
+
const degreeSummary = record(report.summary);
|
|
144
|
+
const hubs = array(degreeSummary?.hubs).map((entry) => record(entry)).filter((entry) => entry !== undefined);
|
|
145
|
+
if (hubs.length > 0) {
|
|
146
|
+
const counts = degreeSummary?.nodes !== undefined && degreeSummary?.edges !== undefined ? ` ${String(degreeSummary.nodes)} modules, ${String(degreeSummary.edges)} edges` : '';
|
|
147
|
+
lines.push(`GRAPH${counts}`);
|
|
148
|
+
lines.push(' ' + hubs.map((entry) => `${String(entry.id)} (${String(entry.dependents)} dependents)`).join(', '));
|
|
149
|
+
const leaves = array(degreeSummary?.leaves).map((leaf) => String(leaf));
|
|
150
|
+
if (leaves.length > 0)
|
|
151
|
+
lines.push(' imports nothing: ' + leaves.join(', '));
|
|
152
|
+
}
|
|
153
|
+
// A scan has to say what it covered, or a model that cannot see the boundary recounts the tree by hand
|
|
154
|
+
// and spends more than the tool saved. The numbers are the boundary: files read, references resolved.
|
|
155
|
+
const scan = record(report.scan);
|
|
156
|
+
if (scan !== undefined) {
|
|
157
|
+
const files = String(scan.files ?? '?');
|
|
158
|
+
const unresolved = Number(scan.unresolved ?? 0);
|
|
159
|
+
const languages = array(scan.languages).map((entry) => String(entry));
|
|
160
|
+
lines.push(`SCAN ${files} files read, ${String(unresolved)} unresolved${languages.length === 0 ? '' : ', ' + languages.join(', ')}`);
|
|
161
|
+
lines.push(unresolved === 0
|
|
162
|
+
? ' this is the whole tree: every relative reference resolved, so a recount by hand cannot find more'
|
|
163
|
+
: ' the unresolved lines above are outside this tree; count nothing twice');
|
|
164
|
+
}
|
|
129
165
|
// A refusal carries no model to hash, and the field is an empty string rather than absent, so it rendered
|
|
130
166
|
// as `modelHash=…` - which reads as a truncated hash rather than as nothing to hash. An empty value means
|
|
131
167
|
// the same thing as a missing one here.
|
|
@@ -192,6 +228,29 @@ function footerLines(report) {
|
|
|
192
228
|
return lines;
|
|
193
229
|
}
|
|
194
230
|
/** A `checks[]` report (verify, compose, datamodel): silence pass, expand the rest. */
|
|
231
|
+
/**
|
|
232
|
+
* An edit result: what changed in the model, then what changed in the findings.
|
|
233
|
+
*
|
|
234
|
+
* The two belong together because the second is the reason the first happened. The delta is
|
|
235
|
+
* rendered by the delta renderer rather than restated here, so an edit and a `report_diff` of the
|
|
236
|
+
* same two reports cannot drift apart.
|
|
237
|
+
*/
|
|
238
|
+
function renderEditReport(report, options) {
|
|
239
|
+
const lines = headerLines(report, options);
|
|
240
|
+
const changes = array(report.changes).map((value) => record(value) ?? {});
|
|
241
|
+
const added = changes.filter((change) => text(change.kind) === 'added').length;
|
|
242
|
+
const removed = changes.filter((change) => text(change.kind) === 'removed').length;
|
|
243
|
+
const updated = changes.filter((change) => text(change.kind) === 'updated').length;
|
|
244
|
+
lines.push(`CHANGES ${String(added)} added, ${String(removed)} removed, ${String(updated)} updated`);
|
|
245
|
+
for (const change of changes) {
|
|
246
|
+
const detail = text(change.detail);
|
|
247
|
+
lines.push(` ${text(change.kind) ?? '?'} ${text(change.what) ?? '?'}${detail === undefined || detail === '' ? '' : ` — ${detail}`}`);
|
|
248
|
+
}
|
|
249
|
+
const delta = record(report.reportDelta);
|
|
250
|
+
if (delta !== undefined)
|
|
251
|
+
lines.push('', ...renderDeltaReport(delta, options));
|
|
252
|
+
return [...lines, ...wrapFooter(report)];
|
|
253
|
+
}
|
|
195
254
|
function renderChecksReport(report, options) {
|
|
196
255
|
const checks = array(report.checks).map((value) => record(value) ?? {});
|
|
197
256
|
const byStatus = (status) => checks.filter((check) => text(check.status) === status).map((check) => text(check.id) ?? '?');
|
|
@@ -218,14 +277,26 @@ function renderChecksReport(report, options) {
|
|
|
218
277
|
.filter(([, ids]) => ids.length > 0)
|
|
219
278
|
.map(([status, ids]) => `${String(status)} ${ids.join(' ')}`)
|
|
220
279
|
.join(' | ');
|
|
221
|
-
const lines = [...headerLines(report, options)
|
|
280
|
+
const lines = [...headerLines(report, options)];
|
|
281
|
+
lines.push(`CHECKS ${inventory}`);
|
|
282
|
+
const requested = options.check;
|
|
283
|
+
const ids = checks.map((check) => text(check.id)).filter((id) => id !== undefined);
|
|
284
|
+
const selected = requested === undefined ? checks : checks.filter((check) => text(check.id) === requested);
|
|
285
|
+
// A name that matches nothing is answered with the names that exist: silently printing the whole
|
|
286
|
+
// report, or an empty body, both read as "that check was clean".
|
|
287
|
+
if (requested !== undefined && selected.length === 0) {
|
|
288
|
+
lines.push(`NOTE no check \`${requested}\` in this report; it carries: ${ids.join(' ')}`);
|
|
289
|
+
}
|
|
290
|
+
// Asking for one check is a request for its lines: the per-check budget that keeps the digest
|
|
291
|
+
// small does not apply, since the caller has already paid for the narrowing.
|
|
292
|
+
const view = requested === undefined ? options : { ...options, includePassing: true, maxFindingsPerCheck: Number.MAX_SAFE_INTEGER };
|
|
222
293
|
const seenFacts = new Set();
|
|
223
|
-
for (const check of
|
|
294
|
+
for (const check of selected) {
|
|
224
295
|
const status = text(check.status) ?? 'skip';
|
|
225
296
|
const findings = array(check.findings).map((finding) => toViewFinding(finding, text(check.id)));
|
|
226
297
|
if (status === 'pass' && !options.includePassing)
|
|
227
298
|
continue;
|
|
228
|
-
const body = findingLines(findings,
|
|
299
|
+
const body = findingLines(findings, view, seenFacts);
|
|
229
300
|
if (status !== 'fail' && body.length === 0)
|
|
230
301
|
continue;
|
|
231
302
|
lines.push('');
|
|
@@ -380,8 +451,10 @@ function renderFallbackReport(report, options) {
|
|
|
380
451
|
*/
|
|
381
452
|
export function renderReportText(value, options = {}) {
|
|
382
453
|
const resolved = { ...DEFAULTS, ...options };
|
|
454
|
+
// Compact rather than pretty: `full` exists so a caller can hand the object on (report_diff matches
|
|
455
|
+
// on check + code + evidence), and indentation is a third of a large report's bytes for no reader.
|
|
383
456
|
if (resolved.full)
|
|
384
|
-
return JSON.stringify(value
|
|
457
|
+
return JSON.stringify(value);
|
|
385
458
|
const report = record(value);
|
|
386
459
|
if (report === undefined)
|
|
387
460
|
return JSON.stringify(value) ?? String(value);
|
|
@@ -395,14 +468,18 @@ export function renderReportText(value, options = {}) {
|
|
|
395
468
|
// which printed the whole object as JSON.
|
|
396
469
|
const isDelta = Array.isArray(report.added) || Array.isArray(report.removed) || Array.isArray(report.changed) || typeof report.currentVerdict === 'string';
|
|
397
470
|
const isPayload = typeof report.diagram === 'string' || typeof report.primary === 'string';
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
471
|
+
// An edit result names its changes; the findings it brings along are a delta, which already has a renderer.
|
|
472
|
+
const isEdit = Array.isArray(report.changes);
|
|
473
|
+
const lines = isEdit
|
|
474
|
+
? renderEditReport(report, resolved)
|
|
475
|
+
: hasChecks
|
|
476
|
+
? renderChecksReport(report, resolved)
|
|
477
|
+
: isDelta
|
|
478
|
+
? renderDeltaReport(report, resolved)
|
|
479
|
+
: hasFindings
|
|
480
|
+
? renderFindingsReport(report, resolved)
|
|
481
|
+
: isPayload
|
|
482
|
+
? renderPayloadReport(report, resolved)
|
|
483
|
+
: renderFallbackReport(report, resolved);
|
|
407
484
|
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
408
485
|
}
|
package/lib/routing.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
export const ROUTES = {
|
|
17
|
+
model_verify: {
|
|
18
|
+
owns: 'one machine: reachable states, invariants, deadlock, and a before/after regression',
|
|
19
|
+
notFor: { when: 'two or more machines must handshake or share a location', tool: 'model_compose_verify' },
|
|
20
|
+
},
|
|
21
|
+
model_compose_verify: {
|
|
22
|
+
owns: 'several machines run together: rendezvous, shared locations, lost updates',
|
|
23
|
+
notFor: { when: 'the question is about one machine on its own', tool: 'model_verify' },
|
|
24
|
+
},
|
|
25
|
+
model_automaton_verify: {
|
|
26
|
+
owns: 'a finite-word language, and its relation to another automaton',
|
|
27
|
+
notFor: { when: 'the question is reachability or deadlock inside one state machine', tool: 'model_verify' },
|
|
28
|
+
},
|
|
29
|
+
model_edit: {
|
|
30
|
+
owns: 'one editing round on a model file, answered with the structural diff and the change in findings',
|
|
31
|
+
notFor: { when: 'the edit is done and the model has to be decided again', tool: 'model_verify' },
|
|
32
|
+
},
|
|
33
|
+
model_datamodel_verify: {
|
|
34
|
+
owns: 'entities, keys, migrations, and whether the data survived them',
|
|
35
|
+
notFor: { when: 'the question is about behaviour over time rather than stored state', tool: 'model_verify' },
|
|
36
|
+
},
|
|
37
|
+
model_design_review: {
|
|
38
|
+
owns: "a design's own declarations: layering, responsibility, substitutability, interface width",
|
|
39
|
+
notFor: { when: 'the question is a fact about the graph - does this edge exist', tool: 'uml_structure_verify' },
|
|
40
|
+
},
|
|
41
|
+
model_interleaving_scan: {
|
|
42
|
+
owns: 'mining a document or plan for concurrency claims, and routing each to the analysis that decides it',
|
|
43
|
+
notFor: { when: 'the protocol itself has to be decided', tool: 'model_compose_verify' },
|
|
44
|
+
},
|
|
45
|
+
model_export: {
|
|
46
|
+
owns: 'a model translated into UPPAAL, TLA+, PRISM or SPIN for a property this plugin does not decide',
|
|
47
|
+
notFor: { when: 'the property can be decided here', tool: 'model_verify' },
|
|
48
|
+
},
|
|
49
|
+
report_diff: {
|
|
50
|
+
owns: 'what changed between two reports of the same family',
|
|
51
|
+
notFor: { when: 'the current report does not exist yet', tool: 'model_verify' },
|
|
52
|
+
},
|
|
53
|
+
uml_render: {
|
|
54
|
+
owns: 'drawing a model in the notation that can carry its family',
|
|
55
|
+
notFor: { when: 'the question is whether the modelling is sound', tool: 'uml_review' },
|
|
56
|
+
},
|
|
57
|
+
uml_parse: {
|
|
58
|
+
owns: 'reading a diagram back into the model its family declares',
|
|
59
|
+
notFor: { when: 'the question is whether the diagram says something sound', tool: 'uml_review' },
|
|
60
|
+
},
|
|
61
|
+
uml_review: {
|
|
62
|
+
owns: 'auditing the modelling: reachability, ambiguity, round-trip fidelity, family-specific defects',
|
|
63
|
+
notFor: { when: 'the claim is about behaviour rather than the drawing', tool: 'model_verify' },
|
|
64
|
+
},
|
|
65
|
+
uml_explain_labels: {
|
|
66
|
+
owns: 'the label spellings a notation accepts and the directives that pin the rest',
|
|
67
|
+
notFor: { when: 'a diagram has to be read rather than explained', tool: 'uml_parse' },
|
|
68
|
+
},
|
|
69
|
+
uml_structure_verify: {
|
|
70
|
+
owns: 'a dependency graph as a graph - cycles, layers, dangling edges - from a drawing or a source tree',
|
|
71
|
+
notFor: { when: 'the question is a rule the design declared for itself, not an edge that exists', tool: 'model_design_review' },
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
/** The route for a tool, or undefined when the tool has none. */
|
|
75
|
+
export function routeFor(tool) {
|
|
76
|
+
return ROUTES[tool];
|
|
77
|
+
}
|
|
78
|
+
/** Every tool a route names, for a caller that has to check the table against the registry. */
|
|
79
|
+
export function routedTools() {
|
|
80
|
+
return Object.keys(ROUTES);
|
|
81
|
+
}
|
|
82
|
+
/** The tools named as the owner of an adjacent case. */
|
|
83
|
+
export function siblingTools() {
|
|
84
|
+
return [...new Set(Object.values(ROUTES).map((route) => route.notFor.tool))].sort();
|
|
85
|
+
}
|