archsight 0.1.0

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 (122) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/CONTRIBUTING.md +186 -0
  5. data/Dockerfile +39 -0
  6. data/LICENSE.txt +201 -0
  7. data/README.md +170 -0
  8. data/SECURITY.md +27 -0
  9. data/exe/archsight +9 -0
  10. data/lib/archsight/annotations/aggregators.rb +109 -0
  11. data/lib/archsight/annotations/annotation.rb +168 -0
  12. data/lib/archsight/annotations/architecture_annotations.rb +59 -0
  13. data/lib/archsight/annotations/backup_annotations.rb +21 -0
  14. data/lib/archsight/annotations/computed.rb +264 -0
  15. data/lib/archsight/annotations/email_recipient.rb +35 -0
  16. data/lib/archsight/annotations/generated_annotations.rb +17 -0
  17. data/lib/archsight/annotations/git_annotations.rb +21 -0
  18. data/lib/archsight/annotations/relation_resolver.rb +160 -0
  19. data/lib/archsight/cli.rb +120 -0
  20. data/lib/archsight/configuration.rb +36 -0
  21. data/lib/archsight/database.rb +183 -0
  22. data/lib/archsight/documentation.rb +171 -0
  23. data/lib/archsight/graph.rb +113 -0
  24. data/lib/archsight/helpers.rb +210 -0
  25. data/lib/archsight/linter.rb +77 -0
  26. data/lib/archsight/mcp/analyze_resource_tool.rb +222 -0
  27. data/lib/archsight/mcp/base.rb +48 -0
  28. data/lib/archsight/mcp/query_tool.rb +113 -0
  29. data/lib/archsight/mcp/resource_doc_tool.rb +87 -0
  30. data/lib/archsight/mcp.rb +6 -0
  31. data/lib/archsight/query/ast.rb +279 -0
  32. data/lib/archsight/query/errors.rb +39 -0
  33. data/lib/archsight/query/evaluator.rb +707 -0
  34. data/lib/archsight/query/lexer.rb +289 -0
  35. data/lib/archsight/query/parser.rb +506 -0
  36. data/lib/archsight/query.rb +68 -0
  37. data/lib/archsight/renderer.rb +134 -0
  38. data/lib/archsight/resources/application_component.rb +346 -0
  39. data/lib/archsight/resources/application_interface.rb +54 -0
  40. data/lib/archsight/resources/application_service.rb +222 -0
  41. data/lib/archsight/resources/base.rb +300 -0
  42. data/lib/archsight/resources/business_actor.rb +195 -0
  43. data/lib/archsight/resources/business_constraint.rb +32 -0
  44. data/lib/archsight/resources/business_process.rb +37 -0
  45. data/lib/archsight/resources/business_product.rb +206 -0
  46. data/lib/archsight/resources/business_requirement.rb +56 -0
  47. data/lib/archsight/resources/compliance_evidence.rb +42 -0
  48. data/lib/archsight/resources/data_object.rb +49 -0
  49. data/lib/archsight/resources/motivation_goal.rb +37 -0
  50. data/lib/archsight/resources/motivation_outcome.rb +33 -0
  51. data/lib/archsight/resources/motivation_stakeholder.rb +38 -0
  52. data/lib/archsight/resources/strategy_capability.rb +38 -0
  53. data/lib/archsight/resources/technology_artifact.rb +154 -0
  54. data/lib/archsight/resources/technology_interface.rb +34 -0
  55. data/lib/archsight/resources/technology_node.rb +42 -0
  56. data/lib/archsight/resources/technology_service.rb +35 -0
  57. data/lib/archsight/resources/technology_system_software.rb +37 -0
  58. data/lib/archsight/resources/view.rb +51 -0
  59. data/lib/archsight/resources.rb +49 -0
  60. data/lib/archsight/template.rb +49 -0
  61. data/lib/archsight/version.rb +5 -0
  62. data/lib/archsight/web/application.rb +290 -0
  63. data/lib/archsight/web/doc/archimate.md +215 -0
  64. data/lib/archsight/web/doc/computed_annotations.md +316 -0
  65. data/lib/archsight/web/doc/icons.md +303 -0
  66. data/lib/archsight/web/doc/index.md.erb +74 -0
  67. data/lib/archsight/web/doc/modeling.md +200 -0
  68. data/lib/archsight/web/doc/search.md +227 -0
  69. data/lib/archsight/web/doc/togaf.md +255 -0
  70. data/lib/archsight/web/doc/tool.md +90 -0
  71. data/lib/archsight/web/public/css/artifact.css +985 -0
  72. data/lib/archsight/web/public/css/base.css +201 -0
  73. data/lib/archsight/web/public/css/graph.css +106 -0
  74. data/lib/archsight/web/public/css/highlight.min.css +10 -0
  75. data/lib/archsight/web/public/css/iconoir.css +22 -0
  76. data/lib/archsight/web/public/css/instance.css +329 -0
  77. data/lib/archsight/web/public/css/layout.css +421 -0
  78. data/lib/archsight/web/public/css/mermaid-layers.css +188 -0
  79. data/lib/archsight/web/public/css/pico.min.css +4 -0
  80. data/lib/archsight/web/public/favicon.ico +0 -0
  81. data/lib/archsight/web/public/img/archimate.png +0 -0
  82. data/lib/archsight/web/public/img/togaf-high-level.png +0 -0
  83. data/lib/archsight/web/public/js/graph-zoom.js +18 -0
  84. data/lib/archsight/web/public/js/highlight.min.js +3899 -0
  85. data/lib/archsight/web/public/js/htmx.min.js +1 -0
  86. data/lib/archsight/web/public/js/mermaid-init.js +88 -0
  87. data/lib/archsight/web/public/js/mermaid.min.js +2811 -0
  88. data/lib/archsight/web/public/js/sparkline.js +42 -0
  89. data/lib/archsight/web/public/js/svg-pan-zoom.min.js +3 -0
  90. data/lib/archsight/web/public/js/svg-zoom-controls.js +93 -0
  91. data/lib/archsight/web/views/index.haml +12 -0
  92. data/lib/archsight/web/views/partials/artifact/_activity.haml +55 -0
  93. data/lib/archsight/web/views/partials/artifact/_agentic.haml +25 -0
  94. data/lib/archsight/web/views/partials/artifact/_deployment.haml +29 -0
  95. data/lib/archsight/web/views/partials/artifact/_git_info.haml +16 -0
  96. data/lib/archsight/web/views/partials/artifact/_language_stats.haml +53 -0
  97. data/lib/archsight/web/views/partials/artifact/_links.haml +24 -0
  98. data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +26 -0
  99. data/lib/archsight/web/views/partials/artifact/_repositories.haml +55 -0
  100. data/lib/archsight/web/views/partials/artifact/_team.haml +83 -0
  101. data/lib/archsight/web/views/partials/artifact/_workflow.haml +69 -0
  102. data/lib/archsight/web/views/partials/components/_activity.haml +37 -0
  103. data/lib/archsight/web/views/partials/components/_git.haml +17 -0
  104. data/lib/archsight/web/views/partials/components/_jira.haml +18 -0
  105. data/lib/archsight/web/views/partials/components/_languages.haml +29 -0
  106. data/lib/archsight/web/views/partials/components/_owner.haml +15 -0
  107. data/lib/archsight/web/views/partials/components/_repositories.haml +37 -0
  108. data/lib/archsight/web/views/partials/components/_status.haml +23 -0
  109. data/lib/archsight/web/views/partials/instance/_detail.haml +99 -0
  110. data/lib/archsight/web/views/partials/instance/_graph.haml +6 -0
  111. data/lib/archsight/web/views/partials/instance/_list.haml +84 -0
  112. data/lib/archsight/web/views/partials/instance/_relations.haml +43 -0
  113. data/lib/archsight/web/views/partials/instance/_requirements.haml +41 -0
  114. data/lib/archsight/web/views/partials/instance/_view_detail.haml +57 -0
  115. data/lib/archsight/web/views/partials/layout/_content.haml +40 -0
  116. data/lib/archsight/web/views/partials/layout/_error.haml +22 -0
  117. data/lib/archsight/web/views/partials/layout/_head.haml +24 -0
  118. data/lib/archsight/web/views/partials/layout/_navigation.haml +20 -0
  119. data/lib/archsight/web/views/partials/layout/_sidebar.haml +27 -0
  120. data/lib/archsight/web/views/search.haml +53 -0
  121. data/lib/archsight.rb +17 -0
  122. metadata +311 -0
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ComputedRelationResolver provides methods for traversing resource relations.
4
+ # It mirrors the relation traversal operators in the query language:
5
+ # - outgoing (->): Direct outgoing relations
6
+ # - outgoing_transitive (~>): Transitive outgoing relations
7
+ # - incoming (<-): Direct incoming relations
8
+ # - incoming_transitive (<~): Transitive incoming relations
9
+ #
10
+ # Filter parameter can be:
11
+ # - Symbol: Simple kind filter (e.g., :TechnologyArtifact)
12
+ # - String: Query selector (e.g., 'TechnologyArtifact: activity/status == "active"')
13
+ class Archsight::Annotations::ComputedRelationResolver
14
+ MAX_DEPTH = 10
15
+
16
+ def initialize(instance, database)
17
+ @instance = instance
18
+ @database = database
19
+ @query_cache = {}
20
+ end
21
+
22
+ # Get direct outgoing relations (-> Kind)
23
+ # @param filter [Symbol, String, nil] Optional kind filter (Symbol) or query selector (String)
24
+ # @return [Array] Array of related instances
25
+ def outgoing(filter = nil)
26
+ results = []
27
+
28
+ @instance.class.relations.each do |_verb, kind_name, _klass_name|
29
+ rels = @instance.relations(_verb, kind_name)
30
+ rels.each do |rel|
31
+ results << rel if matches_filter?(rel, filter)
32
+ end
33
+ end
34
+
35
+ results.uniq
36
+ end
37
+
38
+ # Get transitive outgoing relations (~> Kind)
39
+ # Follows all relation chains up to max_depth
40
+ # @param filter [Symbol, String, nil] Optional kind filter (Symbol) or query selector (String)
41
+ # @param max_depth [Integer] Maximum traversal depth (default 10)
42
+ # @return [Array] Array of transitively related instances
43
+ def outgoing_transitive(filter = nil, max_depth: MAX_DEPTH)
44
+ visited = Set.new
45
+ results = []
46
+
47
+ collect_transitive_outgoing(@instance, filter, visited, 0, max_depth, results)
48
+ results.uniq
49
+ end
50
+
51
+ # Get direct incoming relations (<- Kind)
52
+ # Uses the references array maintained during relation resolution
53
+ # @param filter [Symbol, String, nil] Optional kind filter (Symbol) or query selector (String)
54
+ # @return [Array] Array of instances that reference this one
55
+ def incoming(filter = nil)
56
+ refs = @instance.references || []
57
+ # Extract instances from reference hashes
58
+ instances = refs.map { |ref| ref.is_a?(Hash) ? ref[:instance] : ref }.compact
59
+
60
+ if filter.nil?
61
+ instances
62
+ else
63
+ instances.select { |ref| matches_filter?(ref, filter) }
64
+ end
65
+ end
66
+
67
+ # Get transitive incoming relations (<~ Kind)
68
+ # Follows all reverse relation chains up to max_depth
69
+ # @param filter [Symbol, String, nil] Optional kind filter (Symbol) or query selector (String)
70
+ # @param max_depth [Integer] Maximum traversal depth (default 10)
71
+ # @return [Array] Array of instances that transitively reference this one
72
+ def incoming_transitive(filter = nil, max_depth: MAX_DEPTH)
73
+ visited = Set.new
74
+ results = []
75
+
76
+ collect_transitive_incoming(@instance, filter, visited, 0, max_depth, results)
77
+ results.uniq
78
+ end
79
+
80
+ private
81
+
82
+ # Check if an instance matches the given filter
83
+ # @param instance [Object] The instance to check
84
+ # @param filter [Symbol, String, nil] Kind filter or query selector
85
+ # @return [Boolean] true if instance matches
86
+ def matches_filter?(instance, filter)
87
+ return true if filter.nil?
88
+
89
+ instance_kind = instance.class.name.split("::").last
90
+
91
+ if filter.is_a?(Symbol)
92
+ # Simple kind check
93
+ instance_kind == filter.to_s
94
+ else
95
+ # Query selector - parse and evaluate
96
+ query_node = parse_query(filter)
97
+ evaluator.matches?(query_node, instance)
98
+ end
99
+ end
100
+
101
+ # Parse a query string (with caching)
102
+ def parse_query(query_string)
103
+ @query_cache[query_string] ||= begin
104
+ require_relative "../query/lexer"
105
+ require_relative "../query/parser"
106
+ tokens = Archsight::Query::Lexer.new(query_string).tokenize
107
+ Archsight::Query::Parser.new(tokens).parse
108
+ end
109
+ end
110
+
111
+ # Get or create the query evaluator
112
+ def evaluator
113
+ @evaluator ||= begin
114
+ require_relative "../query/evaluator"
115
+ Archsight::Query::Evaluator.new(@database)
116
+ end
117
+ end
118
+
119
+ # Recursively collect transitive outgoing relations
120
+ def collect_transitive_outgoing(inst, filter, visited, depth, max_depth, results)
121
+ return if depth >= max_depth
122
+
123
+ key = "#{inst.class}/#{inst.name}"
124
+ return if visited.include?(key)
125
+
126
+ visited.add(key)
127
+
128
+ inst.class.relations.each do |verb, kind_name, _klass_name|
129
+ rels = inst.relations(verb, kind_name)
130
+ rels.each do |rel|
131
+ # Add to results if matches filter (or no filter)
132
+ results << rel if matches_filter?(rel, filter)
133
+
134
+ # Continue traversal (regardless of whether this matched)
135
+ collect_transitive_outgoing(rel, filter, visited.dup, depth + 1, max_depth, results)
136
+ end
137
+ end
138
+ end
139
+
140
+ # Recursively collect transitive incoming relations
141
+ def collect_transitive_incoming(inst, filter, visited, depth, max_depth, results)
142
+ return if depth >= max_depth
143
+
144
+ key = "#{inst.class}/#{inst.name}"
145
+ return if visited.include?(key)
146
+
147
+ visited.add(key)
148
+
149
+ refs = inst.references || []
150
+ # Extract instances from reference hashes
151
+ instances = refs.map { |ref| ref.is_a?(Hash) ? ref[:instance] : ref }.compact
152
+ instances.each do |ref|
153
+ # Add to results if matches filter (or no filter)
154
+ results << ref if matches_filter?(ref, filter)
155
+
156
+ # Continue traversal (regardless of whether this matched)
157
+ collect_transitive_incoming(ref, filter, visited.dup, depth + 1, max_depth, results)
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Archsight
6
+ class CLI < Thor
7
+ class_option :resources,
8
+ aliases: "-r",
9
+ type: :string,
10
+ desc: "Path to resources directory (default: ARCHSIGHT_RESOURCES_DIR or current directory)"
11
+
12
+ desc "web", "Start the web server"
13
+ option :port, aliases: "-p", type: :numeric, default: 4567, desc: "Port to listen on"
14
+ option :host, aliases: "-H", type: :string, default: "localhost", desc: "Host to bind to"
15
+ def web
16
+ configure_resources
17
+ require "archsight/web/application"
18
+ Archsight::Web::Application.run!(port: options[:port], bind: options[:host])
19
+ end
20
+
21
+ desc "lint", "Validate architecture resources"
22
+ def lint
23
+ configure_resources
24
+ require "archsight/database"
25
+ require "archsight/linter"
26
+ require "archsight/helpers"
27
+
28
+ db = Archsight::Database.new(Archsight.resources_dir, compute_annotations: false, verbose: true)
29
+ begin
30
+ db.reload!
31
+ rescue Archsight::ResourceError => e
32
+ display_error_with_context(e.to_s)
33
+ exit 1
34
+ end
35
+
36
+ linter = Archsight::Linter.new(db)
37
+ errors = linter.validate
38
+
39
+ if errors.any?
40
+ puts "Validation Errors (#{errors.count}):"
41
+ errors.each { |error| display_error_with_context(error) }
42
+ exit 1
43
+ end
44
+
45
+ puts "All validations passed!"
46
+ end
47
+
48
+ desc "template [KIND]", "Generate a YAML template for a resource kind"
49
+ def template(kind = nil)
50
+ require "archsight/template"
51
+ require "archsight/resources"
52
+
53
+ if kind.nil?
54
+ list_kinds
55
+ else
56
+ puts Archsight::Template.generate(kind)
57
+ end
58
+ end
59
+
60
+ desc "console", "Start an interactive console"
61
+ def console
62
+ configure_resources
63
+ require "archsight/database"
64
+ require "irb"
65
+
66
+ db = Archsight::Database.new(Archsight.resources_dir, verbose: true)
67
+ db.reload!
68
+
69
+ puts "Database loaded. Available: db"
70
+ binding.irb
71
+ end
72
+
73
+ desc "version", "Show version"
74
+ def version
75
+ puts "archsight #{Archsight::VERSION}"
76
+ end
77
+
78
+ default_task :version
79
+
80
+ private
81
+
82
+ def configure_resources
83
+ Archsight.resources_dir = options[:resources] if options[:resources]
84
+ end
85
+
86
+ def list_kinds
87
+ puts "Available resource kinds:\n\n"
88
+ Archsight::Resources.resource_classes.each_key { |kind| puts " - #{kind}" }
89
+ puts "\nUsage: archsight template <kind>"
90
+ end
91
+
92
+ def display_error_with_context(error_string)
93
+ # Parse error to extract file path and line number
94
+ if error_string =~ /^(.+?):(\d+):/
95
+ file_path = ::Regexp.last_match(1)
96
+ line_number = ::Regexp.last_match(2).to_i
97
+ puts "\n#{error_string}"
98
+ show_file_context(file_path, line_number)
99
+ else
100
+ puts error_string
101
+ end
102
+ end
103
+
104
+ def show_file_context(file_path, line_number, context_lines: 3)
105
+ return unless File.exist?(file_path)
106
+
107
+ lines = File.readlines(file_path)
108
+ start_line = [line_number - context_lines - 1, 0].max
109
+ end_line = [line_number + context_lines - 1, lines.length - 1].min
110
+
111
+ puts ""
112
+ (start_line..end_line).each do |i|
113
+ line_num = i + 1
114
+ prefix = line_num == line_number ? ">> " : " "
115
+ puts format("%s%4d | %s", prefix, line_num, lines[i])
116
+ end
117
+ puts ""
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Archsight
4
+ class Configuration
5
+ attr_accessor :resources_dir, :verbose, :verify, :compute_annotations
6
+
7
+ def initialize
8
+ @resources_dir = ENV["ARCHSIGHT_RESOURCES_DIR"] || Dir.pwd
9
+ @verbose = true
10
+ @verify = true
11
+ @compute_annotations = true
12
+ end
13
+ end
14
+
15
+ class << self
16
+ def configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ def configure
21
+ yield(configuration)
22
+ end
23
+
24
+ def resources_dir
25
+ configuration.resources_dir
26
+ end
27
+
28
+ def resources_dir=(path)
29
+ configuration.resources_dir = File.absolute_path(path)
30
+ end
31
+
32
+ def reset_configuration!
33
+ @configuration = Configuration.new
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "graph"
5
+ require_relative "resources"
6
+ require_relative "query"
7
+
8
+ module Archsight
9
+ # LineReference combines a path and line reference
10
+ class LineReference
11
+ attr_accessor :path, :line_no
12
+
13
+ def initialize(path, line_no)
14
+ @path = path
15
+ @line_no = line_no
16
+ end
17
+
18
+ def to_s
19
+ "#{@path}:#{@line_no}"
20
+ end
21
+
22
+ def at_line(line_no)
23
+ self.class.new(@path, line_no)
24
+ end
25
+ end
26
+
27
+ # ResourceError is an error with a path and line no attached
28
+ class ResourceError < StandardError
29
+ attr_reader :ref, :message
30
+
31
+ def initialize(msg, ref)
32
+ super(msg)
33
+ @message = msg
34
+ @ref = ref
35
+ end
36
+
37
+ def to_s
38
+ "#{ref}: #{super}"
39
+ end
40
+ end
41
+
42
+ # Database loads yaml files and folders to create an in-memory representation
43
+ # of the structure. The loading and parsing of files will raise errors
44
+ # if invalid data is passed.
45
+ class Database
46
+ attr_accessor :instances, :verbose, :verify, :compute_annotations
47
+
48
+ def initialize(path, verbose: false, verify: true, compute_annotations: true)
49
+ @path = path
50
+ @verbose = verbose
51
+ @verify = verify
52
+ @compute_annotations = compute_annotations
53
+ @instances = {}
54
+ end
55
+
56
+ def reload!
57
+ @instances = {}
58
+
59
+ # load all resources
60
+ Dir.glob(File.join(@path, "**/*.yaml")).each do |path|
61
+ @current_ref = LineReference.new(path, 0)
62
+ puts "parsing #{path}..." if @verbose
63
+ load_file(path)
64
+ end
65
+
66
+ verify! if @verify
67
+ compute_all_annotations! if @verify && @compute_annotations
68
+ rescue Psych::SyntaxError => e
69
+ # Wrap YAML syntax errors in ResourceError for consistent handling
70
+ ref = LineReference.new(e.file || @current_ref&.path || "unknown", e.line || 0)
71
+ Kernel.raise(ResourceError.new(e.problem || e.message, ref))
72
+ end
73
+
74
+ def instances_by_kind(kind)
75
+ @instances[Archsight::Resources[kind]] || {}
76
+ end
77
+
78
+ def instance_by_kind(kind, instance)
79
+ @instances[Archsight::Resources[kind]][instance]
80
+ end
81
+
82
+ # Collect unique annotation values across all instances of a kind
83
+ def annotation_values(kind, annotation)
84
+ instances = instances_by_kind(kind).values
85
+ values = instances.flat_map { |inst| Array(annotation.value_for(inst)) }
86
+ values.compact.uniq.sort
87
+ end
88
+
89
+ # Get filterable annotations with their values for a kind (excludes empty)
90
+ def filters_for_kind(kind)
91
+ klass = Archsight::Resources[kind]
92
+ return [] unless klass
93
+
94
+ klass.filterable_annotations
95
+ .map { |a| [a, annotation_values(kind, a)] }
96
+ .reject { |_, values| values.empty? }
97
+ end
98
+
99
+ # Execute a query string and return matching instances
100
+ def query(query_string)
101
+ q = Archsight::Query.parse(query_string)
102
+ q.filter(self)
103
+ end
104
+
105
+ # Check if a specific instance matches a query
106
+ def instance_matches?(instance, query_string)
107
+ q = Archsight::Query.parse(query_string)
108
+ q.matches?(instance, database: self)
109
+ end
110
+
111
+ private
112
+
113
+ def create_valid_instance(obj)
114
+ raise("invalid api version") if obj["apiVersion"] != "architecture/v1alpha1"
115
+
116
+ kind = obj["kind"] || raise("kind not defined")
117
+ klass = Archsight::Resources[kind] || raise("#{kind} is not a valid kind")
118
+ inst = klass.new(obj, @current_ref)
119
+ inst.name || raise("metadata name of #{kind} not present")
120
+ inst
121
+ end
122
+
123
+ def load_file(path)
124
+ File.open(path, "r") do |f|
125
+ YAML.parse_stream(f) do |node|
126
+ @current_ref = @current_ref.at_line(node.children.first.start_line)
127
+ obj = node.to_ruby
128
+ next unless obj # skip empty / unknown documents
129
+
130
+ self << create_valid_instance(obj)
131
+ end
132
+ end
133
+ end
134
+
135
+ def <<(inst)
136
+ inst.verify!
137
+ klass = inst.class
138
+ @instances[klass] ||= {}
139
+ if (existing_inst = @instances[klass][inst.name])
140
+ existing_inst.merge!(inst)
141
+ else
142
+ @instances[klass][inst.name] = inst
143
+ end
144
+ end
145
+
146
+ # raise provides a helper for better error messages including current path and line no
147
+ def raise(msg)
148
+ Kernel.raise(ResourceError.new(msg, @current_ref))
149
+ end
150
+
151
+ # raise_for provides error messages with the instance's path reference
152
+ def raise_for(inst, msg)
153
+ Kernel.raise(ResourceError.new(msg, inst.path_ref))
154
+ end
155
+
156
+ # verify and resolve relations between resources
157
+ def verify!
158
+ @instances.each_value do |instances|
159
+ instances.each_value do |inst|
160
+ verify_instance_relations!(inst)
161
+ end
162
+ end
163
+ end
164
+
165
+ def verify_instance_relations!(inst)
166
+ inst.class.relations.each do |verb, kind, klass_name|
167
+ rels = inst.relations(verb, kind).map do |rel_name|
168
+ rel_klass = Archsight::Resources[klass_name] || raise_for(inst, "#{klass_name} is not a valid relation kind")
169
+ kind_display = rel_klass.to_s.sub(/^Archsight::Resources::/, "")
170
+ @instances[rel_klass] || raise_for(inst, "#{rel_name} is not defined as kind #{kind_display}")
171
+ @instances[rel_klass][rel_name] || raise_for(inst, "#{rel_name} is not defined as kind #{kind_display}")
172
+ end
173
+ inst.set_relations(verb, kind, rels) unless rels.empty?
174
+ end
175
+ end
176
+
177
+ # Compute all computed annotations for all instances
178
+ def compute_all_annotations!
179
+ manager = Archsight::Annotations::ComputedManager.new(self)
180
+ manager.compute_all!
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resources"
4
+ require_relative "template"
5
+
6
+ module Archsight
7
+ # Documentation generates markdown documentation for architecture resources
8
+ class Documentation
9
+ # Layer display order (top to bottom)
10
+ LAYER_ORDER = %w[motivation strategy business application technology].freeze
11
+
12
+ # Layer display names
13
+ LAYER_NAMES = {
14
+ "motivation" => "Motivation Layer",
15
+ "strategy" => "Strategy Layer",
16
+ "business" => "Business Layer",
17
+ "application" => "Application Layer",
18
+ "technology" => "Technology Layer"
19
+ }.freeze
20
+
21
+ # Resource kinds to exclude from the diagram
22
+ EXCLUDED_KINDS = %w[View].freeze
23
+
24
+ @mermaid_cache = nil
25
+
26
+ class << self
27
+ attr_accessor :mermaid_cache
28
+
29
+ # Generate mermaid flowchart diagram showing all resource types and relationships
30
+ # Results are cached for performance
31
+ # @return [String] Mermaid flowchart diagram
32
+ def generate_mermaid_diagram
33
+ return @mermaid_cache if @mermaid_cache
34
+
35
+ @mermaid_cache = build_mermaid_diagram
36
+ end
37
+
38
+ # Clear the mermaid cache (call when resources change)
39
+ def clear_cache
40
+ @mermaid_cache = nil
41
+ end
42
+
43
+ # Build the mermaid diagram from resource definitions
44
+ # Layer colors are defined in CSS (public/css/mermaid-layers.css)
45
+ def build_mermaid_diagram
46
+ lines = []
47
+ lines << "flowchart TB"
48
+
49
+ # Group resources by layer (excluding certain kinds)
50
+ resources_by_layer = Hash.new { |h, k| h[k] = [] }
51
+ Archsight::Resources.each do |kind_name|
52
+ next if EXCLUDED_KINDS.include?(kind_name.to_s)
53
+
54
+ klass = Archsight::Resources.const_get(kind_name)
55
+ layer = klass.layer
56
+ next unless LAYER_ORDER.include?(layer) # Skip resources in unlisted layers
57
+
58
+ resources_by_layer[layer] << kind_name.to_s
59
+ end
60
+
61
+ # Generate subgraphs for each layer in order
62
+ LAYER_ORDER.each do |layer|
63
+ next unless resources_by_layer.key?(layer)
64
+
65
+ kinds = resources_by_layer[layer].sort
66
+ next if kinds.empty?
67
+
68
+ lines << ""
69
+ lines << " subgraph #{layer.capitalize}[\"#{LAYER_NAMES[layer]}\"]"
70
+ kinds.each do |kind|
71
+ lines << " #{kind}:::#{layer}"
72
+ end
73
+ lines << " end"
74
+ end
75
+
76
+ # Collect all relations and deduplicate
77
+ relations = collect_relations
78
+ lines << ""
79
+ relations.each do |from_kind, verb, to_kind|
80
+ lines << " #{from_kind} -->|#{verb}| #{to_kind}"
81
+ end
82
+
83
+ # Add click handlers for each kind
84
+ lines << ""
85
+ all_kinds = resources_by_layer.values.flatten
86
+ all_kinds.sort.each do |kind_name|
87
+ lines << " click #{kind_name} \"/kinds/#{kind_name}\""
88
+ end
89
+
90
+ lines.join("\n")
91
+ end
92
+
93
+ # Collect all unique relations from resource definitions
94
+ # @return [Array<Array>] Array of [from_kind, verb, to_kind] tuples
95
+ def collect_relations
96
+ relations = []
97
+ seen = Set.new
98
+
99
+ Archsight::Resources.each do |kind_name|
100
+ # Skip excluded kinds
101
+ next if EXCLUDED_KINDS.include?(kind_name.to_s)
102
+
103
+ klass = Archsight::Resources.const_get(kind_name)
104
+ # Skip kinds not in a displayed layer
105
+ next unless LAYER_ORDER.include?(klass.layer)
106
+
107
+ klass.relations.each do |verb, _relation_kind, target_klass|
108
+ # Skip relations to excluded kinds
109
+ next if EXCLUDED_KINDS.include?(target_klass.to_s)
110
+
111
+ key = "#{kind_name}|#{verb}|#{target_klass}"
112
+ next if seen.include?(key)
113
+
114
+ seen.add(key)
115
+ relations << [kind_name.to_s, verb.to_s.delete_prefix(":"), target_klass.to_s]
116
+ end
117
+ end
118
+
119
+ relations.sort_by { |from, verb, to| [from, to, verb] }
120
+ end
121
+
122
+ def generate(kind_name)
123
+ klass = Archsight::Resources[kind_name.to_s]
124
+ raise "Unknown resource kind '#{kind_name}'" unless klass
125
+
126
+ md = []
127
+ md << "# #{kind_name}\n"
128
+ md << klass.description if klass.description
129
+ md << "\n## Annotations\n"
130
+ md << generate_annotations_table(klass)
131
+ md << "\n## Relations\n"
132
+ md << generate_relations_table(klass)
133
+ md << "\n## Example\n"
134
+ md << "```yaml\n#{Archsight::Template.generate(kind_name)}```"
135
+ md.compact.join("\n")
136
+ end
137
+
138
+ def generate_annotations_table(klass)
139
+ annotations = klass.annotations.reject(&:pattern?)
140
+ return "_No annotations defined._" if annotations.empty?
141
+
142
+ rows = ["| Annotation | Description | Values |", "|------------|-------------|--------|"]
143
+ annotations.each do |a|
144
+ values = format_values(a)
145
+ rows << "| `#{a.key}` | #{a.description || "-"} | #{values} |"
146
+ end
147
+ rows.join("\n")
148
+ end
149
+
150
+ def generate_relations_table(klass)
151
+ return "_No relations defined._" if klass.relations.empty?
152
+
153
+ rows = ["| Relation | Target | Kind |", "|----------|--------|------|"]
154
+ klass.relations.each do |verb, kind, target_klass|
155
+ rows << "| #{verb} | #{target_klass} | #{kind} |"
156
+ end
157
+ rows.join("\n")
158
+ end
159
+
160
+ def format_values(annotation)
161
+ if annotation.enum
162
+ annotation.enum.join(", ")
163
+ elsif annotation.type
164
+ annotation.type.to_s.split("::").last
165
+ else
166
+ "-"
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end