archsight 0.1.1 → 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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +21 -22
  3. data/README.md +38 -12
  4. data/exe/archsight +3 -1
  5. data/lib/archsight/analysis/executor.rb +112 -0
  6. data/lib/archsight/analysis/result.rb +174 -0
  7. data/lib/archsight/analysis/sandbox.rb +319 -0
  8. data/lib/archsight/analysis.rb +11 -0
  9. data/lib/archsight/annotations/architecture_annotations.rb +2 -2
  10. data/lib/archsight/cli.rb +164 -0
  11. data/lib/archsight/database.rb +6 -2
  12. data/lib/archsight/helpers/analysis_renderer.rb +83 -0
  13. data/lib/archsight/helpers/formatting.rb +95 -0
  14. data/lib/archsight/helpers.rb +20 -4
  15. data/lib/archsight/import/concurrent_progress.rb +341 -0
  16. data/lib/archsight/import/executor.rb +466 -0
  17. data/lib/archsight/import/git_analytics.rb +626 -0
  18. data/lib/archsight/import/handler.rb +263 -0
  19. data/lib/archsight/import/handlers/github.rb +161 -0
  20. data/lib/archsight/import/handlers/gitlab.rb +202 -0
  21. data/lib/archsight/import/handlers/jira_base.rb +189 -0
  22. data/lib/archsight/import/handlers/jira_discover.rb +161 -0
  23. data/lib/archsight/import/handlers/jira_metrics.rb +179 -0
  24. data/lib/archsight/import/handlers/openapi_schema_parser.rb +279 -0
  25. data/lib/archsight/import/handlers/repository.rb +439 -0
  26. data/lib/archsight/import/handlers/rest_api.rb +293 -0
  27. data/lib/archsight/import/handlers/rest_api_index.rb +183 -0
  28. data/lib/archsight/import/progress.rb +91 -0
  29. data/lib/archsight/import/registry.rb +54 -0
  30. data/lib/archsight/import/shared_file_writer.rb +67 -0
  31. data/lib/archsight/import/team_matcher.rb +195 -0
  32. data/lib/archsight/import.rb +14 -0
  33. data/lib/archsight/resources/analysis.rb +91 -0
  34. data/lib/archsight/resources/application_component.rb +2 -2
  35. data/lib/archsight/resources/application_service.rb +12 -12
  36. data/lib/archsight/resources/business_product.rb +12 -12
  37. data/lib/archsight/resources/data_object.rb +1 -1
  38. data/lib/archsight/resources/import.rb +79 -0
  39. data/lib/archsight/resources/technology_artifact.rb +23 -2
  40. data/lib/archsight/version.rb +1 -1
  41. data/lib/archsight/web/api/docs.rb +17 -0
  42. data/lib/archsight/web/api/json_helpers.rb +164 -0
  43. data/lib/archsight/web/api/openapi/spec.yaml +500 -0
  44. data/lib/archsight/web/api/routes.rb +101 -0
  45. data/lib/archsight/web/application.rb +66 -43
  46. data/lib/archsight/web/doc/import.md +458 -0
  47. data/lib/archsight/web/doc/index.md.erb +2 -1
  48. data/lib/archsight/web/public/css/artifact.css +10 -0
  49. data/lib/archsight/web/public/css/graph.css +14 -0
  50. data/lib/archsight/web/public/css/instance.css +489 -0
  51. data/lib/archsight/web/views/api_docs.erb +19 -0
  52. data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +14 -8
  53. data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +74 -0
  54. data/lib/archsight/web/views/partials/instance/_analysis_result.haml +64 -0
  55. data/lib/archsight/web/views/partials/instance/_detail.haml +7 -3
  56. data/lib/archsight/web/views/partials/instance/_import_detail.haml +87 -0
  57. data/lib/archsight/web/views/partials/instance/_relations.haml +4 -4
  58. data/lib/archsight/web/views/partials/layout/_content.haml +4 -0
  59. data/lib/archsight/web/views/partials/layout/_navigation.haml +6 -5
  60. metadata +78 -1
