archsight 0.1.4 → 0.2.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 (190) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +5 -4
  3. data/README.md +44 -59
  4. data/chart/archsight/Chart.yaml +6 -0
  5. data/chart/archsight/README.md +3 -0
  6. data/chart/archsight/templates/NOTES.txt +22 -0
  7. data/chart/archsight/templates/_helpers.tpl +62 -0
  8. data/chart/archsight/templates/deployment.yaml +114 -0
  9. data/chart/archsight/templates/ingress.yaml +56 -0
  10. data/chart/archsight/templates/resources-configmap.yaml +10 -0
  11. data/chart/archsight/templates/resources-pvc.yaml +23 -0
  12. data/chart/archsight/templates/service.yaml +15 -0
  13. data/chart/archsight/templates/serviceaccount.yaml +12 -0
  14. data/chart/archsight/values.yaml +162 -0
  15. data/docs/architecture.md +39 -0
  16. data/docs/docker.md +49 -0
  17. data/{lib/archsight/web/doc → docs}/import.md +10 -2
  18. data/{lib/archsight/web/doc → docs}/index.md.erb +3 -1
  19. data/docs/kubernetes.md +149 -0
  20. data/docs/licenses.md +307 -0
  21. data/lib/archsight/analysis/executor.rb +0 -10
  22. data/lib/archsight/annotations/annotation.rb +85 -36
  23. data/lib/archsight/annotations/architecture_annotations.rb +1 -34
  24. data/lib/archsight/annotations/computed.rb +1 -1
  25. data/lib/archsight/annotations/generated_annotations.rb +6 -3
  26. data/lib/archsight/annotations/git_annotations.rb +8 -4
  27. data/lib/archsight/annotations/interface_annotations.rb +35 -0
  28. data/lib/archsight/cli.rb +4 -2
  29. data/lib/archsight/editor/content_hasher.rb +37 -0
  30. data/lib/archsight/editor/file_writer.rb +79 -0
  31. data/lib/archsight/editor.rb +237 -0
  32. data/lib/archsight/graph.rb +1 -51
  33. data/lib/archsight/helpers.rb +0 -20
  34. data/lib/archsight/import/handlers/github.rb +16 -6
  35. data/lib/archsight/import/handlers/gitlab.rb +28 -10
  36. data/lib/archsight/import/handlers/repository.rb +56 -6
  37. data/lib/archsight/import/handlers/rest_api.rb +13 -1
  38. data/lib/archsight/import/license_analyzer.rb +650 -0
  39. data/lib/archsight/import/team_matcher.rb +111 -61
  40. data/lib/archsight/linter.rb +1 -1
  41. data/lib/archsight/mcp/base.rb +11 -0
  42. data/lib/archsight/mcp/execute_analysis_tool.rb +100 -0
  43. data/lib/archsight/mcp.rb +1 -0
  44. data/lib/archsight/renderer.rb +4 -4
  45. data/lib/archsight/resources/analysis.rb +1 -17
  46. data/lib/archsight/resources/application_interface.rb +1 -5
  47. data/lib/archsight/resources/base.rb +14 -14
  48. data/lib/archsight/resources/business_actor.rb +18 -3
  49. data/lib/archsight/resources/technology_artifact.rb +48 -0
  50. data/lib/archsight/resources/technology_interface.rb +1 -1
  51. data/lib/archsight/resources/technology_service.rb +5 -0
  52. data/lib/archsight/version.rb +1 -1
  53. data/lib/archsight/web/api/docs.rb +37 -2
  54. data/lib/archsight/web/api/json_helpers.rb +79 -13
  55. data/lib/archsight/web/api/openapi/spec.yaml +699 -0
  56. data/lib/archsight/web/api/routes.rb +23 -0
  57. data/lib/archsight/web/application.rb +48 -128
  58. data/lib/archsight/web/editor/form_builder.rb +100 -0
  59. data/lib/archsight/web/editor/helpers.rb +150 -0
  60. data/lib/archsight/web/editor/routes.rb +166 -0
  61. data/lib/archsight/web/public/vue/ApiDocsPage-B1RqTNqh.js +1 -0
  62. data/lib/archsight/web/public/vue/ApiDocsPage-DhNTOH4o.css +1 -0
  63. data/lib/archsight/web/public/vue/DocPage-DzwBgBd4.js +1 -0
  64. data/lib/archsight/web/public/vue/EditorPage-D_miHSv4.js +34 -0
  65. data/lib/archsight/web/public/vue/EditorPage-Dq0MuTnp.css +1 -0
  66. data/lib/archsight/web/public/vue/ErrorPage-CQQtPey3.js +2 -0
  67. data/lib/archsight/web/public/vue/ErrorPage-CwPT3JUr.css +1 -0
  68. data/lib/archsight/web/public/vue/GraphView-DRcIqAiR.css +1 -0
  69. data/lib/archsight/web/public/vue/GraphView-T9jFH_qg.js +1 -0
  70. data/lib/archsight/web/public/vue/InstanceRouter-1Sm-CRhf.js +2 -0
  71. data/lib/archsight/web/public/vue/InstanceRouter-BJkDRXZY.css +1 -0
  72. data/lib/archsight/web/public/vue/KindList-JA_L_-Cz.js +1 -0
  73. data/lib/archsight/web/public/vue/ResourceList-8iqavWdg.js +1 -0
  74. data/lib/archsight/web/public/vue/ResourceList-DP-z-j71.css +1 -0
  75. data/lib/archsight/web/public/vue/SearchResults-BGHbg48-.css +1 -0
  76. data/lib/archsight/web/public/vue/SearchResults-BdgFeHcm.js +1 -0
  77. data/lib/archsight/web/public/vue/_basePickBy-CVgieyx-.js +1 -0
  78. data/lib/archsight/web/public/vue/_baseUniq-BNfrOSaP.js +1 -0
  79. data/lib/archsight/web/public/vue/architectureDiagram-VXUJARFQ-CJXNpTr5.js +36 -0
  80. data/lib/archsight/web/public/vue/blockDiagram-VD42YOAC-B5488Hes.js +122 -0
  81. data/lib/archsight/web/public/vue/c4Diagram-YG6GDRKO-eYY3hprM.js +10 -0
  82. data/lib/archsight/web/public/vue/chunk-4BX2VUAB-ZoXeL4D1.js +1 -0
  83. data/lib/archsight/web/public/vue/chunk-55IACEB6-rNtQYnu_.js +1 -0
  84. data/lib/archsight/web/public/vue/chunk-B4BG7PRW-DolAeVV9.js +165 -0
  85. data/lib/archsight/web/public/vue/chunk-DI55MBZ5-DnN0f_hj.js +220 -0
  86. data/lib/archsight/web/public/vue/chunk-FMBD7UC4-BQWOCMuR.js +15 -0
  87. data/lib/archsight/web/public/vue/chunk-QN33PNHL-DId301Kb.js +1 -0
  88. data/lib/archsight/web/public/vue/chunk-QZHKN3VN-xbY0NLgv.js +1 -0
  89. data/lib/archsight/web/public/vue/chunk-TZMSLE5B-CgF9_37b.js +1 -0
  90. data/lib/archsight/web/public/vue/classDiagram-2ON5EDUG-jGlvI-Za.js +1 -0
  91. data/lib/archsight/web/public/vue/classDiagram-v2-WZHVMYZB-jGlvI-Za.js +1 -0
  92. data/lib/archsight/web/public/vue/clone-6iRPe1-W.js +1 -0
  93. data/lib/archsight/web/public/vue/cose-bilkent-S5V4N54A-CB9Zfu50.js +1 -0
  94. data/lib/archsight/web/public/vue/cytoscape.esm-5J0xJHOV.js +321 -0
  95. data/lib/archsight/web/public/vue/dagre-6UL2VRFP-BqkmE-LI.js +4 -0
  96. data/lib/archsight/web/public/vue/diagram-PSM6KHXK-CKBfqtw3.js +24 -0
  97. data/lib/archsight/web/public/vue/diagram-QEK2KX5R-B78rOlvK.js +43 -0
  98. data/lib/archsight/web/public/vue/diagram-S2PKOQOG-BlXC6Cia.js +24 -0
  99. data/lib/archsight/web/public/vue/erDiagram-Q2GNP2WA-BnliyziJ.js +60 -0
  100. data/lib/archsight/web/public/vue/flowDiagram-NV44I4VS-wuqPowTd.js +162 -0
  101. data/lib/archsight/web/public/vue/ganttDiagram-JELNMOA3-GSffAIH3.js +267 -0
  102. data/lib/archsight/web/public/vue/gitGraphDiagram-V2S2FVAM-OA7VyugW.js +65 -0
  103. data/lib/archsight/web/public/vue/graph-BXHAtA0S.js +1 -0
  104. data/lib/archsight/web/public/vue/graphviz-CJms5bxZ.js +13 -0
  105. data/lib/archsight/web/public/vue/index-DsEsN0_K.js +2 -0
  106. data/lib/archsight/web/public/vue/index-Tiu4C-Sb.css +1 -0
  107. data/lib/archsight/web/public/vue/infoDiagram-HS3SLOUP-nlVe2qgv.js +2 -0
  108. data/lib/archsight/web/public/vue/journeyDiagram-XKPGCS4Q-CtTIcKwf.js +139 -0
  109. data/lib/archsight/web/public/vue/kanban-definition-3W4ZIXB7-837KX0sW.js +89 -0
  110. data/lib/archsight/web/public/vue/katex-C-M49wc6.js +261 -0
  111. data/lib/archsight/web/public/vue/layout-DtE0QdL6.js +1 -0
  112. data/lib/archsight/web/public/vue/mermaid-DpPHPFQh.js +250 -0
  113. data/lib/archsight/web/public/vue/mindmap-definition-VGOIOE7T-9gLF2AoY.js +68 -0
  114. data/lib/archsight/web/public/vue/pieDiagram-ADFJNKIX-CyCNgw3u.js +30 -0
  115. data/lib/archsight/web/public/vue/quadrantDiagram-AYHSOK5B-CkPh8g02.js +7 -0
  116. data/lib/archsight/web/public/vue/requirementDiagram-UZGBJVZJ-Dkt6OSlY.js +64 -0
  117. data/lib/archsight/web/public/vue/sankeyDiagram-TZEHDZUN-BqprTk8x.js +10 -0
  118. data/lib/archsight/web/public/vue/sequenceDiagram-WL72ISMW-CTmTe1FQ.js +145 -0
  119. data/lib/archsight/web/public/vue/stateDiagram-FKZM4ZOC-CphqmkEU.js +1 -0
  120. data/lib/archsight/web/public/vue/stateDiagram-v2-4FDKWEC3-CxaDW5sW.js +1 -0
  121. data/lib/archsight/web/public/vue/timeline-definition-IT6M3QCI-CSQUZkyE.js +61 -0
  122. data/lib/archsight/web/public/vue/treemap-GDKQZRPO-DTojm7Yr.js +162 -0
  123. data/lib/archsight/web/public/{css/graph.css → vue/useGraphviz-A5s4h76R.js} +2 -1
  124. data/lib/archsight/web/public/vue/useHighlight-C6Kb5G3l.js +10 -0
  125. data/lib/archsight/web/public/vue/useMermaid-DqxTrLRB.js +1 -0
  126. data/lib/archsight/web/public/vue/usePanZoom-BybZ_rfh.js +11 -0
  127. data/lib/archsight/web/public/vue/xychartDiagram-PRI3JC2R-B1ZJZtDC.js +7 -0
  128. data/lib/archsight/web/public/vue.html +15 -0
  129. data/media/artifact.jpg +0 -0
  130. data/media/service.jpg +0 -0
  131. metadata +104 -77
  132. data/lib/archsight/web/public/css/artifact.css +0 -995
  133. data/lib/archsight/web/public/css/base.css +0 -201
  134. data/lib/archsight/web/public/css/highlight.min.css +0 -10
  135. data/lib/archsight/web/public/css/iconoir.css +0 -22
  136. data/lib/archsight/web/public/css/instance.css +0 -818
  137. data/lib/archsight/web/public/css/layout.css +0 -421
  138. data/lib/archsight/web/public/css/mermaid-layers.css +0 -188
  139. data/lib/archsight/web/public/css/pico.min.css +0 -4
  140. data/lib/archsight/web/public/img/archimate.png +0 -0
  141. data/lib/archsight/web/public/img/togaf-high-level.png +0 -0
  142. data/lib/archsight/web/public/js/graph-zoom.js +0 -18
  143. data/lib/archsight/web/public/js/highlight.min.js +0 -3899
  144. data/lib/archsight/web/public/js/htmx.min.js +0 -1
  145. data/lib/archsight/web/public/js/mermaid-init.js +0 -88
  146. data/lib/archsight/web/public/js/mermaid.min.js +0 -2811
  147. data/lib/archsight/web/public/js/sparkline.js +0 -42
  148. data/lib/archsight/web/public/js/svg-pan-zoom.min.js +0 -3
  149. data/lib/archsight/web/public/js/svg-zoom-controls.js +0 -93
  150. data/lib/archsight/web/views/api_docs.erb +0 -19
  151. data/lib/archsight/web/views/index.haml +0 -12
  152. data/lib/archsight/web/views/partials/artifact/_activity.haml +0 -55
  153. data/lib/archsight/web/views/partials/artifact/_agentic.haml +0 -25
  154. data/lib/archsight/web/views/partials/artifact/_deployment.haml +0 -29
  155. data/lib/archsight/web/views/partials/artifact/_git_info.haml +0 -16
  156. data/lib/archsight/web/views/partials/artifact/_language_stats.haml +0 -53
  157. data/lib/archsight/web/views/partials/artifact/_links.haml +0 -24
  158. data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +0 -32
  159. data/lib/archsight/web/views/partials/artifact/_repositories.haml +0 -55
  160. data/lib/archsight/web/views/partials/artifact/_team.haml +0 -83
  161. data/lib/archsight/web/views/partials/artifact/_workflow.haml +0 -69
  162. data/lib/archsight/web/views/partials/components/_activity.haml +0 -37
  163. data/lib/archsight/web/views/partials/components/_git.haml +0 -17
  164. data/lib/archsight/web/views/partials/components/_jira.haml +0 -18
  165. data/lib/archsight/web/views/partials/components/_languages.haml +0 -29
  166. data/lib/archsight/web/views/partials/components/_owner.haml +0 -15
  167. data/lib/archsight/web/views/partials/components/_repositories.haml +0 -37
  168. data/lib/archsight/web/views/partials/components/_status.haml +0 -23
  169. data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +0 -74
  170. data/lib/archsight/web/views/partials/instance/_analysis_result.haml +0 -64
  171. data/lib/archsight/web/views/partials/instance/_detail.haml +0 -103
  172. data/lib/archsight/web/views/partials/instance/_graph.haml +0 -6
  173. data/lib/archsight/web/views/partials/instance/_import_detail.haml +0 -87
  174. data/lib/archsight/web/views/partials/instance/_list.haml +0 -84
  175. data/lib/archsight/web/views/partials/instance/_relations.haml +0 -43
  176. data/lib/archsight/web/views/partials/instance/_requirements.haml +0 -41
  177. data/lib/archsight/web/views/partials/instance/_view_detail.haml +0 -57
  178. data/lib/archsight/web/views/partials/layout/_content.haml +0 -44
  179. data/lib/archsight/web/views/partials/layout/_error.haml +0 -22
  180. data/lib/archsight/web/views/partials/layout/_head.haml +0 -24
  181. data/lib/archsight/web/views/partials/layout/_navigation.haml +0 -21
  182. data/lib/archsight/web/views/partials/layout/_sidebar.haml +0 -27
  183. data/lib/archsight/web/views/search.haml +0 -53
  184. /data/{lib/archsight/web/doc → docs}/archimate.md +0 -0
  185. /data/{lib/archsight/web/doc → docs}/computed_annotations.md +0 -0
  186. /data/{lib/archsight/web/doc → docs}/icons.md +0 -0
  187. /data/{lib/archsight/web/doc → docs}/modeling.md +0 -0
  188. /data/{lib/archsight/web/doc → docs}/search.md +0 -0
  189. /data/{lib/archsight/web/doc → docs}/togaf.md +0 -0
  190. /data/{lib/archsight/web/doc → docs}/tool.md +0 -0
