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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '02159c7a5daad27f25979069c4b99ffaf571a21d39aff2cab0f5596a3214f897'
4
- data.tar.gz: ab77c07451e71853aaca877b2642df46ffc32c81b5dfbcc0d7b86893a26ef955
3
+ metadata.gz: 697c02b1e33af4fde32636af6020b7ab6513245216b694453d03dab186f0dd99
4
+ data.tar.gz: 58a8cd9b7e530796d04424f885e931863a1bf6ed2c2d0366bf76d28453eb80fb
5
5
  SHA512:
6
- metadata.gz: d753c04025bea2bb8ce93b55e0cb479eb3c44ad756d1e123b1cda844119fedfcce43d8f4b824d6cefd6c5c98401099eaa00c1416cc2302f2486514917f3df05e
7
- data.tar.gz: 3992fc81f881e7de8c751c6af12354463eabfeb8ac30d8019106ec0676d792543cdf4b4658aef1075b5f8a85d1ef037d31cfe0b33df0b2867de7d0f3b1b97157
6
+ metadata.gz: a80fc22f8b7030c71cb2422b839521e96f31ecced71386ed075c51d9ca2d53d2a057ced240a4085ad032c895ce8526d44a585b6970c67f2217fe15232a2209cd
7
+ data.tar.gz: 3d0648dca25e6337679d6a236b256b5580d9a5260df927a792680e5e9187e289b0c781aa36d0fb81893fea3889e57b7ec1b7a397d731039d5f5dcbebdff2f587
data/Dockerfile CHANGED
@@ -1,39 +1,38 @@
1
- FROM ruby:4.0-alpine3.23
1
+ # Build stage
2
+ FROM ruby:4.0-alpine3.23 AS builder
2
3
 
3
- # Install system dependencies required for building gems
4
- RUN apk add --no-cache \
5
- build-base \
6
- git \
7
- libffi-dev \
8
- yaml-dev \
9
- graphviz
4
+ RUN apk add --no-cache build-base git libffi-dev yaml-dev
10
5
 
11
- # Set working directory
12
6
  WORKDIR /app
7
+ COPY . .
8
+
9
+ # Build and install the gem
10
+ RUN gem build archsight.gemspec && \
11
+ gem install --no-document archsight-*.gem
13
12
 
14
- # Copy gemspec, Gemfile, and .ruby-version for dependency installation
15
- COPY archsight.gemspec Gemfile Gemfile.lock* .ruby-version ./
16
- COPY lib/archsight/version.rb lib/archsight/version.rb
13
+ # Runtime stage
14
+ FROM ruby:4.0-alpine3.23
17
15
 
18
- # Install Ruby dependencies
19
- RUN bundle install --jobs 4
16
+ RUN apk add --no-cache graphviz
20
17
 
21
- # Copy application code
22
- COPY . .
18
+ # Copy installed gems from builder (including default gems that were updated)
19
+ COPY --from=builder /usr/local/bundle /usr/local/bundle
20
+ COPY --from=builder /usr/local/lib/ruby/gems /usr/local/lib/ruby/gems
21
+
22
+ # Ensure Ruby finds gems in both /usr/local/bundle and default gems location
23
+ ENV GEM_HOME=/usr/local/bundle
24
+ ENV GEM_PATH=/usr/local/bundle:/usr/local/lib/ruby/gems/4.0.0
25
+ ENV PATH="/usr/local/bundle/bin:${PATH}"
23
26
 
24
- # Create volume mount point for resources
25
27
  RUN mkdir -p /resources
26
28
 
27
- # Set resources directory environment variable
28
29
  ENV ARCHSIGHT_RESOURCES_DIR=/resources
29
30
  ENV APP_ENV=production
30
31
 
31
- # Expose port for web server
32
32
  EXPOSE 4567
33
33
 
34
- # Health check
35
34
  HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
36
35
  CMD wget --no-verbose --tries=1 --spider http://localhost:4567/ || exit 1
37
36
 
38
- # Default command - use the archsight CLI
39
- CMD ["bundle", "exec", "exe/archsight", "web", "--port", "4567"]
37
+ ENTRYPOINT ["archsight"]
38
+ CMD ["web", "--port", "4567"]
data/README.md CHANGED
@@ -41,29 +41,55 @@ Access at: <http://localhost:4567>
41
41
  ### Option 2: Docker
42
42
 
43
43
  ```bash
44
- docker build -t archsight .
45
- docker run -p 4567:4567 -v "/path/to/resources:/resources" archsight
44
+ # Run web server (default)
45
+ docker run -p 4567:4567 -v "/path/to/resources:/resources" ghcr.io/ionos-cloud/archsight
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
+
50
+ # Run lint
51
+ docker run -v "/path/to/resources:/resources" ghcr.io/ionos-cloud/archsight lint -r /resources
52
+
53
+ # Run any command
54
+ docker run ghcr.io/ionos-cloud/archsight version
46
55
  ```
47
56
 
48
- Access at: <http://localhost:4567>
57
+ Access web interface at: <http://localhost:4567>
49
58
 
50
59
  **Notes:**
51
60
 
52
- - The volume mount `-v "/path/to/resources:/resources"` mounts your resources directory
53
- - Resources are accessible at `/resources` inside the container
54
- - Live YAML updates are reflected without rebuilding
55
- - Health check verifies the service is running
61
+ - Volume mount `-v "/path/to/resources:/resources"` mounts your resources directory
62
+ - Default command starts the web server on port 4567
63
+ - Pass subcommands directly (lint, version, console, template)
56
64
 
57
65
  ## CLI Commands
58
66
 
59
67
  ```bash
60
- archsight web [--resources PATH] [--port PORT] # Start web server
61
- archsight lint [--resources PATH] # Validate YAML and relations
62
- archsight template KIND # Generate YAML template for a resource type
63
- archsight console [--resources PATH] # Interactive Ruby console
64
- 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
65
75
  ```
66
76
 
77
+ ### Web Server Options
78
+
79
+ ```bash
80
+ archsight web [--resources PATH] [--port PORT] [--host HOST]
81
+ [--production] [--disable-reload] [--enable-logging]
82
+ ```
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
+
67
93
  ## Features
68
94
 
69
95
  ### MCP Server
data/exe/archsight CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+ # Support running from source (development)
5
+ lib_path = File.expand_path("../lib", __dir__)
6
+ $LOAD_PATH.unshift(lib_path) if File.directory?(File.join(lib_path, "archsight"))
5
7
 
6
8
  require "archsight"
7
9
  require "archsight/cli"
@@ -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