dsh-model-everything 0.2.0 → 0.4.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 (54) hide show
  1. package/README.en-US.md +104 -76
  2. package/README.md +84 -71
  3. package/lib/automaton-tool.js +70 -0
  4. package/lib/automaton.js +328 -0
  5. package/lib/compose-tool.js +7 -0
  6. package/lib/concurrency-tool.js +7 -0
  7. package/lib/data-tool.js +7 -0
  8. package/lib/design-tool.js +8 -1
  9. package/lib/edit-tool.js +113 -0
  10. package/lib/editor.js +293 -0
  11. package/lib/engine.js +7 -0
  12. package/lib/export-tool.js +8 -0
  13. package/lib/index.js +4 -0
  14. package/lib/presentation.js +309 -0
  15. package/lib/report-diff-tool.js +7 -0
  16. package/lib/report-view.js +62 -10
  17. package/lib/routing.js +85 -0
  18. package/lib/scan.js +547 -0
  19. package/lib/structure-tool.js +35 -1
  20. package/lib/structure.js +28 -3
  21. package/lib/tool.js +7 -0
  22. package/lib/types/automaton-tool.d.ts +2 -0
  23. package/lib/types/automaton.d.ts +57 -0
  24. package/lib/types/edit-tool.d.ts +2 -0
  25. package/lib/types/editor.d.ts +46 -0
  26. package/lib/types/engine.d.ts +1 -0
  27. package/lib/types/presentation.d.ts +164 -0
  28. package/lib/types/routing.d.ts +31 -0
  29. package/lib/types/scan.d.ts +37 -0
  30. package/lib/types/structure.d.ts +16 -0
  31. package/lib/uml-tool.js +21 -6
  32. package/package.json +6 -3
  33. package/skills/model-everything/SKILL.md +6 -2
  34. package/skills/model-everything/references/automata.md +102 -0
  35. package/src/automaton-tool.ts +74 -0
  36. package/src/automaton.ts +376 -0
  37. package/src/compose-tool.ts +13 -5
  38. package/src/concurrency-tool.ts +8 -0
  39. package/src/data-tool.ts +8 -0
  40. package/src/design-tool.ts +76 -68
  41. package/src/edit-tool.ts +121 -0
  42. package/src/editor.ts +297 -0
  43. package/src/engine.ts +7 -0
  44. package/src/export-tool.ts +90 -81
  45. package/src/index.ts +4 -0
  46. package/src/presentation.ts +365 -0
  47. package/src/report-diff-tool.ts +8 -0
  48. package/src/report-view.ts +53 -2
  49. package/src/routing.ts +96 -0
  50. package/src/scan.ts +564 -0
  51. package/src/structure-tool.ts +34 -1
  52. package/src/structure.ts +37 -3
  53. package/src/tool.ts +7 -0
  54. package/src/uml-tool.ts +245 -230
@@ -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), `CHECKS ${inventory}`]
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
- const lines = hasChecks
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
+ }