@@ -15,11 +15,16 @@ class Archsight::Import::TeamMatcher
15
15
  # Teams to ignore when matching (bots, unknown, etc.)
16
16
  IGNORED_TEAMS = %w[Bot:Team No:Team Team:Unknown Team:Bot].freeze
17
17
 
18
- def initialize(database)
18
+ # Team annotation keys that contain member/lead information
19
+ TEAM_ANNOTATION_KEYS = %w[team/members team/lead].freeze
20
+
21
+ def initialize(database, corporate_affixes: [])
19
22
  @database = database
23
+ @corporate_affixes = corporate_affixes
20
24
  @teams = load_teams
21
25
  @email_to_team = build_email_index
22
26
  @name_to_team = build_name_index
27
+ @member_identities = build_member_identities
23
28
  end
24
29
 
25
30
  # Analyze top contributors and return team assignments
@@ -72,6 +77,12 @@ class Archsight::Import::TeamMatcher
72
77
  return team if team
73
78
  end
74
79
 
80
+ # Try corporate username pattern match (e.g. jsmith-ionos -> John Smith)
81
+ if name
82
+ team = pattern_match_username(name)
83
+ return team if team
84
+ end
85
+
75
86
  nil
76
87
  end
77
88
 
@@ -87,21 +98,22 @@ class Archsight::Import::TeamMatcher
87
98
  end
