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.
- checksums.yaml +4 -4
- data/README.md +26 -5
- data/lib/archsight/analysis/executor.rb +112 -0
- data/lib/archsight/analysis/result.rb +174 -0
- data/lib/archsight/analysis/sandbox.rb +319 -0
- data/lib/archsight/analysis.rb +11 -0
- data/lib/archsight/annotations/architecture_annotations.rb +2 -2
- data/lib/archsight/cli.rb +163 -0
- data/lib/archsight/database.rb +6 -2
- data/lib/archsight/helpers/analysis_renderer.rb +83 -0
- data/lib/archsight/helpers/formatting.rb +95 -0
- data/lib/archsight/helpers.rb +20 -4
- data/lib/archsight/import/concurrent_progress.rb +341 -0
- data/lib/archsight/import/executor.rb +466 -0
- data/lib/archsight/import/git_analytics.rb +626 -0
- data/lib/archsight/import/handler.rb +263 -0
- data/lib/archsight/import/handlers/github.rb +161 -0
- data/lib/archsight/import/handlers/gitlab.rb +202 -0
- data/lib/archsight/import/handlers/jira_base.rb +189 -0
- data/lib/archsight/import/handlers/jira_discover.rb +161 -0
- data/lib/archsight/import/handlers/jira_metrics.rb +179 -0
- data/lib/archsight/import/handlers/openapi_schema_parser.rb +279 -0
- data/lib/archsight/import/handlers/repository.rb +439 -0
- data/lib/archsight/import/handlers/rest_api.rb +293 -0
- data/lib/archsight/import/handlers/rest_api_index.rb +183 -0
- data/lib/archsight/import/progress.rb +91 -0
- data/lib/archsight/import/registry.rb +54 -0
- data/lib/archsight/import/shared_file_writer.rb +67 -0
- data/lib/archsight/import/team_matcher.rb +195 -0
- data/lib/archsight/import.rb +14 -0
- data/lib/archsight/resources/analysis.rb +91 -0
- data/lib/archsight/resources/application_component.rb +2 -2
- data/lib/archsight/resources/application_service.rb +12 -12
- data/lib/archsight/resources/business_product.rb +12 -12
- data/lib/archsight/resources/data_object.rb +1 -1
- data/lib/archsight/resources/import.rb +79 -0
- data/lib/archsight/resources/technology_artifact.rb +23 -2
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/docs.rb +17 -0
- data/lib/archsight/web/api/json_helpers.rb +164 -0
- data/lib/archsight/web/api/openapi/spec.yaml +500 -0
- data/lib/archsight/web/api/routes.rb +101 -0
- data/lib/archsight/web/application.rb +66 -43
- data/lib/archsight/web/doc/import.md +458 -0
- data/lib/archsight/web/doc/index.md.erb +1 -0
- data/lib/archsight/web/public/css/artifact.css +10 -0
- data/lib/archsight/web/public/css/graph.css +14 -0
- data/lib/archsight/web/public/css/instance.css +489 -0
- data/lib/archsight/web/views/api_docs.erb +19 -0
- data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +14 -8
- data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +74 -0
- data/lib/archsight/web/views/partials/instance/_analysis_result.haml +64 -0
- data/lib/archsight/web/views/partials/instance/_detail.haml +7 -3
- data/lib/archsight/web/views/partials/instance/_import_detail.haml +87 -0
- data/lib/archsight/web/views/partials/instance/_relations.haml +4 -4
- data/lib/archsight/web/views/partials/layout/_content.haml +4 -0
- data/lib/archsight/web/views/partials/layout/_navigation.haml +6 -5
- metadata +78 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 697c02b1e33af4fde32636af6020b7ab6513245216b694453d03dab186f0dd99
|
|
4
|
+
data.tar.gz: 58a8cd9b7e530796d04424f885e931863a1bf6ed2c2d0366bf76d28453eb80fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 [
|
|
66
|
-
archsight lint
|
|
67
|
-
archsight
|
|
68
|
-
archsight
|
|
69
|
-
archsight
|
|
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",
|