@@ -0,0 +1,319 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../annotations/relation_resolver"
4
+
5
+ module Archsight
6
+ module Analysis
7
+ # Sandbox provides a safe execution context for Analysis scripts.
8
+ # Scripts are evaluated via instance_eval with access only to
9
+ # explicitly defined methods.
10
+ class Sandbox
11
+ # Output sections collected during script execution
12
+ attr_reader :sections
13
+
14
+ def initialize(database)
15
+ @database = database
16
+ @sections = []
17
+ @resolver_cache = {}
18
+ end
19
+
20
+ # Instance iteration methods
21
+
22
+ # Iterate over all instances of a kind
23
+ # @param kind [Symbol, String] Resource kind (e.g., :ApplicationService)
24
+ # @yield [instance] Block called for each instance
25
+ def each_instance(kind, &)
26
+ instances(kind).each(&)
27
+ end
28
+
29
+ # Get all instances of a kind
30
+ # @param kind [Symbol, String] Resource kind
31
+ # @return [Array] Array of instances
32
+ def instances(kind)
33
+ @database.instances_by_kind(kind.to_s).values
34
+ end
35
+
36
+ # Get a specific instance by kind and name
37
+ # @param kind [Symbol, String] Resource kind
38
+ # @param name [String] Instance name
39
+ # @return [Object, nil] Instance or nil if not found
40
+ def instance(kind, name)
41
+ @database.instance_by_kind(kind.to_s, name)
42
+ end
43
+
44
+ # Relation traversal methods (reuse ComputedRelationResolver)
45
+
46
+ # Get direct outgoing relations
47
+ # @param inst [Object] Source instance
48
+ # @param kind [Symbol, nil] Optional kind filter
49
+ # @return [Array] Related instances
50
+ def outgoing(inst, kind = nil)
51
+ resolver_for(inst).outgoing(kind)
52
+ end
53
+
54
+ # Get transitive outgoing relations
55
+ # @param inst [Object] Source instance
56
+ # @param kind [Symbol, nil] Optional kind filter
57
+ # @param max_depth [Integer] Maximum traversal depth
58
+ # @return [Array] Transitively related instances
59
+ def outgoing_transitive(inst, kind = nil, max_depth: 10)
60
+ resolver_for(inst).outgoing_transitive(kind, max_depth: max_depth)
61
+ end
62
+
63
+ # Get direct incoming relations (instances that reference this one)
64
+ # @param inst [Object] Target instance
65
+ # @param kind [Symbol, nil] Optional kind filter
66
+ # @return [Array] Instances referencing this one
67
+ def incoming(inst, kind = nil)
68
+ resolver_for(inst).incoming(kind)
69
+ end
70
+
71
+ # Get transitive incoming relations
72
+ # @param inst [Object] Target instance
73
+ # @param kind [Symbol, nil] Optional kind filter
74
+ # @param max_depth [Integer] Maximum traversal depth
75
+ # @return [Array] Instances transitively referencing this one
76
+ def incoming_transitive(inst, kind = nil, max_depth: 10)
77
+ resolver_for(inst).incoming_transitive(kind, max_depth: max_depth)
78
+ end
79
+
80
+ # Data access methods
81
+
82
+ # Get an annotation value from an instance
83
+ # @param inst [Object] Instance
84
+ # @param key [String] Annotation key
85
+ # @return [Object, nil] Annotation value
86
+ def annotation(inst, key)
87
+ inst.annotations[key]
88
+ end
89
+
90
+ # Get all annotations from an instance (frozen copy)
91
+ # @param inst [Object] Instance
92
+ # @return [Hash] Frozen hash of annotations
93
+ def annotations(inst)
94
+ inst.annotations.dup.freeze
95
+ end
96
+
97
+ # Get the name of an instance
98
+ # @param inst [Object] Instance
99
+ # @return [String] Instance name
100
+ def name(inst)
101
+ inst.name
102
+ end
103
+
104
+ # Get the kind of an instance
105
+ # @param inst [Object] Instance
106
+ # @return [String] Instance kind
107
+ def kind(inst)
108
+ inst.klass
109
+ end
110
+
111
+ # Query method
112
+
113
+ # Execute a query string
114
+ # @param query_string [String] Query string
115
+ # @return [Array] Matching instances
116
+ def query(query_string)
117
+ @database.query(query_string)
118
+ end
119
+
120
+ # Aggregation methods
121
+
122
+ # Sum numeric values
123
+ # @param values [Array<Numeric>] Values to sum
124
+ # @return [Numeric] Sum
125
+ def sum(values)
126
+ values.compact.sum
127
+ end
128
+
129
+ # Count items
130
+ # @param items [Array] Items to count
131
+ # @return [Integer] Count
132
+ def count(items)
133
+ items.compact.count
134
+ end
135
+
136
+ # Calculate average
137
+ # @param values [Array<Numeric>] Values to average
138
+ # @return [Float, nil] Average or nil if empty
139
+ def avg(values)
140
+ compact = values.compact
141
+ return nil if compact.empty?
142
+
143
+ compact.sum.to_f / compact.size
144
+ end
145
+
146
+ # Find minimum value
147
+ # @param values [Array<Numeric>] Values
148
+ # @return [Numeric, nil] Minimum
149
+ def min(values)
150
+ values.compact.min
151
+ end
152
+
153
+ # Find maximum value
154
+ # @param values [Array<Numeric>] Values
155
+ # @return [Numeric, nil] Maximum
156
+ def max(values)
157
+ values.compact.max
158
+ end
159
+
160
+ # Collect values into array
161
+ # @param items [Array] Items
162
+ # @param key [String, Symbol, nil] Optional key to extract
163
+ # @yield [item] Optional block to transform items
164
+ # @return [Array] Collected values
165
+ def collect(items, key = nil, &block)
166
+ if block
167
+ items.map(&block).compact
168
+ elsif key
169
+ items.map { |item| item.respond_to?(key) ? item.send(key) : item.annotations[key.to_s] }.compact
170
+ else
171
+ items.compact
172
+ end
173
+ end
174
+
175
+ # Group items by a key
176
+ # @param items [Array] Items to group
177
+ # @yield [item] Block that returns the grouping key
178
+ # @return [Hash] Grouped items
179
+ def group_by(items, &)
180
+ items.group_by(&)
181
+ end
182
+
183
+ # Output methods - Structured content building
184
+
185
+ # Add a heading
186
+ # @param text [String] Heading text
187
+ # @param level [Integer] Heading level (0-6, default 0)
188
+ # Level 0 creates accordion sections in the web UI
189
+ # Levels 1-6 map to HTML h2-h6 within sections
190
+ def heading(text, level: 0)
191
+ @sections << { type: :heading, text: text, level: level.clamp(0, 6) }
192
+ end
193
+
194
+ # Add a text paragraph
195
+ # @param content [String] Text content (supports markdown)
196
+ def text(content)
197
+ @sections << { type: :text, content: content }
198
+ end
199
+
200
+ # Add a table
201
+ # @param headers [Array<String>] Column headers
202
+ # @param rows [Array<Array>] Table rows (each row is an array of values)
203
+ def table(headers:, rows:)
204
+ return if rows.empty?
205
+
206
+ @sections << { type: :table, headers: headers, rows: rows }
207
+ end
208
+
209
+ # Add a bullet list
210
+ # @param items [Array<String, Hash>] List items (strings or hashes with :text key)
211
+ def list(items)
212
+ return if items.empty?
213
+
214
+ @sections << { type: :list, items: items }
215
+ end
216
+
217
+ # Add a code block
218
+ # @param content [String] Code content
219
+ # @param lang [String] Language for syntax highlighting (default: plain)
220
+ def code(content, lang: "")
221
+ @sections << { type: :code, content: content, lang: lang }
222
+ end
223
+
224
+ # Legacy output methods - still supported for backward compatibility
225
+
226
+ # Report findings (legacy method - creates appropriate section based on data type)
227
+ # @param data [Object] Report data (usually Array or Hash)
228
+ # @param title [String, nil] Optional report title
229
+ def report(data, title: nil)
230
+ heading(title, level: 1) if title
231
+
232
+ case data
233
+ when Array
234
+ report_array(data)
235
+ when Hash
236
+ report_hash(data)
237
+ else
238
+ text(data.to_s)
239
+ end
240
+ end
241
+
242
+ # Log a warning message
243
+ # @param message [String] Warning message
244
+ def warning(message)
245
+ @sections << { type: :message, level: :warning, message: message }
246
+ end
247
+
248
+ # Log an error message
249
+ # @param message [String] Error message
250
+ def error(message)
251
+ @sections << { type: :message, level: :error, message: message }
252
+ end
253
+
254
+ # Log an info message
255
+ # @param message [String] Info message
256
+ def info(message)
257
+ @sections << { type: :message, level: :info, message: message }
258
+ end
259
+
260
+ # Configuration access
261
+
262
+ # Get a configuration value from the analysis
263
+ # @param key [String] Configuration key (without 'analysis/config/' prefix)
264
+ # @return [Object, nil] Configuration value
265
+ def config(key)
266
+ @current_analysis&.annotations&.[]("analysis/config/#{key}")
267
+ end
268
+
269
+ # Set the current analysis being executed (called by Executor)
270
+ # @param analysis [Object] Analysis instance
271
+ # @api private
272
+ def _set_analysis(analysis)
273
+ @current_analysis = analysis
274
+ end
275
+
276
+ private
277
+
278
+ # Get or create a relation resolver for an instance
279
+ def resolver_for(inst)
280
+ @resolver_cache[inst.name] ||= Archsight::Annotations::ComputedRelationResolver.new(inst, @database)
281
+ end
282
+
283
+ # Report an array - auto-detect if it's a table or list
284
+ def report_array(data)
285
+ return if data.empty?
286
+
287
+ # If array of hashes with consistent keys, render as table
288
+ if data.all? { |item| item.is_a?(Hash) }
289
+ keys = data.first.keys
290
+ if data.all? { |item| item.keys == keys }
291
+ table(headers: keys.map(&:to_s), rows: data.map { |item| keys.map { |k| item[k] } })
292
+ return
293
+ end
294
+ end
295
+
296
+ # Otherwise render as list
297
+ list(data.map { |item| format_list_item(item) })
298
+ end
299
+
300
+ # Report a hash - render as definition list or table
301
+ def report_hash(data)
302
+ return if data.empty?
303
+
304
+ # Render as simple key-value list
305
+ list(data.map { |k, v| "**#{k}:** #{v}" })
306
+ end
307
+
308
+ # Format a list item for display
309
+ def format_list_item(item)
310
+ case item
311
+ when Hash
312
+ item.map { |k, v| "#{k}=#{v}" }.join(", ")
313
+ else
314
+ item.to_s
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Archsight
4
+ # Analysis module provides sandboxed execution of Analysis scripts
5
+ module Analysis
6
+ end
7
+ end
8
+
9
+ require_relative "analysis/result"
10
+ require_relative "analysis/sandbox"
11
+ require_relative "analysis/executor"
@@ -46,9 +46,9 @@ module Archsight::Annotations::Architecture
46
46
  filter: :word,
