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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5daf787a0535e787bb31a5abbe46e8da7621256e710dd59ec7328c116b988df4
4
- data.tar.gz: e662425404ec865eef3701723135c103e688e43bb8d560708f936dd9021acd88
3
+ metadata.gz: 697c02b1e33af4fde32636af6020b7ab6513245216b694453d03dab186f0dd99
4
+ data.tar.gz: 58a8cd9b7e530796d04424f885e931863a1bf6ed2c2d0366bf76d28453eb80fb
5
5
  SHA512:
6
- metadata.gz: c31f6486348cd3e4e2b3a8247071f3f1ddbe139b0cb20faeba51167122d860d74cf205128532d6184ac78a9eac5be64ccee28c41dec9fff094963d77e1ee34bc
7
- data.tar.gz: cb36b1eb48ed9c44985925e2e54ffa7c74d66ee51ca0407882151db27ce34168f747dc9cb4aadbfe35c19d1fb5c5a1b3860741635a6a342a810faac926638885
6
+ metadata.gz: a80fc22f8b7030c71cb2422b839521e96f31ecced71386ed075c51d9ca2d53d2a057ced240a4085ad032c895ce8526d44a585b6970c67f2217fe15232a2209cd
7
+ data.tar.gz: 3d0648dca25e6337679d6a236b256b5580d9a5260df927a792680e5e9187e289b0c781aa36d0fb81893fea3889e57b7ec1b7a397d731039d5f5dcbebdff2f587
data/README.md CHANGED
@@ -44,6 +44,9 @@ Access at: <http://localhost:4567>
44
44
  # Run web server (default)
45
45
  docker run -p 4567:4567 -v "/path/to/resources:/resources" ghcr.io/ionos-cloud/archsight
46
46
 
47
+ # Run in production mode with logging
48
+ docker run -p 4567:4567 -v "/path/to/resources:/resources" ghcr.io/ionos-cloud/archsight web --production
49
+
47
50
  # Run lint
48
51
  docker run -v "/path/to/resources:/resources" ghcr.io/ionos-cloud/archsight lint -r /resources
49
52
 
@@ -62,13 +65,31 @@ Access web interface at: <http://localhost:4567>
62
65
  ## CLI Commands
63
66
 
64
67
  ```bash
65
- archsight web [--resources PATH] [--port PORT] # Start web server
66
- archsight lint [--resources PATH] # Validate YAML and relations
67
- archsight template KIND # Generate YAML template for a resource type
68
- archsight console [--resources PATH] # Interactive Ruby console
69
- archsight version # Show version
68
+ archsight web [OPTIONS] # Start web server
69
+ archsight lint # Validate YAML and relations
70
+ archsight import # Execute pending imports
71
+ archsight analyze # Execute analysis scripts
72
+ archsight template KIND # Generate YAML template for a resource type
73
+ archsight console # Interactive Ruby console
74
+ archsight version # Show version
75
+ ```
76
+
77
+ ### Web Server Options
78
+
79
+ ```bash
80
+ archsight web [--resources PATH] [--port PORT] [--host HOST]
81
+ [--production] [--disable-reload] [--enable-logging]
70
82
  ```
71
83
 
84
+ | Option | Description |
85
+ |--------|-------------|
86
+ | `-r, --resources PATH` | Path to resources directory |
87
+ | `-p, --port PORT` | Port to listen on (default: 4567) |
88
+ | `-H, --host HOST` | Host to bind to (default: localhost) |
89
+ | `--production` | Run in production mode (quiet startup) |
90
+ | `--disable-reload` | Disable the reload button in the UI |
91
+ | `--enable-logging` | Enable request logging (default: false in dev, true in prod) |
92
+
72
93
  ## Features
73
94
 