88
99
  end
89
100
 
90
- def build_email_index
91
- index = {}
92
-
101
+ # Yields (team_name, annotation_value) for each team/members and team/lead annotation
102
+ def each_team_annotation
93
103
  @teams.each do |team|
94
104
  team_name = team.name
95
-
96
- # Extract emails from team/members annotation
97
- members = team.annotations["team/members"]
98
- parse_email_list(members).each do |email|
99
- index[email.downcase] = team_name
105
+ TEAM_ANNOTATION_KEYS.each do |key|
106
+ value = team.annotations[key]
107
+ yield team_name, value if value && !value.empty?
100
108
  end
109
+ end
110
+ end
111
+
112
+ def build_email_index
113
+ index = {}
101
114
 
102
- # Extract email from team/lead annotation
103
- lead = team.annotations["team/lead"]
104
- parse_email_list(lead).each do |email|
115
+ each_team_annotation do |team_name, value|
116
+ parse_email_list(value).each do |email|
105
117
  index[email.downcase] = team_name
106
118
  end
107
119
  end
@@ -112,19 +124,8 @@ class Archsight::Import::TeamMatcher
112
124
  def build_name_index
113
125
  index = {}
114
126
 
115
- @teams.each do |team|
116
- team_name = team.name
117
-
118
- # Extract names from team/members annotation
119
- members = team.annotations["team/members"]
120
- parse_name_list(members).each do |name|
121
- normalized = normalize_name(name)
122
- index[normalized] = team_name if normalized && !normalized.empty?
123
- end
124
-
125
- # Extract name from team/lead annotation
126
- lead = team.annotations["team/lead"]
127
- parse_name_list(lead).each do |name|
127
+ each_team_annotation do |team_name, value|
128
+ parse_name_list(value).each do |name|
128
129
  normalized = normalize_name(name)
