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
@@ -2,9 +2,11 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
2
2
  import type { JsonValue } from './json-value.js'
3
3
  import { REPORT_SCHEMAS } from './engine.js'
4
4
  import { reviewStructure } from './structure.js'
5
+ import { scanDependencyGraph } from './scan.js'
5
6
  import { reviewGranularity, type GranularityDiagram } from './granularity.js'
6
7
  import { OptionShapeError, optionRefusal, requireArrayOption } from './option-shape.js'
7
8
  import { renderReportText } from './report-view.js'
9
+ import { reportCard, reportResultView } from './presentation.js'
8
10
 
9
11
  export const STRUCTURE_TOOL_NAME = 'uml_structure_verify'
10
12
 
@@ -19,7 +21,7 @@ export const STRUCTURE_TOOL_NAME = 'uml_structure_verify'
19
21
  export const umlStructureTool = defineTool({
20
22
  name: STRUCTURE_TOOL_NAME,
21
23
  description:
22
- 'Audit a structure diagram as a dependency graph. Takes PlantUML component, package, class or deployment text, or a Mermaid class diagram. Reports UML020 isolated node, UML021 dangling reference, UML022 cycle (shortest path), UML023 disallowed edge and UML024 layer violation against a matrix, UML025 missing required edge, and UML026 when no node is declared. The matrix is {rules: [{id, source, allow?, deny?, require?}], layers: [{name, members}], default: "allow"|"deny"}. Ids and members accept globs (`*`, `?`). Deny beats allow, and layers run top-first, so downward edges pass and upward ones are UML024. With a matrix, every edge appears in `edgeVerdicts` with the rules it matched. Pass `diagrams` instead of `diagram` to check several levels at once: each child must refine its parent. This audits the diagram, not the code: reconcile it with a source-side scan. Read `verdict`, not `ok`. For state or activity diagrams use `uml` and `model_verify`.',
24
+ 'Audit a structure diagram as a dependency graph. Takes PlantUML component, package, class or deployment text, or a Mermaid class diagram - or `repoPath`, a source tree, in which case it builds the graph from the import constructs the source itself uses and no drawing is needed. Reports UML020 isolated node, UML021 dangling reference, UML022 cycle (shortest path), UML023 disallowed edge and UML024 layer violation against a matrix, UML025 missing required edge, and UML026 when no node is declared. The matrix is {rules: [{id, source, allow?, deny?, require?}], layers: [{name, members}], default: "allow"|"deny"}. Ids and members accept globs (`*`, `?`). Deny beats allow, and layers run top-first, so downward edges pass and upward ones are UML024. With a matrix, every edge appears in `edgeVerdicts` with the rules it matched. Pass `diagrams` instead of `diagram` to check several levels at once: each child must refine its parent. It answers **what the graph is**: which edges exist, which node has no edge, which cycle, and whether an edge breaks a matrix you supplied. It does not judge whether the design meant to allow those edges - `model_design_review` does that against the rules the design declares for itself. When the question is whether an import exists from one layer to another, this answers it, from the code. Read `verdict`, not `ok`. For state or activity diagrams use `uml` and `model_verify`.',
23
25
  parameters: {
24
26
  diagram: {
25
27
  type: 'string',
@@ -34,6 +36,10 @@ export const umlStructureTool = defineTool({
34
36
  enum: ['auto', 'plantuml', 'mermaid'],
35
37
  description: 'Diagram language. Default auto (detected from the text).',
36
38
  },
39
+ repoPath: {
40
+ type: 'string',
41
+ description: 'Directory or single source file to scan instead of drawing the diagram yourself. Reads the import construct of C/C++, Python, TypeScript/JavaScript, Go, Rust, Java/Kotlin/Scala, C#, PHP, Ruby, Lua, Zig, Dart and Elixir, building a file graph for file languages and a package graph for package languages (Go, Java, C#). The graph is reviewed by the same checks; `diagram` and `diagrams` are then ignored.',
42
+ },
37
43
  matrix: {
38
44
  type: 'json',
39
45
  description: 'Optional dependency matrix: {rules: [{id, source, allow?, deny?, require?}], layers: [{name, members}], default: "allow"|"deny"}. Every edge is judged against it and the matched rule ids are reported.',
@@ -52,7 +58,12 @@ export const umlStructureTool = defineTool({
52
58
  render(args, value) {
53
59
  return [{ type: 'text' as const, text: renderReportText(value, { full: args.reportView === 'full' }) }]
54
60
  },
61
+ // The card a UI shows, separated from the text the model reads.
62
+ presentationMeta(_args, value) {
63
+ return reportCard(value, STRUCTURE_TOOL_NAME) as unknown as JsonValue
64
+ },
55
65
  },
66
+ presentResult: reportResultView(STRUCTURE_TOOL_NAME),
56
67
  timeoutMs: 10_000,
57
68
  isConcurrencySafe: () => true,
58
69
  async execute(args) {
@@ -81,6 +92,27 @@ export const umlStructureTool = defineTool({
81
92
  return optionRefusal(error, 'STRUCTURE_OPTION_INVALID') as unknown as JsonValue
82
93
  }
83
94
  }
95
+ if (args.repoPath !== undefined) {
96
+ try {
97
+ const scanned = scanDependencyGraph(args.repoPath)
98
+ // A source module is a declared node that depends on others: not a container, and it has a
99
+ // line only when it came from a drawing, so the field stays absent.
100
+ const graph = {
101
+ notation: 'plantuml' as const,
102
+ nodes: scanned.nodes.map((id) => ({ id, label: id, kind: 'component' as const, container: false, declared: true })),
103
+ edges: scanned.edges.map((edge) => ({ from: edge.from, to: edge.to, line: 0 })),
104
+ warnings: [],
105
+ }
106
+ return reviewStructure({
107
+ diagram: '',
108
+ graph,
109
+ scan: { files: scanned.filesRead, unresolved: scanned.unresolved.length, packages: scanned.packages.length, languages: scanned.languages.map((entry) => entry.language) },
110
+ ...(args.matrix === undefined ? {} : { matrix: args.matrix }),
111
+ }) as unknown as JsonValue
112
+ } catch (error) {
113
+ return optionRefusal(error, 'STRUCTURE_OPTION_INVALID') as unknown as JsonValue
114
+ }
115
+ }
84
116
  if (args.diagram === undefined) {
85
117
  return { ok: false, ran: false, verdict: 'fail', verdictReason: 'the tool did not run: pass `diagram` (one structure diagram) or `diagrams` (several levels with parents)', schema: REPORT_SCHEMAS.structure, errorCode: 'UML_INPUT', error: 'structure review needs `diagram` or `diagrams`' } as unknown as JsonValue
86
118
  }
package/src/structure.ts CHANGED
@@ -107,6 +107,10 @@ export interface StructureSummary {
107
107
  errors: number
108
108
  warnings: number
109
109
  rules: number
110
+ /** The most depended-on nodes, by incoming edges. What a reader asks first and counts worst. */
111
+ hubs: { id: string; dependents: number }[]
112
+ /** Modules nothing depends on (no outgoing edges), capped for the report. */
113
+ leaves: string[]
110
114
  }
111
115
 
112
116
  export interface StructureReport {
@@ -503,6 +507,10 @@ export function shortestCycle(nodes: string[], edges: StructureEdge[]): string[]
503
507
  export interface StructureReviewOptions {
504
508
  /** Diagram text to review. */
505
509
  diagram: string
510
+ /** A graph built from source, instead of parsed from a diagram. `diagram` is then ignored. */
511
+ graph?: StructureGraph
512
+ /** How the graph was produced, when it came from a scan rather than a drawing. */
513
+ scan?: { files: number; unresolved: number; packages: number; languages: string[] }
506
514
  notation?: 'auto' | StructureNotation
507
515
  /** Dependency matrix: allowed/denied edges, layers, required edges. */
508
516
  matrix?: unknown
@@ -531,8 +539,9 @@ function requireDiagramText(value: unknown): string {
531
539
  export function reviewStructure(options: StructureReviewOptions): StructureReport {
532
540
  // Not `if (options.diagram !== undefined)`: a missing `diagram` reached the parser as `undefined`
533
541
  // and threw a read of `split` on it - the same defect, one guard narrower.
534
- requireDiagramText(options.diagram)
535
- const graph = parseStructure(options.diagram, options.notation ?? 'auto')
542
+ // A graph built from source arrives already parsed; a caller that passes one has no diagram text to
543
+ // require, so the text guard belongs to the other branch.
544
+ const graph = options.graph ?? parseStructure(requireDiagramText(options.diagram), options.notation ?? 'auto')
536
545
  const warnings = [...graph.warnings]
537
546
  const findings: Finding[] = []
538
547
  const nodes = graph.nodes
@@ -730,6 +739,26 @@ export function reviewStructure(options: StructureReviewOptions): StructureRepor
730
739
  })
731
740
  }
732
741
 
742
+ // Degree is what a reader asks first and counts worst: which module everything depends on, and which
743
+ // depends on nothing. Counting it here means the answer is one line in the report rather than a scan of
744
+ // the graph by hand, which is where a correct graph still produced a wrong answer.
745
+ const inDegree = new Map<string, number>()
746
+ const outDegree = new Map<string, number>()
747
+ for (const edge of graph.edges) {
748
+ inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1)
749
+ outDegree.set(edge.from, (outDegree.get(edge.from) ?? 0) + 1)
750
+ }
751
+ const hubs = [...inDegree.entries()]
752
+ .filter(([, count]) => count > 0)
753
+ .sort((a, b) => (b[1] === a[1] ? a[0].localeCompare(b[0]) : b[1] - a[1]))
754
+ .slice(0, 5)
755
+ .map(([id, dependents]) => ({ id, dependents }))
756
+ const leaves = nodes
757
+ .filter((node) => !node.container && (outDegree.get(node.id) ?? 0) === 0)
758
+ .map((node) => node.id)
759
+ .sort()
760
+ .slice(0, 10)
761
+
733
762
  const errors = findings.filter((finding) => finding.severity === 'error').length
734
763
  const warningCount = findings.filter((finding) => finding.severity === 'warning').length
735
764
  const summary: StructureSummary = {
@@ -745,6 +774,8 @@ export function reviewStructure(options: StructureReviewOptions): StructureRepor
745
774
  errors,
746
775
  warnings: warningCount,
747
776
  rules: rules.length,
777
+ hubs,
778
+ leaves,
748
779
  }
749
780
 
750
781
  const nextSteps: string[] = []
@@ -761,10 +792,13 @@ export function reviewStructure(options: StructureReviewOptions): StructureRepor
761
792
  notation: graph.notation,
762
793
  graph,
763
794
  ...(options.matrix === undefined ? {} : { edgeVerdicts }),
795
+ ...(options.scan === undefined ? {} : { scan: options.scan }),
764
796
  findings,
765
797
  summary,
766
798
  hashes: {
767
- diagram: createHash('sha256').update(options.diagram).digest('hex'),
799
+ // A graph built from source has no diagram text to hash; hashing the text would hash the empty
800
+ // string, and every scan would then carry the same input hash.
801
+ diagram: createHash('sha256').update(options.graph === undefined ? options.diagram : canonicalJson({ nodes: options.graph.nodes.map((node) => node.id), edges: options.graph.edges.map((edge) => ({ from: edge.from, to: edge.to })) })).digest('hex'),
768
802
  ...(options.matrix === undefined ? {} : { matrix: createHash('sha256').update(canonicalJson(options.matrix)).digest('hex') }),
769
803
  },
770
804
  warnings,
package/src/tool.ts CHANGED
@@ -2,7 +2,9 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
2
2
  import type { JsonValue } from './json-value.js'
3
3
  import { runVerification } from './engine.js'
4
4
  import { renderReportText } from './report-view.js'
5
+ import { reportCard, reportResultView } from './presentation.js'
5
6
  import { OptionShapeError, optionRefusal, requireObjectOption } from './option-shape.js'
7
+ import { oneOf } from './model-file.js'
6
8
 
7
9
  export const MODEL_VERIFY_TOOL_NAME = 'model_verify'
8
10
 
@@ -22,8 +24,11 @@ export const modelVerifyTool = defineTool({
22
24
  parameters: {
23
25
  model: {
24
26
  type: 'json',
25
- required: true,
26
- description: 'LogicModelV1 state machine model to verify.',
27
+ description: 'LogicModelV1 state machine model to verify. Pass this, or modelPath, not both.',
28
+ },
29
+ modelPath: {
30
+ type: 'string',
31
+ description: 'Path to a JSON file holding the model, instead of inlining it. Formatting does not matter: whitespace, a BOM and pretty printing all parse. Relative paths resolve against the process working directory, which is the session workspace.',
27
32
  },
28
33
  maxStates: {
29
34
  type: 'integer',
@@ -41,6 +46,10 @@ export const modelVerifyTool = defineTool({
41
46
  type: 'json',
42
47
  description: 'Optional object mapping BEFORE state ids to AFTER state ids. Omit for identity mapping (same state names).',
43
48
  },
49
+ check: {
50
+ type: 'string',
51
+ description: 'Print one check in full when the digest truncated it. Pass the check id the CHECKS line lists (for example A9); the per-check budget no longer applies, and an id this report does not carry is answered with the ids that it does.',
52
+ },
44
53
  reportView: {
45
54
  type: 'string',
46
55
  enum: ['digest', 'full'],
@@ -53,9 +62,14 @@ export const modelVerifyTool = defineTool({
53
62
  description: 'this plugin verification report with summary and per-check findings.',
54
63
  },
55
64
  render(args, value) {
56
- return [{ type: 'text' as const, text: renderReportText(value, { full: args.reportView === 'full' }) }]
65
+ return [{ type: 'text' as const, text: renderReportText(value, { full: args.reportView === 'full', check: args.check }) }]
66
+ },
67
+ // The card a UI shows, separated from the text the model reads.
68
+ presentationMeta(_args, value) {
69
+ return reportCard(value, MODEL_VERIFY_TOOL_NAME) as unknown as JsonValue
57
70
  },
58
71
  },
72
+ presentResult: reportResultView(MODEL_VERIFY_TOOL_NAME),
59
73
  timeoutMs: 10_000,
60
74
  isConcurrencySafe: () => true,
61
75
  async execute(args) {
@@ -81,7 +95,8 @@ export const modelVerifyTool = defineTool({
81
95
  )
82
96
  }
83
97
  }
84
- return runVerification(args.model, {
98
+ const model = oneOf(args.model, args.modelPath, 'model', 'modelPath', 'VERIFY_OPTION_INVALID')
99
+ return runVerification(model, {
85
100
  maxStates: args.maxStates,
86
101
  maxPermutationEvents: args.maxPermutationEvents,
87
102
  beforeModel: args.beforeModel,