74
95
  ### MCP Server
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+ require_relative "sandbox"
5
+ require_relative "result"
6
+
7
+ module Archsight
8
+ module Analysis
9
+ # Executor runs Analysis scripts in a sandboxed environment with timeout enforcement
10
+ class Executor
11
+ # Default timeout in seconds
12
+ DEFAULT_TIMEOUT = 30
13
+
14
+ # Duration parsing patterns
15
+ DURATION_PATTERNS = {
16
+ /^(\d+)s$/ => 1,
17
+ /^(\d+)m$/ => 60,
18
+ /^(\d+)h$/ => 3600
19
+ }.freeze
20
+
21
+ attr_reader :database
22
+
23
+ # @param database [Archsight::Database] Loaded database instance
24
+ def initialize(database)
25
+ @database = database
26
+ end
27
+
28
+ # Execute an Analysis resource
29
+ # @param analysis [Archsight::Resources::Analysis] Analysis to execute
30
+ # @return [Archsight::Analysis::Result] Execution result
31
+ def execute(analysis)
32
+ script = analysis.annotations["analysis/script"]
33
+ timeout_seconds = parse_timeout(analysis.annotations["analysis/timeout"])
34
+
35
+ return Result.new(analysis, success: false, error: "No script defined") if script.nil? || script.empty?
36
+
37
+ sandbox = Sandbox.new(@database)
38
+ sandbox._set_analysis(analysis)
39
+
40
+ start_time = Time.now
41
+ begin
42
+ Timeout.timeout(timeout_seconds) do
43
+ # Execute script in sandbox context
44
+ # Using instance_eval ensures script only has access to sandbox methods
45
+ sandbox.instance_eval(script, "analysis:#{analysis.name}", 1)
46
+ end
47
+
48
+ duration = Time.now - start_time
49
+ Result.new(
50
+ analysis,
51
+ success: true,
52
+ sections: sandbox.sections,
53
+ duration: duration
54
+ )
55
+ rescue Timeout::Error
56
+ Result.new(
57
+ analysis,
58
+ success: false,
59
+ error: "Execution timed out after #{timeout_seconds}s",
60
+ sections: sandbox.sections
61
+ )
62
+ rescue StandardError, SyntaxError => e
63
+ Result.new(
64
+ analysis,
65
+ success: false,
66
+ error: "#{e.class}: #{e.message}",
67
+ error_backtrace: e.backtrace&.first(5),
68
+ sections: sandbox.sections
69
+ )
70
+ end
71
+ end
72
+
73
+ # Execute all enabled Analysis resources
74
+ # @param filter [Regexp, nil] Optional filter for analysis names
75
+ # @return [Array<Archsight::Analysis::Result>] Array of results
76
+ def execute_all(filter: nil)
77
+ analyses = @database.instances_by_kind("Analysis").values
78
+
79
+ # Filter by name pattern if provided
80
+ analyses = analyses.select { |a| filter.match?(a.name) } if filter
81
+
82
+ # Filter to enabled analyses
83
+ analyses = analyses.select { |a| analysis_enabled?(a) }
84
+
85
+ analyses.map { |analysis| execute(analysis) }
86
+ end
87
+
88
+ private
89
+
90
+ # Parse timeout string to seconds
91
+ # @param timeout_str [String, nil] Timeout string (e.g., "30s", "5m")
92
+ # @return [Integer] Timeout in seconds
93
+ def parse_timeout(timeout_str)
94
+ return DEFAULT_TIMEOUT if timeout_str.nil? || timeout_str.empty?
95
+
96
+ DURATION_PATTERNS.each do |pattern, multiplier|
97
+ match = timeout_str.match(pattern)
98
+ return match[1].to_i * multiplier if match
99
+ end
100
+
101
+ DEFAULT_TIMEOUT
102
+ end
103
+
104
+ # Check if analysis is enabled
105
+ # @param analysis [Object] Analysis instance
106
+ # @return [Boolean] true if enabled
107
+ def analysis_enabled?(analysis)
108
+ analysis.annotations["analysis/enabled"] != "false"
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Archsight
4
+ module Analysis
5
+ # Result represents the outcome of an Analysis execution
6
+ class Result
7
+ attr_reader :analysis, :success, :error, :error_backtrace, :sections, :duration
8
+
9
+ # @param analysis [Object] The Analysis resource that was executed
10
+ # @param success [Boolean] Whether execution completed successfully
11
+ # @param error [String, nil] Error message if execution failed
12
+ # @param error_backtrace [Array<String>, nil] Backtrace if execution failed
13
+ # @param sections [Array<Hash>] Output sections generated during execution
14
+ # @param duration [Float, nil] Execution duration in seconds
15
+ def initialize(analysis, success:, error: nil, error_backtrace: nil, sections: [], duration: nil)
16
+ @analysis = analysis
17
+ @success = success
18
+ @error = error
19
+ @error_backtrace = error_backtrace
20
+ @sections = sections || []
21
+ @duration = duration
22
+ end
23
+
24
+ # @return [Boolean] true if execution succeeded
25
+ def success?
26
+ @success
27
+ end
28
+
29
+ # @return [Boolean] true if execution failed
30
+ def failed?
31
+ !@success
32
+ end
33
+
34
+ # @return [String] Analysis name
35
+ def name
36
+ @analysis.name
37
+ end
38
+
39
+ # @return [Boolean] true if any content sections exist (excluding messages)
40
+ def has_findings?
41
+ @sections.any? { |s| %i[table list text heading code].include?(s[:type]) }
42
+ end
43
+
44
+ # @return [Integer] Count of error-level messages
45
+ def error_count
46
+ @sections.count { |s| s[:type] == :message && s[:level] == :error }
47
+ end
48
+
49
+ # @return [Integer] Count of warning-level messages
50
+ def warning_count
51
+ @sections.count { |s| s[:type] == :message && s[:level] == :warning }
52
+ end
53
+
54
+ # Convert result to markdown (only script-generated content)
55
+ # @param verbose [Boolean] Include detailed output
56
+ # @return [String] Markdown formatted output
57
+ def to_markdown(verbose: false)
58
+ format_sections_markdown(verbose).compact.join("\n\n")
59
+ end
60
+
61
+ # Render markdown for CLI display using tty-markdown
62
+ # @param verbose [Boolean] Include detailed output
63
+ # @return [String] Rendered output for terminal
64
+ def render(verbose: false)
65
+ require "tty-markdown"
66
+ md = to_markdown(verbose: verbose)
67
+ md.empty? ? "" : TTY::Markdown.parse(md)
68
+ rescue LoadError
69
+ # Fallback to plain markdown if tty-markdown not available
70
+ to_markdown(verbose: verbose)
71
+ end
72
+
73
+ # Format result for console output (backward compatible)
74
+ # @param verbose [Boolean] Include detailed output
75
+ # @return [String] Formatted output
76
+ def to_s(verbose: false)
77
+ render(verbose: verbose)
78
+ end
79
+
80
+ # Status emoji for display
81
+ # @return [String] Status emoji
82
+ def status_emoji
83
+ return "❌" unless success?
84
+
85
+ has_findings? ? "⚠️" : "✅"
86
+ end
87
+
88
+ # Formatted duration string
89
+ # @return [String] Duration string or empty
90
+ def duration_str
91
+ @duration ? format("%.2fs", @duration) : ""
92
+ end
93
+
94
+ # Error details as markdown (for CLI to use if needed)
95
+ # @param verbose [Boolean] Include backtrace
96
+ # @return [String, nil] Error markdown or nil
97
+ def error_markdown(verbose: false)
98
+ return nil unless failed?
99
+
100
+ lines = ["**Error:** #{@error}"]
101
+ if verbose && @error_backtrace&.any?
102
+ lines << ""
103
+ lines << "```"
104
+ lines.concat(@error_backtrace)
105
+ lines << "```"
106
+ end
107
+ lines.join("\n")
108
+ end
109
+
110
+ private
111
+
112
+ def format_sections_markdown(verbose)
113
+ @sections.map { |section| format_section_markdown(section, verbose) }.compact
114
+ end
115
+
116
+ def format_section_markdown(section, verbose)
117
+ case section[:type]
118
+ when :message then format_message_markdown(section)
119
+ when :heading then format_heading_markdown(section)
120
+ when :text then section[:content]
121
+ when :table then format_table_markdown(section, verbose)
122
+ when :list then format_list_markdown(section, verbose)
123
+ when :code then format_code_markdown(section)
124
+ end
125
+ end
126
+
127
+ def format_message_markdown(section)
128
+ emoji = { error: "🔴", warning: "🟡", info: "🔵" }.fetch(section[:level], "ℹ️")
129
+ "#{emoji} #{section[:message]}"
130
+ end
131
+
132
+ def format_heading_markdown(section)
133
+ "#{"#" * (section[:level] + 1)} #{section[:text]}"
134
+ end
135
+
136
+ def format_table_markdown(section, verbose)
137
+ headers = section[:headers]
138
+ rows = section[:rows]
139
+
140
+ # Limit rows if not verbose
141
+ display_rows = verbose ? rows : rows.first(10)
142
+ truncated = !verbose && rows.size > 10
143
+
144
+ lines = []
145
+ lines << "| #{headers.join(" | ")} |"
146
+ lines << "| #{headers.map { "---" }.join(" | ")} |"
147
+ display_rows.each do |row|
148
+ lines << "| #{row.map { |cell| cell.to_s.gsub("|", "\\|") }.join(" | ")} |"
149
+ end
150
+ lines << "_...and #{rows.size - 10} more rows_" if truncated
151
+
152
+ lines.join("\n")
153
+ end
154
+
155
+ def format_list_markdown(section, verbose)
156
+ items = section[:items]
157
+
158
+ # Limit items if not verbose
159
+ display_items = verbose ? items : items.first(10)
160
+ truncated = !verbose && items.size > 10
161
+
162
+ lines = display_items.map { |item| "- #{item}" }
163
+ lines << "_...and #{items.size - 10} more items_" if truncated
164
+
165
+ lines.join("\n")
166
+ end
167
+
168
+ def format_code_markdown(section)
169
+ lang = section[:lang] || ""
170
+ "```#{lang}\n#{section[:content]}\n```"
171
+ end
172
+ end
173
+ end
174
+ end
@@ -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",