129
130
  index[normalized] = team_name if normalized && !normalized.empty?
130
131
  end
@@ -136,60 +137,109 @@ class Archsight::Import::TeamMatcher
136
137
  # Parse email addresses from team annotation
137
138
  # Supports formats: "Name <email>", "email", or comma/newline separated lists
138
139
  def parse_email_list(value)
139
- return [] if value.nil? || value.empty?
140
+ parse_name_email_pairs(value).filter_map { |pair| pair[:email] }
141
+ end
140
142
 
141
- emails = []
143
+ # Parse names from team annotation
144
+ # Supports formats: "Name <email>", "First Last", or comma/newline separated lists
145
+ def parse_name_list(value)
146
+ parse_name_email_pairs(value).filter_map { |pair| pair[:name] }
147
+ end
142
148
 
143
- # Split by comma or newline
144
- value.split(/[,\n]/).each do |entry|
145
- entry = entry.strip
146
- next if entry.empty?
149
+ # Normalize name for matching
150
+ # Converts to lowercase, removes extra spaces, handles common variations
151
+ def normalize_name(name)
152
+ return nil if name.nil?
147
153
 
148
- # Try to extract email from "Name <email>" format
149
- if (match = entry.match(/<([^>]+)>/))
150
- emails << match[1].strip
151
- elsif entry.include?("@")
152
- # Plain email address
153
- emails << entry
154
+ name.to_s
155
+ .downcase
156
+ .gsub(/\s+/, " ")
157
+ .strip
158
+ end
159
+
160
+ # Match git author name as corporate username pattern {first_initial}{lastname}[-affix] or [affix-]{first_initial}{lastname}
161
+ # e.g. "jsmith-ionos" or "ionos-jsmith" -> initial "j", lastname "smith" -> matches "John Smith"
162
+ #
163
+ # Limitations: Multi-part names (e.g. "Hans von Braun") only match by the last
164
+ # name part ("braun"), so "hvonbraun" would not match. Hyphenated lastnames
165
+ # (e.g. "Meyer-Schmidt") only match the full hyphenated form or the email lastname.
166
+ def pattern_match_username(name)
167
+ username = name.downcase.strip
168
+ return nil unless username.match?(/\A[a-z0-9][-a-z0-9]*\z/)
169
+ return nil if @corporate_affixes.empty?
170
+
171
+ @corporate_affixes.each do |affix|
172
+ clean = affix.delete_prefix("-").delete_suffix("-")
173
+ username = username.delete_suffix("-#{clean}")
174
+ username = username.delete_prefix("#{clean}-")
175
+ end
176
+ return nil if username.length < 3
177
+
178
+ initial = username[0]
179
+ lastname = username[1..]
180
+ return nil if lastname.length < 3
181
+
182
+ candidates = @member_identities.select do |member|
183
+ lastname_match = member[:lastname] == lastname || member[:email_lastname] == lastname
184
+ initial_match = member[:firstname]&.start_with?(initial)
185
+ lastname_match && initial_match
186
+ end
187
+
188
+ teams = candidates.map { |c| c[:team] }.uniq
189
+ return teams.first if teams.size == 1
190
+
191
+ nil
192
+ end
193
+
194
+ # Build identity records for corporate username pattern matching
195
+ def build_member_identities
196
+ identities = []
197
+
198
+ each_team_annotation do |team_name, value|
199
+ parse_name_email_pairs(value).each do |entry|
200
+ name_parts = entry[:name]&.downcase&.split(/\s+/)
201
+ next if name_parts.nil? || name_parts.size < 2
202
+
203
+ firstname = name_parts.first
204
+ lastname = name_parts.last
205
+
206
+ email_prefix = entry[:email]&.split("@")&.first
207
+ email_parts = email_prefix&.split(/[.-]/)
208
+ email_lastname = email_parts&.last&.downcase
209
+
210
+ identities << { team: team_name, firstname: firstname, lastname: lastname, email_lastname: email_lastname }
154
211
  end
