archsight 0.1.2 → 0.1.3

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +26 -5
  3. data/lib/archsight/analysis/executor.rb +112 -0
  4. data/lib/archsight/analysis/result.rb +174 -0
  5. data/lib/archsight/analysis/sandbox.rb +319 -0
  6. data/lib/archsight/analysis.rb +11 -0
  7. data/lib/archsight/annotations/architecture_annotations.rb +2 -2
  8. data/lib/archsight/cli.rb +163 -0
  9. data/lib/archsight/database.rb +6 -2
  10. data/lib/archsight/helpers/analysis_renderer.rb +83 -0
  11. data/lib/archsight/helpers/formatting.rb +95 -0
  12. data/lib/archsight/helpers.rb +20 -4
  13. data/lib/archsight/import/concurrent_progress.rb +341 -0
  14. data/lib/archsight/import/executor.rb +466 -0
  15. data/lib/archsight/import/git_analytics.rb +626 -0
  16. data/lib/archsight/import/handler.rb +263 -0
  17. data/lib/archsight/import/handlers/github.rb +161 -0
  18. data/lib/archsight/import/handlers/gitlab.rb +202 -0
  19. data/lib/archsight/import/handlers/jira_base.rb +189 -0
  20. data/lib/archsight/import/handlers/jira_discover.rb +161 -0
  21. data/lib/archsight/import/handlers/jira_metrics.rb +179 -0
  22. data/lib/archsight/import/handlers/openapi_schema_parser.rb +279 -0
  23. data/lib/archsight/import/handlers/repository.rb +439 -0
  24. data/lib/archsight/import/handlers/rest_api.rb +293 -0
  25. data/lib/archsight/import/handlers/rest_api_index.rb +183 -0
  26. data/lib/archsight/import/progress.rb +91 -0
  27. data/lib/archsight/import/registry.rb +54 -0
  28. data/lib/archsight/import/shared_file_writer.rb +67 -0
  29. data/lib/archsight/import/team_matcher.rb +195 -0
  30. data/lib/archsight/import.rb +14 -0
  31. data/lib/archsight/resources/analysis.rb +91 -0
  32. data/lib/archsight/resources/application_component.rb +2 -2
  33. data/lib/archsight/resources/application_service.rb +12 -12
  34. data/lib/archsight/resources/business_product.rb +12 -12
  35. data/lib/archsight/resources/data_object.rb +1 -1
  36. data/lib/archsight/resources/import.rb +79 -0
  37. data/lib/archsight/resources/technology_artifact.rb +23 -2
  38. data/lib/archsight/version.rb +1 -1
  39. data/lib/archsight/web/api/docs.rb +17 -0
  40. data/lib/archsight/web/api/json_helpers.rb +164 -0
  41. data/lib/archsight/web/api/openapi/spec.yaml +500 -0
  42. data/lib/archsight/web/api/routes.rb +101 -0
  43. data/lib/archsight/web/application.rb +66 -43
  44. data/lib/archsight/web/doc/import.md +458 -0
  45. data/lib/archsight/web/doc/index.md.erb +1 -0
  46. data/lib/archsight/web/public/css/artifact.css +10 -0
  47. data/lib/archsight/web/public/css/graph.css +14 -0
  48. data/lib/archsight/web/public/css/instance.css +489 -0
  49. data/lib/archsight/web/views/api_docs.erb +19 -0
  50. data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +14 -8
  51. data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +74 -0
  52. data/lib/archsight/web/views/partials/instance/_analysis_result.haml +64 -0
  53. data/lib/archsight/web/views/partials/instance/_detail.haml +7 -3
  54. data/lib/archsight/web/views/partials/instance/_import_detail.haml +87 -0
  55. data/lib/archsight/web/views/partials/instance/_relations.haml +4 -4
  56. data/lib/archsight/web/views/partials/layout/_content.haml +4 -0
  57. data/lib/archsight/web/views/partials/layout/_navigation.haml +6 -5
  58. metadata +78 -1