47
47
  title: "Status"
48
48
  annotation "architecture/visibility",
49
- description: "API visibility (public, private)",
49
+ description: "API visibility (public, private, internal)",
50
50
  filter: :word,
51
- enum: %w[public private],
51
+ enum: %w[public private internal],
52
52
  title: "Visibility"
53
53
  annotation "architecture/applicationSets",
54
54
  description: "Related ArgoCD ApplicationSets",
data/lib/archsight/cli.rb CHANGED
@@ -4,6 +4,10 @@ require "thor"
4
4
 
5
5
  module Archsight
6
6
  class CLI < Thor
7
+ def self.exit_on_failure?
8
+ true
9
+ end
10
+
7
11
  class_option :resources,
8
12
  aliases: "-r",
9
13
  type: :string,
@@ -12,10 +16,21 @@ module Archsight
12
16
  desc "web", "Start the web server"
13
17
  option :port, aliases: "-p", type: :numeric, default: 4567, desc: "Port to listen on"
14
18
  option :host, aliases: "-H", type: :string, default: "localhost", desc: "Host to bind to"
19
+ option :production, type: :boolean, default: false, desc: "Run in production mode"
20
+ option :disable_reload, type: :boolean, default: false, desc: "Disable the reload button in the UI"
21
+ option :enable_logging, type: :boolean, default: nil, desc: "Enable request logging (default: false in dev, true in prod)"
15
22
  def web