155
212
  end
156
213
 
157
- emails
214
+ identities
158
215
  end
159
216
 
160
- # Parse names from team annotation
161
- # Supports formats: "Name <email>", "First Last", or comma/newline separated lists
162
- def parse_name_list(value)
217
+ # Parse name and email pairs from team annotation
218
+ # Returns array of { name:, email: } hashes from "Name <email>" format
219
+ def parse_name_email_pairs(value)
163
220
  return [] if value.nil? || value.empty?
164
221
 
165
- names = []
222
+ pairs = []
166
223
 
167
- # Split by comma or newline
168
224
  value.split(/[,\n]/).each do |entry|
169
225
  entry = entry.strip
170
226
  next if entry.empty?
171
227
 
172
- # Try to extract name from "Name <email>" format
173
- if (match = entry.match(/^([^<]+)</))
228
+ name = nil
229
+ email = nil
230
+
231
+ if (match = entry.match(/^([^<]+)<([^>]+)>/))
174
232
  name = match[1].strip
175
- names << name unless name.empty?
176
- elsif !entry.include?("@")
177
- # Plain name (no email)
178
- names << entry
233
+ email = match[2].strip
234
+ elsif entry.include?("@")
235
+ email = entry.strip
236
+ else
237
+ name = entry.strip
179
238
  end
180
- end
181
-
182
- names
183
- end
184
239
 
185
- # Normalize name for matching
186
- # Converts to lowercase, removes extra spaces, handles common variations
187
- def normalize_name(name)
188
- return nil if name.nil?
240
+ pairs << { name: name, email: email } if name || email
241
+ end
189
242
 
190
- name.to_s
191
- .downcase
192
- .gsub(/\s+/, " ")
193
- .strip
243
+ pairs
194
244
  end
195
245
  end
@@ -4,7 +4,7 @@ require "kramdown"
4
4
 
5
5
  module Archsight
6
6
  class Linter
7
- # Available view components (partials in views/partials/components/)
7
+ # Valid @component references in View annotations
8
8
  VALID_COMPONENTS = %w[activity git jira languages owner repositories status].freeze
9
9
 
10
10
  def initialize(database)
@@ -27,6 +27,17 @@ module Archsight::MCP
27
27
  result
28
28
  end
29
29
 
30
+ def annotations_summary(resource, omit_kind: false)
31
+ result = {
32
+ name: resource.name,
33
+ metadata: {
34
+ annotations: resource.annotations
35
+ }
36
+ }
37
+ result[:kind] = resource.class.to_s.split("::").last unless omit_kind
38
+ result
39
+ end
40
+
30
41
  def extract_description(resource)
31
42
  description = resource.annotations["architecture/description"]