@@ -0,0 +1,341 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Manages concurrent progress output with slot-based display
4
+ #
5
+ # In TTY mode: Each slot gets its own line, updated in place using ANSI codes with colors
6
+ # In non-TTY mode: Each update prints on its own line with context prefix (no colors)
7
+ class Archsight::Import::ConcurrentProgress
8
+ # ANSI color codes
9
+ COLORS = {
10
+ reset: "\e[0m",
11
+ bold: "\e[1m",
12
+ dim: "\e[2m",
13
+ green: "\e[32m",
14
+ yellow: "\e[33m",
15
+ blue: "\e[34m",
16
+ magenta: "\e[35m",
17
+ cyan: "\e[36m",
18
+ red: "\e[31m"
19
+ }.freeze
20
+
21
+ # ANSI cursor control
22
+ CURSOR_HIDE = "\e[?25l"
23
+ CURSOR_SHOW = "\e[?25h"
24
+ CURSOR_SAVE = "\e[s"
25
+ CURSOR_RESTORE = "\e[u"
26
+
27
+ def initialize(max_slots:, output: $stdout)
28
+ @output = output
29
+ @tty = output.respond_to?(:tty?) && output.tty?
30
+ @max_slots = max_slots
31
+ @mutex = Mutex.new
32
+ @slots = {}
33
+ @slot_queue = Queue.new
34
+ @lines_printed = 0
35
+
36
+ # Overall progress tracking
37
+ @total_imports = 0
38
+ @completed_imports = 0
39
+ @has_overall_line = false
40
+ @start_time = nil
41
+
42
+ # Initialize slot queue
43
+ max_slots.times { |i| @slot_queue << i }
44
+ end
45
+
46
+ def tty?
47
+ @tty
48
+ end
49
+
50
+ # Initialize total number of imports for overall progress
51
+ def total=(total)
52
+ @mutex.synchronize do
53
+ @total_imports = total
54
+ @completed_imports = 0
55
+ @start_time = Time.now
56
+ if @tty && !@has_overall_line
57
+ # Save cursor position and hide cursor for clean display
58
+ @output.print CURSOR_SAVE
59
+ @output.print CURSOR_HIDE
60
+ @output.puts build_overall_line
61
+ @has_overall_line = true
62
+ @lines_printed += 1
63
+ end
64
+ end
65
+ end
66
+
67
+ # Update total without resetting completed count (for multi-stage imports)
68
+ def update_total(total)
69
+ @mutex.synchronize do
70
+ @total_imports = total
71
+ update_overall_line if @tty && @has_overall_line
72
+ end
73
+ end
74
+
75
+ # Increment completed count and update overall progress
76
+ def increment_completed
77
+ @mutex.synchronize do
78
+ @completed_imports += 1
79
+ update_overall_line if @tty && @has_overall_line
80
+ end
81
+ end
82
+
83
+ # Acquire a slot for a new task
84
+ # @return [SlotProgress] A progress reporter for this slot
85
+ def acquire_slot(context)
86
+ slot_id = @slot_queue.pop
87
+ slot = SlotProgress.new(self, slot_id, context)
88
+
89
+ @mutex.synchronize do
90
+ @slots[slot_id] = slot
91
+ # Slot lines start after the overall progress line (if present)
92
+ effective_line = @has_overall_line ? slot_id + 1 : slot_id
93
+ if @tty && effective_line >= @lines_printed
94
+ # Print empty lines to reserve space
95
+ (@lines_printed..effective_line).each { @output.puts }
96
+ @lines_printed = effective_line + 1
97
+ end
98
+ end
99
+
100
+ slot
101
+ end
102
+
103
+ # Release a slot back to the pool
104
+ def release_slot(slot_id)
105
+ @mutex.synchronize do
106
+ @slots.delete(slot_id)
107
+ end
108
+ @slot_queue << slot_id
109
+ end
110
+
111
+ # Update a specific slot's display
112
+ def update_slot(slot_id, context, message, current: nil, total: nil, color: nil)
113
+ line = build_line(context, message, current, total, color: color)
114
+
115
+ @mutex.synchronize do
116
+ if @tty
117
+ # Move cursor to slot line and update (account for overall progress line)
118
+ effective_line = @has_overall_line ? slot_id + 1 : slot_id
119
+ lines_up = @lines_printed - effective_line
120
+ @output.print "\e[#{lines_up}A" # Move up
121
+ @output.print "\e[2K" # Clear line
122
+ @output.print line
123
+ @output.print "\e[#{lines_up}B" # Move back down
124
+ @output.print "\r" # Return to start of line
125
+ @output.flush
126
+ else
127
+ @output.puts line
128
+ end
129
+ end
130
+ end
131
+
132
+ # Mark a slot as complete
133
+ def complete_slot(slot_id, context, message = nil)
134
+ @mutex.synchronize do
135
+ if @tty
136
+ effective_line = @has_overall_line ? slot_id + 1 : slot_id
137
+ lines_up = @lines_printed - effective_line
138
+ @output.print "\e[#{lines_up}A"
139
+ @output.print "\e[2K"
140
+ msg = message || "Done"
141
+ @output.print "#{COLORS[:bold]}#{context}#{COLORS[:reset]} - #{COLORS[:green]}#{msg}#{COLORS[:reset]}"
142
+ @output.print "\e[#{lines_up}B"
143
+ @output.print "\r"
144
+ @output.flush
145
+ elsif message
146
+ @output.puts "#{context} - #{message}"
147
+ end
148
+ end
149
+ end
150
+
151
+ # Report an error for a slot
152
+ def error_slot(slot_id, context, message)
153
+ safe_message = sanitize_message(message)
154
+ @mutex.synchronize do
155
+ if @tty
156
+ effective_line = @has_overall_line ? slot_id + 1 : slot_id
157
+ lines_up = @lines_printed - effective_line
158
+ @output.print "\e[#{lines_up}A"
159
+ @output.print "\e[2K"
160
+ @output.print "#{COLORS[:bold]}#{context}#{COLORS[:reset]} - #{COLORS[:red]}Error: #{safe_message}#{COLORS[:reset]}"
161
+ @output.print "\e[#{lines_up}B"
162
+ @output.print "\r"
163
+ @output.flush
164
+ else
165
+ @output.puts "#{context} - Error: #{safe_message}"
166
+ end
167
+ end
168
+ end
169
+
170
+ # Sanitize message to prevent breaking TTY display (remove newlines, truncate)
171
+ def sanitize_message(message)
172
+ return "" if message.nil?
173
+
174
+ # Replace newlines with spaces and collapse multiple spaces
175
+ clean = message.to_s.gsub(/[\r\n]+/, " ").gsub(/\s+/, " ").strip
176
+ # Truncate if too long
177
+ clean.length > 80 ? "#{clean[0, 77]}..." : clean
178
+ end
179
+
180
+ # Print a final summary (restores cursor and shows it)
181
+ # Note: Use finish_from_trap when called from a signal handler
182
+ def finish(message)
183
+ @mutex.synchronize do
184
+ finish_unsafe(message)
185
+ end
186
+ end
187
+
188
+ # Trap-safe version of finish (no mutex, safe to call from signal handlers)
189
+ def finish_from_trap(message)
190
+ finish_unsafe(message)
191
+ end
192
+
193
+ # Show interrupt message without clearing progress (called on first Ctrl-C)
194
+ # Note: Use interrupt_from_trap when called from a signal handler
195
+ def interrupt(message)
196
+ @mutex.synchronize do
197
+ interrupt_unsafe(message)
198
+ end
199
+ end
200
+
201
+ # Trap-safe version of interrupt (no mutex, safe to call from signal handlers)
202
+ def interrupt_from_trap(message)
203
+ interrupt_unsafe(message)
204
+ end
205
+
206
+ private
207
+
208
+ def finish_unsafe(message)
209
+ if @tty
210
+ # Restore cursor position and show cursor
211
+ @output.print CURSOR_RESTORE
212
+ # Clear from cursor to end of screen to remove progress lines
213
+ @output.print "\e[J"
214
+ @output.print CURSOR_SHOW
215
+ end
216
+ @output.puts message if message
217
+ end
218
+
219
+ def interrupt_unsafe(message)
220
+ if @tty
221
+ # Move to the line below progress and print message
222
+ @output.print "\n"
223
+ @output.print "\e[2K"
224
+ @output.print "#{COLORS[:yellow]}#{message}#{COLORS[:reset]}"
225
+ @output.print "\n"
226
+ @output.flush
227
+ # Increase lines printed to account for the interrupt message
228
+ @lines_printed += 2
229
+ else
230
+ @output.puts message
231
+ end
232
+ end
233
+
234
+ def build_overall_line
235
+ percentage = @total_imports.positive? ? ((@completed_imports.to_f / @total_imports) * 100).round : 0
236
+ progress_bar = build_progress_bar(percentage)
237
+ eta_str = calculate_eta_string
238
+ if @tty
239
+ "#{COLORS[:bold]}#{COLORS[:magenta]}Overall#{COLORS[:reset]} #{progress_bar} " \
240
+ "#{COLORS[:cyan]}#{percentage}%#{COLORS[:reset]} " \
241
+ "[#{@completed_imports}/#{@total_imports}] " \
242
+ "#{COLORS[:dim]}#{eta_str}#{COLORS[:reset]}"
243
+ else
244
+ "Overall: [#{@completed_imports}/#{@total_imports}] #{percentage}% #{eta_str}"
245
+ end
246
+ end
247
+
248
+ def calculate_eta_string
249
+ return "ETA: --:--" if @start_time.nil? || @completed_imports.zero?
250
+
251
+ elapsed = Time.now - @start_time
252
+ remaining = @total_imports - @completed_imports
253
+ rate = @completed_imports.to_f / elapsed
254
+ eta_seconds = (remaining / rate).round
255
+
256
+ if eta_seconds < 60
257
+ "ETA: #{eta_seconds}s"
258
+ elsif eta_seconds < 3600
259
+ minutes = eta_seconds / 60
260
+ seconds = eta_seconds % 60
261
+ format("ETA: %d:%02d", minutes, seconds)
262
+ else
263
+ hours = eta_seconds / 3600
264
+ minutes = (eta_seconds % 3600) / 60
265
+ format("ETA: %d:%02d:%02d", hours, minutes, eta_seconds % 60)
266
+ end
267
+ end
268
+
269
+ def build_progress_bar(percentage)
270
+ width = 20
271
+ filled = [(percentage / 5.0).round, width].min
272
+ filled = [filled, 0].max
273
+ empty = width - filled
274
+ bar = "#{"█" * filled}#{"░" * empty}"
275
+ "#{COLORS[:green]}#{bar}#{COLORS[:reset]}"
276
+ end
277
+
278
+ def update_overall_line
279
+ # Move to first line (overall progress line), update, move back
280
+ lines_up = @lines_printed
281
+ @output.print "\e[#{lines_up}A"
282
+ @output.print "\e[2K"
283
+ @output.print build_overall_line
284
+ @output.print "\e[#{lines_up}B"
285
+ @output.print "\r"
286
+ @output.flush
287
+ end
288
+
289
+ def build_line(context, message, current, total, color: nil)
290
+ parts = []
291
+ if @tty
292
+ parts << "#{COLORS[:bold]}#{context}#{COLORS[:reset]}"
293
+ parts << "#{COLORS[:cyan]}#{progress_indicator(current, total)}#{COLORS[:reset]}" if current && total
294
+ msg_color = color || COLORS[:reset]
295
+ parts << "#{msg_color}#{message}#{COLORS[:reset]}"
296
+ else
297
+ parts << context
298
+ parts << progress_indicator(current, total) if current && total
299
+ parts << message
300
+ end
301
+ parts.join(" - ")
302
+ end
303
+
304
+ def progress_indicator(current, total)
305
+ percentage = ((current.to_f / total) * 100).round
306
+ "[#{current}/#{total} #{percentage}%]"
307
+ end
308
+
309
+ # Individual slot progress reporter
310
+ class SlotProgress
311
+ attr_reader :slot_id
312
+ attr_accessor :context
313
+
314
+ def initialize(parent, slot_id, context)
315
+ @parent = parent
316
+ @slot_id = slot_id
317
+ @context = context
318
+ end
319
+
320
+ def update(message, current: nil, total: nil)
321
+ @parent.update_slot(@slot_id, @context, message, current: current, total: total)
322
+ end
323
+
324
+ def complete(message = nil)
325
+ @parent.complete_slot(@slot_id, @context, message)
326
+ end
327
+
328
+ def error(message)
329
+ @parent.error_slot(@slot_id, @context, message)
330
+ end
331
+
332
+ def warn(message)
333
+ # Warnings are shown inline with the current context (yellow in TTY mode)
334
+ @parent.update_slot(@slot_id, @context, "Warning: #{message}", color: COLORS[:yellow])
335
+ end
336
+
337
+ def release
338
+ @parent.release_slot(@slot_id)
339
+ end
340
+ end
341
+ end