16
23
  configure_resources
17
24
  require "archsight/web/application"
25
+
26
+ env = options[:production] ? :production : :development
27
+ Archsight::Web::Application.configure_environment!(env, logging: options[:enable_logging])
28
+ Archsight::Web::Application.set :reload_enabled, !options[:disable_reload]
29
+ Archsight::Web::Application.setup_mcp!
18
30
  Archsight::Web::Application.run!(port: options[:port], bind: options[:host])
31
+ rescue Archsight::ResourceError => e
32
+ display_error_with_context(e.to_s)
33
+ exit 1
19
34
  end
20
35
 
21
36
  desc "lint", "Validate architecture resources"
@@ -70,6 +85,80 @@ module Archsight
70
85
  binding.irb
71
86
  end
72
87
 
88
+ desc "import", "Execute pending imports"
89
+ option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Verbose output"
90
+ option :dry_run, aliases: "-n", type: :boolean, default: false, desc: "Show execution plan without running"
91
+ option :filter, aliases: "-f", type: :string, desc: "Filter imports by name (regex pattern)"
92
+ option :force, aliases: "-F", type: :boolean, default: false, desc: "Ignore cache and re-run all imports"
93
+ def import
94
+ configure_resources
95
+ require "archsight/database"
96
+ require "archsight/import/executor"
97
+
98
+ resources_dir = Archsight.resources_dir
99
+
100
+ # Load all handlers
101
+ require_import_handlers
102
+
103
+ # Create database that loads from resources directory
104
+ # Only load Import resources to avoid validation errors on incomplete resources
105
+ db = Archsight::Database.new(resources_dir, verbose: options[:verbose], only_kinds: ["Import"], verify: false)
106
+
107
+ if options[:dry_run]
108
+ puts "Execution Plan:"
109
+ executor = Archsight::Import::Executor.new(
110
+ database: db,
111
+ resources_dir: resources_dir,
112
+ verbose: true,
113
+ filter: options[:filter],
114
+ force: options[:force]
115
+ )
116
+ executor.execution_plan
117
+ else
118
+ executor = Archsight::Import::Executor.new(
119
+ database: db,
120
+ resources_dir: resources_dir,
121
+ verbose: options[:verbose],
122
+ filter: options[:filter],
123
+ force: options[:force]
124
+ )
125
+ executor.run!
126
+ puts "All imports completed successfully."
127
+ end
128
+ rescue Archsight::Import::InterruptedError
129
+ # Graceful shutdown already handled by executor
130
+ exit 130
131
+ rescue Archsight::Import::DeadlockError => e
132
+ puts "Error: #{e.message}"
133
+ exit 1
134
+ rescue Archsight::Import::ImportError => e
135
+ puts "Error: #{e.message}"
136
+ exit 1
137
+ rescue Archsight::ResourceError => e
138
+ display_error_with_context(e.to_s)
139
+ exit 1
140
+ end
141
+
142
+ desc "analyze", "Execute analysis scripts"
143
+ option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Verbose output"
144
+ option :dry_run, aliases: "-n", type: :boolean, default: false, desc: "List analyses without running"
145
+ option :filter, aliases: "-f", type: :string, desc: "Filter analyses by name (regex pattern)"
146
+ def analyze
147
+ configure_resources
148
+ require "archsight/database"
149
+ require "archsight/analysis"
150
+
151
+ db = load_database_for_analysis
152
+ analyses = filter_analyses(db)
153
+
154
+ return puts("No analyses found#{" matching '#{options[:filter]}'" if options[:filter]}.") if analyses.empty?
155
+ return print_analysis_dry_run(analyses) if options[:dry_run]
156
+
157
+ results = execute_analyses(db, analyses)
158
+ print_analysis_results(results)
159
+ exit 1 if results.any?(&:failed?)
160
+ end
161
+
73
162
  desc "version", "Show version"