32
43
  return "No description" if description.nil?
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Archsight::MCP::ExecuteAnalysisTool < FastMcp::Tool
6
+ tool_name "execute_analysis"
7
+
8
+ description <<~DESC.gsub("\n", " ").strip
9
+ Discover and execute Analysis resources that validate architecture data and produce reports.
10
+
11
+ TWO MODES OF OPERATION:
12
+
13
+ 1. LIST MODE (no name): Returns all available Analysis resources with name, description,
14
+ handler, and timeout. Use this to discover what analyses exist before running them.
15
+
16
+ 2. EXECUTE MODE (name provided): Runs the specified Analysis script in a sandboxed environment
17
+ and returns the results as markdown. The output includes structured findings like tables,
18
+ lists, warnings, and errors generated by the script.
19
+ DESC
20
+
21
+ arguments do
22
+ optional(:name).filled(:string).description(
23
+ "Analysis name to execute (e.g., 'Analysis:Service:TeamOwnership'). " \
24
+ "Omit to list all available analyses."
25
+ )
26
+ optional(:verbose).filled(:bool).description(
27
+ "When true, tables and lists in output are not truncated. Default: false."
28
+ )
29
+ end
30
+
31
+ def call(name: nil, verbose: false)
32
+ if name.nil?
33
+ list_analyses
34
+ else
35
+ execute_analysis(name, verbose: verbose)
36
+ end
37
+ rescue StandardError => e
38
+ error_response(e.message)
39
+ end
40
+
41
+ private
42
+
43
+ def list_analyses
44
+ db = Archsight::MCP.db
45
+ analyses = db.instances_by_kind("Analysis").values
46
+
47
+ resources = analyses.map do |analysis|
48
+ {
49
+ name: analysis.name,
50
+ description: analysis.annotations["analysis/description"] || analysis.annotations["architecture/description"],
51
+ handler: analysis.annotations["analysis/handler"] || "ruby",
52
+ timeout: analysis.annotations["analysis/timeout"] || "30s"
53
+ }
54
+ end
55
+
56
+ JSON.pretty_generate(
57
+ total: resources.size,
58
+ analyses: resources
59
+ )
60
+ end
61
+
62
+ def execute_analysis(name, verbose: false)
63
+ require "archsight/analysis"
64
+
65
+ db = Archsight::MCP.db
66
+ analysis = db.instance_by_kind("Analysis", name)
67
+ return error_response("Analysis not found: #{name}") unless analysis
68
+
69
+ executor = Archsight::Analysis::Executor.new(db)
70
+ result = executor.execute(analysis)
71
+
72
+ if result.success?
73
+ JSON.pretty_generate(
74
+ name: result.name,
75
+ success: true,
76
+ duration: result.duration,
77
+ output: result.to_markdown(verbose: verbose),
78
+ has_findings: result.has_findings?,
79
+ warning_count: result.warning_count,
80
+ error_count: result.error_count
81
+ )
82
+ else
83
+ response = {
84
+ name: result.name,
85
+ success: false,
86
+ error: result.error
87
+ }
88
+ partial = result.to_markdown(verbose: verbose)
89
+ response[:partial_output] = partial unless partial.empty?
90
+ JSON.pretty_generate(response)
91
+ end
92
+ end
93
+
94
+ def error_response(message)
95
+ JSON.pretty_generate(
96
+ error: "Error",
97
+ message: message
98
+ )
99
+ end
100
+ end
data/lib/archsight/mcp.rb CHANGED
@@ -4,3 +4,4 @@ require_relative "mcp/base"
4
4
  require_relative "mcp/query_tool"
5
5
  require_relative "mcp/analyze_resource_tool"
6
6
  require_relative "mcp/resource_doc_tool"
7
+ require_relative "mcp/execute_analysis_tool"
@@ -49,7 +49,7 @@ module Archsight
49
49
  graph.edge gname(a_inst), gname(b_inst), label: label, fontname: "#{FONT} italic"
50
50
  end
51
51
 
52
- def create_graph_all(db, method = :draw_dot, root_kinds: nil, max_depth: 3, allowed_kinds: nil)
52
+ def create_graph_all(db, root_kinds: nil, max_depth: 3, allowed_kinds: nil)
53
53
  root_kinds ||= [Archsight::Resources["BusinessProduct"], Archsight::Resources["BusinessProcess"]]
54
54
 
55
55
  # Default allowed kinds for overview: Products, Processes, Services, and Teams
@@ -61,7 +61,7 @@ module Archsight
61
61
  ]
62
62
  allowed_kinds_set = allowed_kinds.to_set
63
63
 
64
- Archsight::Graphvis.new("all").send(method) do |g|
64
+ Archsight::Graphvis.new("all").draw_dot do |g|
65
65
  nodes = {} # Track visited nodes
66
66
  edges = {} # Track created edges
67
67
 
@@ -104,11 +104,11 @@ module Archsight
104
104
  end
105
105
  end
106
106
 
107
- def create_graph_one(db, klass_pat, name_pat, method = :draw_dot)
107
+ def create_graph_one(db, klass_pat, name_pat)
108
108
  name = "#{klass_pat}:#{name_pat}"
109
109
  nodes = {}
110
110
  edges = {}
111
- Archsight::Graphvis.new(name).send(method) do |g|
111
+ Archsight::Graphvis.new(name).draw_dot do |g|
112
112
  klass = Archsight::Resources[klass_pat] || raise("kind #{klass_pat} unknown")
113
113
  instances = db.instances[klass]
114
114
  inst = instances[name_pat] || raise("name #{name_pat} for kind #{klass_pat} not found")
@@ -51,6 +51,7 @@ class Archsight::Resources::Analysis < Archsight::Resources::Base
51
51
  annotation "analysis/script",
52
52
  description: "Ruby script to execute in sandboxed environment",
53
53
  title: "Script",
54
+ format: :ruby,
54
55
  sidebar: false
55
56
 
56
57
  # Description
@@ -63,23 +64,6 @@ class Archsight::Resources::Analysis < Archsight::Resources::Base
63
64
  description: "Maximum execution time (e.g., '30s', '5m')",
64
65
  title: "Timeout"
65
66
 
66
- # Output configuration
67
- annotation "analysis/output",
68
- description: "Output mode for results",
69
- title: "Output",
70
- enum: %w[console file]
71
-
72
- annotation "analysis/outputPath",
73
- description: "File path for output (when output mode is 'file')",
74
- title: "Output Path",
75
- sidebar: false
76
-
77
- # Enabled flag
78
- annotation "analysis/enabled",
79
- description: "Whether this analysis is enabled",
80
- title: "Enabled",
81
- enum: %w[true false]
82
-
83
67
  # Pattern annotation for custom configuration
84
68
  annotation "analysis/config/*",
