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.
Files changed (62) hide show
  1. package/README.en-US.md +102 -81
  2. package/README.md +88 -76
  3. package/lib/automaton-tool.js +69 -0
  4. package/lib/automaton.js +327 -0
  5. package/lib/compose-tool.js +11 -2
  6. package/lib/concurrency-tool.js +6 -1
  7. package/lib/data-engine.js +1 -1
  8. package/lib/data-tool.js +18 -5
  9. package/lib/design-tool.js +21 -8
  10. package/lib/edit-tool.js +111 -0
  11. package/lib/editor.js +293 -0
  12. package/lib/engine.js +8 -1
  13. package/lib/export-tool.js +6 -1
  14. package/lib/index.js +4 -0
  15. package/lib/model-file.js +48 -0
  16. package/lib/presentation.js +81 -0
  17. package/lib/report-diff-tool.js +6 -1
  18. package/lib/report-view.js +92 -15
  19. package/lib/routing.js +85 -0
  20. package/lib/scan.js +547 -0
  21. package/lib/structure-tool.js +34 -1
  22. package/lib/structure.js +28 -3
  23. package/lib/tool.js +19 -4
  24. package/lib/types/automaton-tool.d.ts +2 -0
  25. package/lib/types/automaton.d.ts +57 -0
  26. package/lib/types/edit-tool.d.ts +2 -0
  27. package/lib/types/editor.d.ts +46 -0
  28. package/lib/types/engine.d.ts +1 -0
  29. package/lib/types/model-file.d.ts +7 -0
  30. package/lib/types/presentation.d.ts +59 -0
  31. package/lib/types/report-view.d.ts +8 -1
  32. package/lib/types/routing.d.ts +31 -0
  33. package/lib/types/scan.d.ts +37 -0
  34. package/lib/types/structure.d.ts +16 -0
  35. package/lib/uml-tool.js +16 -6
  36. package/package.json +8 -3
  37. package/skills/model-everything/SKILL.md +51 -79
  38. package/skills/model-everything/references/automata.md +102 -0
  39. package/skills/model-everything-interleaving/SKILL.zh-CN.md +60 -60
  40. package/skills/model-everything-report-reading/SKILL.md +137 -137
  41. package/src/automaton-tool.ts +73 -0
  42. package/src/automaton.ts +375 -0
  43. package/src/compose-tool.ts +13 -3
  44. package/src/concurrency-tool.ts +7 -1
  45. package/src/data-engine.ts +1 -1
  46. package/src/data-tool.ts +19 -5
  47. package/src/design-tool.ts +74 -60
  48. package/src/edit-tool.ts +119 -0
  49. package/src/editor.ts +297 -0
  50. package/src/engine.ts +8 -1
  51. package/src/export-tool.ts +87 -81
  52. package/src/index.ts +4 -0
  53. package/src/model-file.ts +49 -0
  54. package/src/presentation.ts +125 -0
  55. package/src/report-diff-tool.ts +7 -1
  56. package/src/report-view.ts +534 -451
  57. package/src/routing.ts +96 -0
  58. package/src/scan.ts +564 -0
  59. package/src/structure-tool.ts +33 -1
  60. package/src/structure.ts +37 -3
  61. package/src/tool.ts +19 -4
  62. package/src/uml-tool.ts +240 -230
@@ -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
- lines.push(` ${state} (${String(group.events.length)}) [${group.severity}/${group.code}]: ${group.events.join(', ')}`);
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), `CHECKS ${inventory}`];
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 checks) {
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, options, seenFacts);
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, null, 2);
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
- const lines = hasChecks
399
- ? renderChecksReport(report, resolved)
400
- : isDelta
401
- ? renderDeltaReport(report, resolved)
402
- : hasFindings
403
- ? renderFindingsReport(report, resolved)
404
- : isPayload
405
- ? renderPayloadReport(report, resolved)
406
- : renderFallbackReport(report, resolved);
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
+ }