74
163
  def version
75
164
  puts "archsight #{Archsight::VERSION}"
@@ -83,6 +172,81 @@ module Archsight
83
172
  Archsight.resources_dir = options[:resources] if options[:resources]
84
173
  end
85
174
 
175
+ def require_import_handlers
176
+ handlers_dir = File.expand_path("import/handlers", __dir__)
177
+ Dir.glob(File.join(handlers_dir, "*.rb")).each do |handler_file|
178
+ require handler_file
179
+ end
180
+ end
181
+
182
+ def load_database_for_analysis
183
+ db = Archsight::Database.new(Archsight.resources_dir, verbose: options[:verbose])
184
+ db.reload!
185
+ db
186
+ rescue Archsight::ResourceError => e
187
+ display_error_with_context(e.to_s)
188
+ exit 1
189
+ end
190
+
191
+ def filter_analyses(db)
192
+ analyses = db.instances_by_kind("Analysis").values
193
+ analyses = analyses.select { |a| Regexp.new(options[:filter], Regexp::IGNORECASE).match?(a.name) } if options[:filter]
194
+ analyses.reject { |a| a.annotations["analysis/enabled"] == "false" }
195
+ end
196
+
197
+ def print_analysis_dry_run(analyses)
198
+ puts "Analyses to run#{" (filter: #{options[:filter]})" if options[:filter]}:"
199
+ analyses.sort_by(&:name).each_with_index do |analysis, idx|
200
+ timeout = analysis.annotations["analysis/timeout"] || "30s"
201
+ desc = analysis.annotations["analysis/description"] || "(no description)"
202
+ puts " #{idx + 1}. #{analysis.name} [#{timeout}]"
203
+ puts " #{desc}"
204
+ end
205
+ end
206
+
207
+ def execute_analyses(db, analyses)
208
+ executor = Archsight::Analysis::Executor.new(db)
209
+ analyses.map { |analysis| executor.execute(analysis) }
210
+ end
211
+
212
+ def print_analysis_results(results)
213
+ require "tty-markdown"
214
+
215
+ results.each do |result|
216
+ print_single_result(result)
217
+ puts ""
218
+ end
219
+
220
+ summary_md = build_analysis_summary_markdown(results)
221
+ puts TTY::Markdown.parse(summary_md)
222
+ end
223
+
224
+ def print_single_result(result)
225
+ # Print status header
226
+ header = "# #{result.status_emoji} #{result.name}"
227
+ header += " (#{result.duration_str})" unless result.duration_str.empty?
228
+ puts TTY::Markdown.parse(header)
229
+
230
+ # Print error if failed
231
+ puts TTY::Markdown.parse(result.error_markdown(verbose: options[:verbose])) if result.failed?
232
+
233
+ # Print script output
234
+ output = result.to_s(verbose: options[:verbose])
235
+ puts output unless output.empty?
236
+ end
237
+
238
+ def build_analysis_summary_markdown(results)
239
+ passed = results.count(&:success?)
240
+ failed = results.count(&:failed?)
241
+ with_findings = results.count(&:has_findings?)
242
+
243
+ lines = ["---", "", "# Summary", ""]
244
+ lines << "- ✅ **#{passed}** passed"
245
+ lines << "- ❌ **#{failed}** failed" if failed.positive?
246
+ lines << "- ⚠️ **#{with_findings}** with findings" if with_findings.positive?
247
+ lines.join("\n")
248
+ end
249
+
86
250
  def list_kinds
87
251
  puts "Available resource kinds:\n\n"
88
252
  Archsight::Resources.resource_classes.each_key { |kind| puts " - #{kind}" }
@@ -43,13 +43,14 @@ module Archsight
43
43
  # of the structure. The loading and parsing of files will raise errors
44
44
  # if invalid data is passed.
45
45
  class Database
46
- attr_accessor :instances, :verbose, :verify, :compute_annotations
46
+ attr_accessor :instances, :verbose, :verify, :compute_annotations, :only_kinds
47
47
 
48
- def initialize(path, verbose: false, verify: true, compute_annotations: true)
48
+ def initialize(path, verbose: false, verify: true, compute_annotations: true, only_kinds: nil)
49
49
  @path = path
50
50
  @verbose = verbose
51
51
  @verify = verify
52
52
  @compute_annotations = compute_annotations
53
+ @only_kinds = only_kinds
53
54
  @instances = {}
54
55
  end
55
56
 
@@ -127,6 +128,9 @@ module Archsight
127
128
  obj = node.to_ruby
128
129
  next unless obj # skip empty / unknown documents
129
130
 
131
+ # Skip resources that don't match only_kinds filter
132
+ next if @only_kinds && !@only_kinds.include?(obj["kind"])
133
+
130
134
  self << create_valid_instance(obj)
131
135
  end
132
136
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/utils"
4
+
5
+ module Archsight
6
+ module Helpers
7
+ # AnalysisRenderer provides HTML rendering for analysis result sections
8
+ module AnalysisRenderer
9
+ module_function
10
+
11
+ # HTML escape helper
12
+ def escape_html(text)
13
+ Rack::Utils.escape_html(text.to_s)
14
+ end
15
+
16
+ # Render a single analysis section to HTML
17
+ # @param section [Hash] Section data with :type and content
18
+ # @param markdown_renderer [Proc, nil] Optional proc to render markdown
19
+ def render_analysis_section(section, markdown_renderer: nil)
20
+ case section[:type]
21
+ when :heading
22
+ render_heading(section)
23
+ when :text
24
+ render_text(section, markdown_renderer)
25
+ when :message
26
+ render_message(section)
27
+ when :table
28
+ render_table(section)
29
+ when :list
30
+ render_list(section)
31
+ when :code
32
+ render_code(section)
33
+ else
34
+ ""
35
+ end
36
+ end
37
+
38
+ # Render analysis table section to HTML
39
+ def render_analysis_table(section)
40
+ render_table(section)
41
+ end
42
+
43
+ # Private rendering methods
44
+
45
+ def render_heading(section)
46
+ %(<div class="analysis-heading level-#{section[:level]}">#{escape_html(section[:text])}</div>)
47
+ end
48
+
49
+ def render_text(section, markdown_renderer)
50
+ content = markdown_renderer ? markdown_renderer.call(section[:content]) : escape_html(section[:content])
51
+ %(<div class="analysis-text">#{content}</div>)
52
+ end
53
+
54
+ def render_message(section)
55
+ icon = case section[:level]
56
+ when :error then "xmark-circle"
57
+ when :warning then "warning-triangle"
58
+ else "info-circle"
59
+ end
60
+ %(<div class="analysis-message message-#{section[:level]}"><i class="iconoir-#{icon}"></i> #{escape_html(section[:message])}</div>)
61
+ end
62
+
63
+ def render_table(section)
64
+ headers = section[:headers].map { |h| "<th>#{escape_html(h)}</th>" }.join
65
+ rows = section[:rows].map do |row|
66
+ cells = row.map { |cell| "<td>#{escape_html(cell.to_s)}</td>" }.join
67
+ "<tr>#{cells}</tr>"
68
+ end.join
69
+ %(<div class="analysis-table-wrapper"><table><thead><tr>#{headers}</tr></thead><tbody>#{rows}</tbody></table></div>)
70
+ end
71
+
72
+ def render_list(section)
73
+ items = section[:items].map { |item| "<li>#{escape_html(item)}</li>" }.join
74
+ %(<ul class="analysis-list">#{items}</ul>)
75
+ end
76
+
77
+ def render_code(section)
78
+ lang_class = section[:lang] ? " class=\"language-#{section[:lang]}\"" : ""
79
+ %(<pre class="code"><code#{lang_class}>#{escape_html(section[:content])}</code></pre>)
80
+ end
81
+ end
82
+ end
83
+ end