85
69
  description: "Custom configuration values for the analysis script",
@@ -2,7 +2,7 @@
2
2
 
3
3
  # ApplicationInterface between ApplicationComponent
4
4
  class Archsight::Resources::ApplicationInterface < Archsight::Resources::Base
5
- include_annotations :git, :architecture, :generated
5
+ include_annotations :git, :architecture, :interface, :generated
6
6
 
7
7
  description <<~MD
8
8
  Represents a point of access where application services are made available.
@@ -39,10 +39,6 @@ class Archsight::Resources::ApplicationInterface < Archsight::Resources::Base
39
39
  description: "API authentication method",
40
40
  title: "API Authentication Method",
41
41
  enum: ["none", "hard coded", "token", "oidc"]
42
- annotation "api/authenticationProvider",
43
- description: "API authentication provider",
44
- title: "API Authentication Provider",
45
- enum: %w[eiam cloud custom]
46
42
  annotation "api/authorization",
47
43
  description: "API authorization mechanism",
48
44
  title: "API Authorization",
@@ -15,7 +15,7 @@ module Archsight
15
15
  end
16
16
 
17
17
  def self.relation(verb, kind, klass_name)
18
- @relations ||= []
18
+ @relations ||= [] #: Array[[Symbol, Symbol, String]]
19
19
  @relations << [verb, kind, klass_name]
20
20
  end
21
21
 
@@ -25,10 +25,10 @@ module Archsight
25
25
 
26
26
  # Define an annotation using the Annotation class
27
27
  def self.annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil, sidebar: true,
28
- type: nil, list: false)
29
- @annotations ||= []
28
+ type: nil, list: false, editor: true)
29
+ @annotations ||= [] #: Array[Archsight::Annotations::Annotation]
30
30
  options = { description: description, filter: filter, title: title, format: format, enum: enum,
31
- sidebar: sidebar, type: type, list: list }
31
+ sidebar: sidebar, type: type, list: list, editor: editor }
32
32
  @annotations << Archsight::Annotations::Annotation.new(key, options)
33
33
  end
34
34
 
@@ -51,15 +51,15 @@ module Archsight
51
51
  # @param list [Boolean] Whether values are lists (default false)
52
52
  # @yield Block that computes the annotation value, evaluated in Evaluator context
53
53
  def self.computed_annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil,
54
- sidebar: false, type: nil, list: false, &)
54
+ sidebar: false, type: nil, list: false, editor: true, &)
55
55
  require_relative "../annotations/computed"
56
- @computed_annotations ||= []
56
+ @computed_annotations ||= [] #: Array[Archsight::Annotations::Computed]
57
57
  @computed_annotations << Archsight::Annotations::Computed.new(key, description: description, type: type, &)
58
58
 
59
59
  # Also register as a regular annotation so it passes validation and is recognized
60
- @annotations ||= []
60
+ @annotations ||= [] #: Array[Archsight::Annotations::Annotation]
61
61
  options = { description: description, filter: filter, title: title, format: format, enum: enum,
62
- sidebar: sidebar, type: type, list: list }
62
+ sidebar: sidebar, type: type, list: list, editor: editor }
63
63
  @annotations << Archsight::Annotations::Annotation.new(key, options)
64
64
  end
65
65
 
@@ -180,11 +180,11 @@ module Archsight
180
180
  # @param key [String] The annotation key
181
181
  # @param value [Object] The computed value
182
182
  def set_computed_annotation(key, value)
183
- @computed_values ||= {}
183
+ @computed_values ||= {} #: Hash[String, untyped]
184
184
  @computed_values[key] = value
185
185
  # Write to annotations hash for query compatibility
186
186
  @raw["metadata"] ||= {}
187
- @raw["metadata"]["annotations"] ||= {}
187
+ @raw["metadata"]["annotations"] = @raw["metadata"]["annotations"] || {} #: Hash[String, String]
188
188
  @raw["metadata"]["annotations"][key] = value
189
189
  end
190
190
 
@@ -241,13 +241,13 @@ module Archsight
241
241
  # Get references grouped by kind and verb for display (incoming)
242
242
  # Returns: { "Kind" => { "verb" => [instances...] } }
243
243
  def references_grouped
244
- grouped = {}
244
+ grouped = {} #: Hash[String, Hash[untyped, Array[Base]]]
245
245
  @references.each do |ref|
246
246
  inst = ref[:instance]
247
247
  verb = ref[:verb]
248
248
  kind = inst.klass
249
249
  grouped[kind] ||= {}
250
- grouped[kind][verb] ||= []
250
+ grouped[kind][verb] ||= [] #: Array[Base]
251
251
  grouped[kind][verb] << inst
252
252
  end
253
253
  # Sort by kind name, then by verb name
@@ -257,7 +257,7 @@ module Archsight
257
257
  # Get outgoing relations grouped by verb and kind for display
258
258
  # Returns: { "verb" => { "Kind" => [instances...] } }
259
259
  def relations_grouped
260
- grouped = {}
260
+ grouped = {} #: Hash[String, Hash[String, Array[Base]]]
261
261
  spec.each do |verb, kinds|
262
262
  next unless kinds.is_a?(Hash)
263
263
 
@@ -267,7 +267,7 @@ module Archsight
267
267
  instances.each do |inst|
268
268
  kind = inst.klass
269
269
  grouped[verb] ||= {}
270
- grouped[verb][kind] ||= []
270
+ grouped[verb][kind] ||= [] #: Array[Base]
271
271
  grouped[verb][kind] << inst
