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,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ComputedAggregators provides static methods for aggregating annotation values.
4
+ # These functions handle nil values gracefully and convert types as needed.
5
+ module Archsight::Annotations::ComputedAggregators
6
+ class << self
7
+ # Sum numeric values
8
+ # @param values [Array] Array of values to sum
9
+ # @return [Float, nil] Sum of values converted to float, nil if no valid values
10
+ def sum(values)
11
+ numeric_values = to_numeric(values)
12
+ return nil if numeric_values.empty?
13
+
14
+ numeric_values.sum
15
+ end
16
+
17
+ # Count non-nil values
18
+ # @param values [Array] Array of values to count
19
+ # @return [Integer] Count of non-nil values
20
+ def count(values)
21
+ values.compact.length
22
+ end
23
+
24
+ # Calculate average of numeric values
25
+ # @param values [Array] Array of values to average
26
+ # @return [Float, nil] Average of values, nil if no valid values
27
+ def avg(values)
28
+ numeric_values = to_numeric(values)
29
+ return nil if numeric_values.empty?
30
+
31
+ numeric_values.sum / numeric_values.length.to_f
32
+ end
33
+
34
+ # Find minimum numeric value
35
+ # @param values [Array] Array of values
36
+ # @return [Float, nil] Minimum value, nil if no valid values
37
+ def min(values)
38
+ numeric_values = to_numeric(values)
39
+ return nil if numeric_values.empty?
40
+
41
+ numeric_values.min
42
+ end
43
+
44
+ # Find maximum numeric value
45
+ # @param values [Array] Array of values
46
+ # @return [Float, nil] Maximum value, nil if no valid values
47
+ def max(values)
48
+ numeric_values = to_numeric(values)
49
+ return nil if numeric_values.empty?
50
+
51
+ numeric_values.max
52
+ end
53
+
54
+ # Collect unique values, flattening arrays and sorting
55
+ # @param values [Array] Array of values (may contain nested arrays)
56
+ # @return [Array] Unique sorted values
57
+ def collect(values)
58
+ flat_values = values.flatten.compact
59
+ # Handle comma-separated strings (list annotations)
60
+ expanded = flat_values.flat_map do |v|
61
+ v.is_a?(String) ? v.split(",").map(&:strip) : v
62
+ end
63
+ expanded.compact.uniq.sort_by(&:to_s)
64
+ end
65
+
66
+ # Get first non-nil value
67
+ # @param values [Array] Array of values
68
+ # @return [Object, nil] First non-nil value
69
+ def first(values)
70
+ values.compact.first
71
+ end
72
+
73
+ # Find most common value (mode)
74
+ # @param values [Array] Array of values (may contain nested arrays)
75
+ # @return [Object, nil] Most frequent value, nil if no values
76
+ def most_common(values)
77
+ flat_values = values.flatten.compact
78
+ return nil if flat_values.empty?
79
+
80
+ # Handle comma-separated strings (list annotations)
81
+ expanded = flat_values.flat_map do |v|
82
+ v.is_a?(String) ? v.split(",").map(&:strip) : v
83
+ end
84
+
85
+ expanded.compact
86
+ .group_by(&:itself)
87
+ .max_by { |_, group| group.length }
88
+ &.first
89
+ end
90
+
91
+ private
92
+
93
+ # Convert values to numeric (float), filtering out non-convertible values
94
+ def to_numeric(values)
95
+ values.compact.filter_map do |v|
96
+ case v
97
+ when Numeric
98
+ v.to_f
99
+ when String
100
+ begin
101
+ Float(v)
102
+ rescue StandardError
103
+ nil
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "email_recipient"
4
+
5
+ # Annotation represents a single annotation definition with its schema and behavior
6
+ class Archsight::Annotations::Annotation
7
+ attr_reader :key, :description, :filter, :format, :enum, :sidebar, :type, :list
8
+
9
+ def initialize(key, options = {})
10
+ @key = key
11
+ @description = options[:description]
12
+ @explicit_title = options[:title]
13
+ @filter = options[:filter]
14
+ @enum = options[:enum]
15
+ @sidebar = options.fetch(:sidebar, true)
16
+ @list = options.fetch(:list, false)
17
+ @type = options[:type]
18
+
19
+ # Auto-add filter if enum present
20
+ @filter ||= :word if @enum
21
+
22
+ # Derive format from filter if not explicitly set
23
+ @format = options[:format] || derive_format
24
+
25
+ # Build regex for pattern annotations
26
+ @regex = build_regex if pattern?
27
+ end
28
+
29
+ # === Schema Methods ===
30
+
31
+ def pattern?
32
+ key.include?("*")
33
+ end
34
+
35
+ def matches?(test_key)
36
+ pattern? ? @regex.match?(test_key) : key == test_key
37
+ end
38
+
39
+ def title
40
+ @explicit_title || key.split("/").last.capitalize
41
+ end
42
+
43
+ def filterable?
44
+ @filter && @sidebar != false
45
+ end
46
+
47
+ def list?
48
+ @filter == :list
49
+ end
50
+
51
+ def list_display?
52
+ @list == true
53
+ end
54
+
55
+ def has_validation?
56
+ @enum || @type.is_a?(Class)
57
+ end
58
+
59
+ # === Value Methods (for instance values) ===
60
+
61
+ # Get value(s) from instance
62
+ # Returns array for list annotations, coerced single value otherwise
63
+ def value_for(instance)
64
+ raw = instance.annotations[key]
65
+
66
+ if list?
67
+ return [] if raw.nil? || raw.to_s.empty?
68
+
69
+ raw.to_s.split(/,|\n/).map(&:strip).reject(&:empty?)
70
+ else
71
+ return nil if raw.nil?
72
+
73
+ case @type
74
+ when Integer then raw.to_i
75
+ when Float then raw.to_f
76
+ else raw
77
+ end
78
+ end
79
+ end
80
+
81
+ # Validate a value and return array of error messages (empty if valid)
82
+ def validate(value)
83
+ errors = []
84
+ return errors if value.nil?
85
+
86
+ # Check enum constraint
87
+ if @enum
88
+ values = list? ? value.to_s.split(",").map(&:strip) : [value.to_s]
89
+ invalid_values = values.reject { |v| @enum.include?(v) }
90
+ invalid_values.each do |v|
91
+ errors << "invalid value '#{v}'. Expected one of: #{@enum.join(", ")}"
92
+ end
93
+ end
94
+
95
+ # Check type constraint
96
+ if @type.is_a?(Class) && errors.empty?
97
+ values_to_check = list? ? value.to_s.split(/,|\n/).map(&:strip).reject(&:empty?) : [value.to_s]
98
+
99
+ values_to_check.each do |string_value|
100
+ valid = case @type.to_s
101
+ when "Integer"
102
+ string_value.match?(/\A-?\d+\z/)
103
+ when "Float"
104
+ string_value.match?(/\A-?\d+(\.\d+)?\z/)
105
+ when "URI"
106
+ begin
107
+ URI.parse(string_value)
108
+ string_value.match?(%r{\Ahttps?://})
109
+ rescue URI::InvalidURIError
110
+ false
111
+ end
112
+ when "Archsight::Annotations::EmailRecipient"
113
+ Archsight::Annotations::EmailRecipient.valid?(string_value)
114
+ else
115
+ true
116
+ end
117
+ errors << "invalid value '#{string_value}'. #{type_error_message}" unless valid
118
+ end
119
+ end
120
+
121
+ errors
122
+ end
123
+
124
+ # Check if value is valid (convenience method)
125
+ def valid?(value)
126
+ validate(value).empty?
127
+ end
128
+
129
+ def markdown?
130
+ @format == :markdown
131
+ end
132
+
133
+ # Example value for templates
134
+ def example_value
135
+ if @enum
136
+ @enum.first || "TODO"
137
+ elsif @type == Float
138
+ 0.0
139
+ elsif @type == Integer
140
+ 0
141
+ else
142
+ "TODO"
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def type_error_message
149
+ case @type.to_s
150
+ when "URI" then "Expected valid HTTP/HTTPS URL"
151
+ when "Integer" then "Expected an integer value"
152
+ when "Float" then "Expected a float value"
153
+ when "Archsight::Annotations::EmailRecipient" then 'Expected email format: "Name <email@domain.com>" or "email@domain.com"'
154
+ else "Invalid value for type #{@type}"
155
+ end
156
+ end
157
+
158
+ def derive_format
159
+ case @filter
160
+ when :word then :tag_word
161
+ when :list then :tag_list
162
+ end
163
+ end
164
+
165
+ def build_regex
166
+ Regexp.new("^#{Regexp.escape(key).gsub('\*', ".+")}$")
167
+ end
168
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ # Architecture module adds common architecture annotations to resource classes
6
+ module Archsight::Annotations::Architecture
7
+ def self.included(base)
8
+ base.class_eval do
9
+ annotation "architecture/abbr",
10
+ description: "Abbreviation or short name",
11
+ title: "Abbreviation"
12
+ annotation "architecture/evidence",
13
+ description: "Supporting evidence or notes",
14
+ title: "Evidence",
15
+ format: :markdown
16
+ annotation "architecture/description",
17
+ description: "Textual description of the interface",
18
+ title: "Description",
19
+ format: :markdown
20
+ annotation "architecture/documentation",
21
+ description: "Documentation URL or reference",
22
+ title: "Documentation",
23
+ type: URI
24
+ annotation "architecture/tags",
25
+ description: "Comma-separated tags",
26
+ filter: :list,
27
+ title: "Tags"
28
+ annotation "architecture/encoding",
29
+ description: "Data encoding format",
30
+ filter: :list,
31
+ title: "Encoding"
32
+ annotation "architecture/title",
33
+ description: "Interface title",
34
+ title: "Title"
35
+ annotation "architecture/openapi",
36
+ description: "OpenAPI specification version",
37
+ filter: :word,
38
+ title: "OpenAPI"
39
+ annotation "architecture/version",
40
+ description: "API or interface version",
41
+ filter: :word,
42
+ title: "Version",
43
+ sidebar: false
44
+ annotation "architecture/status",
45
+ description: "Lifecycle status (General-Availability, Early-Access, Development)",
46
+ filter: :word,
47
+ title: "Status"
48
+ annotation "architecture/visibility",
49
+ description: "API visibility (public, private)",
50
+ filter: :word,
51
+ enum: %w[public private],
52
+ title: "Visibility"
53
+ annotation "architecture/applicationSets",
54
+ description: "Related ArgoCD ApplicationSets",
55
+ title: "ApplicationSets",
56
+ format: :markdown
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Backup module adds backup-related annotations to resource classes
4
+ module Archsight::Annotations::Backup
5
+ def self.included(base)
6
+ base.class_eval do
7
+ annotation "backup/mode",
8
+ description: "Backup mode strategy",
9
+ title: "Backup Mode",
10
+ enum: %w[none full incremental continuous offsite not-needed]
11
+ annotation "backup/rto",
12
+ description: "Recovery Time Objective (RTO) in minutes - the maximum acceptable time to restore service after a failure",
13
+ title: "Backup RTO (min)",
14
+ type: Integer
15
+ annotation "backup/rpo",
16
+ description: "Recovery Point Objective (RPO) in minutes - the maximum acceptable amount of data loss measured in time",
17
+ title: "Backup RPO (min)",
18
+ type: Integer
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "aggregators"
4
+ require_relative "relation_resolver"
5
+
6
+ # Computed represents a computed annotation definition.
7
+ # It stores the key, description, optional type, and the computation block.
8
+ class Archsight::Annotations::Computed
9
+ attr_reader :key, :description, :type, :block
10
+
11
+ def initialize(key, description: nil, type: nil, &block)
12
+ @key = key
13
+ @description = description
14
+ @type = type
15
+ @block = block
16
+ end
17
+
18
+ # Check if this definition matches a given key
19
+ def matches?(other_key)
20
+ @key == other_key
21
+ end
22
+ end
23
+
24
+ # ComputedEvaluator provides the DSL context for computing annotation values.
25
+ # It exposes aggregation functions and relation traversal methods.
26
+ class Archsight::Annotations::ComputedEvaluator
27
+ def initialize(instance, database, manager)
28
+ @instance = instance
29
+ @database = database
30
+ @manager = manager
31
+ @resolver = Archsight::Annotations::ComputedRelationResolver.new(instance, database)
32
+ end
33
+
34
+ # Access a regular annotation value from the current instance
35
+ def annotation(key)
36
+ @instance.annotations[key]
37
+ end
38
+
39
+ # Access a computed annotation value (triggers computation if needed)
40
+ def computed(key)
41
+ @manager.compute_for_key(@instance, key)
42
+ end
43
+
44
+ # --- Relation Traversal Methods ---
45
+
46
+ # Get direct outgoing relations (-> Kind)
47
+ def outgoing(kind = nil)
48
+ @resolver.outgoing(kind)
49
+ end
50
+
51
+ # Get transitive outgoing relations (~> Kind)
52
+ def outgoing_transitive(kind = nil, max_depth: 10)
53
+ @resolver.outgoing_transitive(kind, max_depth: max_depth)
54
+ end
55
+
56
+ # Get direct incoming relations (<- Kind)
57
+ def incoming(kind = nil)
58
+ @resolver.incoming(kind)
59
+ end
60
+
61
+ # Get transitive incoming relations (<~ Kind)
62
+ def incoming_transitive(kind = nil, max_depth: 10)
63
+ @resolver.incoming_transitive(kind, max_depth: max_depth)
64
+ end
65
+
66
+ # --- Aggregation Functions ---
67
+
68
+ # Sum numeric annotation values from instances
69
+ # @param instances [Array] Array of resource instances
70
+ # @param key [String] Annotation key to extract values from
71
+ # @return [Float, nil] Sum of values or nil if no values
72
+ def sum(instances, key)
73
+ values = extract_values(instances, key)
74
+ Archsight::Annotations::ComputedAggregators.sum(values)
75
+ end
76
+
77
+ # Count instances or non-nil annotation values
78
+ # @param instances [Array] Array of resource instances
79
+ # @param key [String, nil] Optional annotation key; if nil, counts instances
80
+ # @return [Integer] Count
81
+ def count(instances, key = nil)
82
+ if key
83
+ values = extract_values(instances, key)
84
+ Archsight::Annotations::ComputedAggregators.count(values)
85
+ else
86
+ instances.length
87
+ end
88
+ end
89
+
90
+ # Average numeric annotation values
91
+ # @param instances [Array] Array of resource instances
92
+ # @param key [String] Annotation key to extract values from
93
+ # @return [Float, nil] Average or nil if no values
94
+ def avg(instances, key)
95
+ values = extract_values(instances, key)
96
+ Archsight::Annotations::ComputedAggregators.avg(values)
97
+ end
98
+
99
+ # Minimum numeric annotation value
100
+ # @param instances [Array] Array of resource instances
101
+ # @param key [String] Annotation key to extract values from
102
+ # @return [Float, nil] Minimum value or nil if no values
103
+ def min(instances, key)
104
+ values = extract_values(instances, key)
105
+ Archsight::Annotations::ComputedAggregators.min(values)
106
+ end
107
+
108
+ # Maximum numeric annotation value
109
+ # @param instances [Array] Array of resource instances
110
+ # @param key [String] Annotation key to extract values from
111
+ # @return [Float, nil] Maximum value or nil if no values
112
+ def max(instances, key)
113
+ values = extract_values(instances, key)
114
+ Archsight::Annotations::ComputedAggregators.max(values)
115
+ end
116
+
117
+ # Collect unique annotation values
118
+ # @param instances [Array] Array of resource instances
119
+ # @param key [String] Annotation key to extract values from
120
+ # @return [Array] Unique sorted values
121
+ def collect(instances, key)
122
+ values = extract_values(instances, key)
123
+ Archsight::Annotations::ComputedAggregators.collect(values)
124
+ end
125
+
126
+ # Get first non-nil annotation value
127
+ # @param instances [Array] Array of resource instances
128
+ # @param key [String] Annotation key to extract values from
129
+ # @return [Object, nil] First non-nil value
130
+ def first(instances, key)
131
+ values = extract_values(instances, key)
132
+ Archsight::Annotations::ComputedAggregators.first(values)
133
+ end
134
+
135
+ # Get most common annotation value (mode)
136
+ # @param instances [Array] Array of resource instances
137
+ # @param key [String] Annotation key to extract values from
138
+ # @return [Object, nil] Most frequent value
139
+ def most_common(instances, key)
140
+ values = extract_values(instances, key)
141
+ Archsight::Annotations::ComputedAggregators.most_common(values)
142
+ end
143
+
144
+ # Get an annotation value from an instance, triggering computation if needed
145
+ # @param instance [Object] Resource instance
146
+ # @param key [String] Annotation key to extract
147
+ # @return [Object, nil] Annotation value
148
+ def get(instance, key)
149
+ @manager.compute_for_key(instance, key) if instance.class.computed_annotations.any? { |d| d.matches?(key) }
150
+ instance.annotations[key]
151
+ end
152
+
153
+ private
154
+
155
+ # Extract annotation values from instances
156
+ # If the key corresponds to a computed annotation that hasn't been computed yet,
157
+ # trigger its computation to handle cross-kind dependencies
158
+ def extract_values(instances, key)
159
+ instances.map do |inst|
160
+ # Check if this is a computed annotation that needs to be computed
161
+ if inst.class.computed_annotations.any? { |d| d.matches?(key) }
162
+ # Trigger computation if not already computed
163
+ @manager.compute_for_key(inst, key)
164
+ end
165
+ inst.annotations[key]
166
+ end
167
+ end
168
+ end
169
+
170
+ # ComputedManager orchestrates the computation of all computed annotations.
171
+ # It handles lazy evaluation, caching, and cycle detection.
172
+ class Archsight::Annotations::ComputedManager
173
+ def initialize(database)
174
+ @database = database
175
+ @computed_cache = {} # { [instance_object_id, key] => value }
176
+ @computing = Set.new # For cycle detection
177
+ end
178
+
179
+ # Compute all computed annotations for all instances
180
+ def compute_all!
181
+ # Collect all resource classes that have computed annotations
182
+ @database.instances.each do |klass, instances_hash|
183
+ definitions = klass.computed_annotations
184
+ next if definitions.empty?
185
+
186
+ instances_hash.each_value do |instance|
187
+ definitions.each do |definition|
188
+ compute_for(instance, definition)
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ # Compute a specific annotation for an instance by key
195
+ def compute_for_key(instance, key)
196
+ definition = instance.class.computed_annotations.find { |d| d.matches?(key) }
197
+ return nil unless definition
198
+
199
+ compute_for(instance, definition)
200
+ end
201
+
202
+ # Compute a specific annotation for an instance
203
+ def compute_for(instance, definition)
204
+ cache_key = [instance.object_id, definition.key]
205
+
206
+ # Return cached value if available
207
+ return @computed_cache[cache_key] if @computed_cache.key?(cache_key)
208
+
209
+ # Cycle detection
210
+ raise "Circular dependency detected: #{definition.key} for #{instance.name}" if @computing.include?(cache_key)
211
+
212
+ @computing.add(cache_key)
213
+ begin
214
+ evaluator = Archsight::Annotations::ComputedEvaluator.new(instance, @database, self)
215
+ value = evaluator.instance_eval(&definition.block)
216
+
217
+ # Apply type coercion if specified
218
+ value = coerce_value(value, definition.type) if definition.type
219
+
220
+ # Cache the computed value (even if nil, to avoid recomputation)
221
+ @computed_cache[cache_key] = value
222
+
223
+ # Only store meaningful values to the instance annotations
224
+ # nil and empty arrays indicate "no data" and should not be stored
225
+ if meaningful_value?(value)
226
+ # Convert arrays to comma-separated strings for consistency with regular annotations
227
+ stored_value = value.is_a?(Array) ? value.join(", ") : value
228
+ instance.set_computed_annotation(definition.key, stored_value)
229
+ end
230
+
231
+ value
232
+ ensure
233
+ @computing.delete(cache_key)
234
+ end
235
+ end
236
+
237
+ private
238
+
239
+ # Check if a value is meaningful (should be stored)
240
+ # nil and empty collections indicate "no data" and should not be stored
241
+ def meaningful_value?(value)
242
+ return false if value.nil?
243
+ return false if value.is_a?(Array) && value.empty?
244
+ return false if value.is_a?(String) && value.empty?
245
+
246
+ true
247
+ end
248
+
249
+ # Coerce value to specified type
250
+ def coerce_value(value, type)
251
+ return nil if value.nil?
252
+
253
+ case type.to_s
254
+ when "Integer"
255
+ value.to_i
256
+ when "Float"
257
+ value.to_f
258
+ when "String"
259
+ value.to_s
260
+ else
261
+ value
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Custom type for email recipient validation
4
+ # Accepts: "Name <email@domain.com>" or "email@domain.com"
5
+ # Rejects: "Name" (no email)
6
+ class Archsight::Annotations::EmailRecipient
7
+ # RFC 5322 simplified email pattern
8
+ EMAIL_PATTERN = /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/
9
+ # Email recipient format: "Display Name <email@domain.com>"
10
+ RECIPIENT_PATTERN = /\A.+\s+<([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>\z/
11
+
12
+ def self.valid?(value)
13
+ return false if value.nil? || value.to_s.strip.empty?
14
+
15
+ str = value.to_s.strip
16
+ # Check if it's a full recipient format "Name <email>"
17
+ return true if str.match?(RECIPIENT_PATTERN)
18
+
19
+ # Check if it's just an email address
20
+ return true if str.match?(EMAIL_PATTERN)
21
+
22
+ false
23
+ end
24
+
25
+ def self.extract_email(value)
26
+ return nil if value.nil?
27
+
28
+ str = value.to_s.strip
29
+ if (match = str.match(RECIPIENT_PATTERN))
30
+ match[1]
31
+ elsif str.match?(EMAIL_PATTERN)
32
+ str
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generated module adds annotations for tracking generated resources
4
+ module Archsight::Annotations::Generated
5
+ def self.included(base)
6
+ base.class_eval do
7
+ annotation "generated/script",
8
+ description: "Name of the script that generated this resource",
9
+ title: "Generated By",
10
+ sidebar: false
11
+ annotation "generated/at",
12
+ description: "Timestamp when this resource was generated (ISO8601)",
13
+ title: "Generated At",
14
+ sidebar: false
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Git module adds git tracking annotations to resource classes
4
+ module Archsight::Annotations::Git
5
+ def self.included(base)
6
+ base.class_eval do
7
+ annotation "git/updatedAt",
8
+ description: "Date when the resource was last updated",
9
+ title: "Updated At"
10
+ annotation "git/updatedBy",
11
+ description: "Email of person who last updated the resource",
12
+ title: "Updated By"
13
+ annotation "git/reviewedAt",
14
+ description: "Date when the resource was last reviewed",
15
+ title: "Reviewed At"
16
+ annotation "git/reviewedBy",
17
+ description: "Email of person who last reviewed the resource",
18
+ title: "Reviewed By"
19
+ end
20
+ end
21
+ end