272
272
  end
273
273
  end
@@ -43,7 +43,7 @@ class Archsight::Resources::BusinessActor < Archsight::Resources::Base
43
43
  title: "Team Members",
44
44
  sidebar: false,
45
45
  filter: :list,
46
- format: :tag_list,
46
+ format: :multiline,
47
47
  type: Archsight::Annotations::EmailRecipient
48
48
 
49
49
  annotation "team/jira",
@@ -181,7 +181,7 @@ class Archsight::Resources::BusinessActor < Archsight::Resources::Base
181
181
 
182
182
  computed_annotation "jira/projectUrl",
183
183
  title: "Jira Board",
184
- description: "Link to Jira board (computed from team/jira)",
184
+ description: "Link to Jira board (computed from team/jira and Import host config)",
185
185
  format: :link do
186
186
  jira_key = @instance.annotations["team/jira"]
187
187
  next nil if jira_key.nil? || jira_key.empty?
@@ -190,6 +190,21 @@ class Archsight::Resources::BusinessActor < Archsight::Resources::Base
190
190
  primary_key = jira_key.split(/[,\n]/).first&.strip
191
191
  next nil if primary_key.nil? || primary_key.empty?
192
192
 
193
- "https://jira.example.com/projects/#{primary_key}/issues"
193
+ # Find Jira host from Import resources with a Jira handler
194
+ jira_host = nil
195
+ imports = @database.instances_by_kind("Import")
196
+ imports.each_value do |imp|
197
+ handler = imp.annotations["import/handler"]
198
+ next unless handler&.match?(/\AJira/i)
199
+
200
+ host = imp.annotations["import/config/host"]
201
+ if host && !host.empty?
202
+ jira_host = host
203
+ break
204
+ end
205
+ end
206
+ next nil unless jira_host
207
+
208
+ "https://#{jira_host}/projects/#{primary_key}/issues"
194
209
  end
195
210
  end
@@ -158,6 +158,54 @@ class Archsight::Resources::TechnologyArtifact < Archsight::Resources::Base
158
158
  title: "Deployment Container Privileges",
159
159
  enum: %w[unprivileged privileged]
160
160
 
161
+ # License information
162
+ annotation "license/spdx",
163
+ description: "SPDX license identifier",
164
+ title: "License",
165
+ filter: :word,
166
+ sidebar: false,
167
+ enum: %w[Apache-2.0 MIT BSD-3-Clause BSD-2-Clause GPL-3.0 GPL-2.0 LGPL-3.0
168
+ LGPL-2.1 MPL-2.0 ISC AGPL-3.0 Unlicense CC0-1.0 BSL-1.0 EUPL-1.2
169
+ 0BSD CDDL-1.0 Ruby NOASSERTION proprietary unknown]
170
+ annotation "license/file",
171
+ description: "License file path",
172
+ title: "License File",
173
+ sidebar: false
174
+ annotation "license/category",
175
+ description: "License category",
176
+ title: "License Category",
177
+ filter: :word,
178
+ enum: %w[permissive copyleft weak-copyleft proprietary unknown]
179
+ annotation "license/dependencies/count",
180
+ description: "Total dependencies",
181
+ title: "Dependencies",
182
+ type: Integer
183
+ annotation "license/dependencies/ecosystems",
184
+ description: "Detected dependency ecosystems",
185
+ title: "Ecosystems",
186
+ filter: :list,
187
+ sidebar: false
188
+ annotation "license/dependencies/licenses",
189
+ description: "Dependency license types",
190
+ title: "Dependency Licenses",
191
+ filter: :list,
192
+ sidebar: false
193
+ annotation "license/dependencies/copyleft",
194
+ description: "Copyleft dependencies present",
195
+ title: "Copyleft Dependencies",
196
+ filter: :word,
197
+ sidebar: false,
198
+ enum: %w[true false unknown]
199
+ annotation "license/dependencies/risk",
200
+ description: "License risk level",
201
+ title: "License Risk",
202
+ filter: :word,
203
+ sidebar: false,
204
+ enum: %w[low weak-copyleft copyleft unknown]
205
+ annotation "license/dependencies/*/count",
206
+ description: "Dependencies per license type",
207
+ sidebar: false
208
+
161
209
  # Contributor metrics (unique counts, stored during import)
162
210
  annotation "activity/contributors/6m",
163
211
  title: "Contributors (6 months)",
@@ -2,7 +2,7 @@
2
2
 
3
3
  # TechnologyInterface is the backing of an applicationInterface
4
4
  class Archsight::Resources::TechnologyInterface < Archsight::Resources::Base
5
- include_annotations :git, :architecture
5
+ include_annotations :git, :architecture, :interface
6
6
 
7
7
  description <<~MD
8
8
  Represents a point of access where technology services are made available.
@@ -30,6 +30,11 @@ class Archsight::Resources::TechnologyService < Archsight::Resources::Base
30
30
  icon "cloud"
31
31
  layer "technology"
32
32
 
33
+ annotation "architecture/applicationSets",
34
+ description: "Related ArgoCD ApplicationSets",
35
+ title: "ApplicationSets",
36
+ format: :markdown
37
+
33
38
  relation :suppliedBy, :technologyComponents, :TechnologySystemSoftware
34
39
  relation :servedBy, :businessActors, :BusinessActor
35
40
  end
@@ -4,5 +4,5 @@
4
4
  # Do not edit manually.
5
5
 
6
6
  module Archsight
7
- VERSION = "0.1.4"
7
+ VERSION = "0.2.0"
8
8
  end