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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/CONTRIBUTING.md +186 -0
- data/Dockerfile +39 -0
- data/LICENSE.txt +201 -0
- data/README.md +170 -0
- data/SECURITY.md +27 -0
- data/exe/archsight +9 -0
- data/lib/archsight/annotations/aggregators.rb +109 -0
- data/lib/archsight/annotations/annotation.rb +168 -0
- data/lib/archsight/annotations/architecture_annotations.rb +59 -0
- data/lib/archsight/annotations/backup_annotations.rb +21 -0
- data/lib/archsight/annotations/computed.rb +264 -0
- data/lib/archsight/annotations/email_recipient.rb +35 -0
- data/lib/archsight/annotations/generated_annotations.rb +17 -0
- data/lib/archsight/annotations/git_annotations.rb +21 -0
- data/lib/archsight/annotations/relation_resolver.rb +160 -0
- data/lib/archsight/cli.rb +120 -0
- data/lib/archsight/configuration.rb +36 -0
- data/lib/archsight/database.rb +183 -0
- data/lib/archsight/documentation.rb +171 -0
- data/lib/archsight/graph.rb +113 -0
- data/lib/archsight/helpers.rb +210 -0
- data/lib/archsight/linter.rb +77 -0
- data/lib/archsight/mcp/analyze_resource_tool.rb +222 -0
- data/lib/archsight/mcp/base.rb +48 -0
- data/lib/archsight/mcp/query_tool.rb +113 -0
- data/lib/archsight/mcp/resource_doc_tool.rb +87 -0
- data/lib/archsight/mcp.rb +6 -0
- data/lib/archsight/query/ast.rb +279 -0
- data/lib/archsight/query/errors.rb +39 -0
- data/lib/archsight/query/evaluator.rb +707 -0
- data/lib/archsight/query/lexer.rb +289 -0
- data/lib/archsight/query/parser.rb +506 -0
- data/lib/archsight/query.rb +68 -0
- data/lib/archsight/renderer.rb +134 -0
- data/lib/archsight/resources/application_component.rb +346 -0
- data/lib/archsight/resources/application_interface.rb +54 -0
- data/lib/archsight/resources/application_service.rb +222 -0
- data/lib/archsight/resources/base.rb +300 -0
- data/lib/archsight/resources/business_actor.rb +195 -0
- data/lib/archsight/resources/business_constraint.rb +32 -0
- data/lib/archsight/resources/business_process.rb +37 -0
- data/lib/archsight/resources/business_product.rb +206 -0
- data/lib/archsight/resources/business_requirement.rb +56 -0
- data/lib/archsight/resources/compliance_evidence.rb +42 -0
- data/lib/archsight/resources/data_object.rb +49 -0
- data/lib/archsight/resources/motivation_goal.rb +37 -0
- data/lib/archsight/resources/motivation_outcome.rb +33 -0
- data/lib/archsight/resources/motivation_stakeholder.rb +38 -0
- data/lib/archsight/resources/strategy_capability.rb +38 -0
- data/lib/archsight/resources/technology_artifact.rb +154 -0
- data/lib/archsight/resources/technology_interface.rb +34 -0
- data/lib/archsight/resources/technology_node.rb +42 -0
- data/lib/archsight/resources/technology_service.rb +35 -0
- data/lib/archsight/resources/technology_system_software.rb +37 -0
- data/lib/archsight/resources/view.rb +51 -0
- data/lib/archsight/resources.rb +49 -0
- data/lib/archsight/template.rb +49 -0
- data/lib/archsight/version.rb +5 -0
- data/lib/archsight/web/application.rb +290 -0
- data/lib/archsight/web/doc/archimate.md +215 -0
- data/lib/archsight/web/doc/computed_annotations.md +316 -0
- data/lib/archsight/web/doc/icons.md +303 -0
- data/lib/archsight/web/doc/index.md.erb +74 -0
- data/lib/archsight/web/doc/modeling.md +200 -0
- data/lib/archsight/web/doc/search.md +227 -0
- data/lib/archsight/web/doc/togaf.md +255 -0
- data/lib/archsight/web/doc/tool.md +90 -0
- data/lib/archsight/web/public/css/artifact.css +985 -0
- data/lib/archsight/web/public/css/base.css +201 -0
- data/lib/archsight/web/public/css/graph.css +106 -0
- data/lib/archsight/web/public/css/highlight.min.css +10 -0
- data/lib/archsight/web/public/css/iconoir.css +22 -0
- data/lib/archsight/web/public/css/instance.css +329 -0
- data/lib/archsight/web/public/css/layout.css +421 -0
- data/lib/archsight/web/public/css/mermaid-layers.css +188 -0
- data/lib/archsight/web/public/css/pico.min.css +4 -0
- data/lib/archsight/web/public/favicon.ico +0 -0
- data/lib/archsight/web/public/img/archimate.png +0 -0
- data/lib/archsight/web/public/img/togaf-high-level.png +0 -0
- data/lib/archsight/web/public/js/graph-zoom.js +18 -0
- data/lib/archsight/web/public/js/highlight.min.js +3899 -0
- data/lib/archsight/web/public/js/htmx.min.js +1 -0
- data/lib/archsight/web/public/js/mermaid-init.js +88 -0
- data/lib/archsight/web/public/js/mermaid.min.js +2811 -0
- data/lib/archsight/web/public/js/sparkline.js +42 -0
- data/lib/archsight/web/public/js/svg-pan-zoom.min.js +3 -0
- data/lib/archsight/web/public/js/svg-zoom-controls.js +93 -0
- data/lib/archsight/web/views/index.haml +12 -0
- data/lib/archsight/web/views/partials/artifact/_activity.haml +55 -0
- data/lib/archsight/web/views/partials/artifact/_agentic.haml +25 -0
- data/lib/archsight/web/views/partials/artifact/_deployment.haml +29 -0
- data/lib/archsight/web/views/partials/artifact/_git_info.haml +16 -0
- data/lib/archsight/web/views/partials/artifact/_language_stats.haml +53 -0
- data/lib/archsight/web/views/partials/artifact/_links.haml +24 -0
- data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +26 -0
- data/lib/archsight/web/views/partials/artifact/_repositories.haml +55 -0
- data/lib/archsight/web/views/partials/artifact/_team.haml +83 -0
- data/lib/archsight/web/views/partials/artifact/_workflow.haml +69 -0
- data/lib/archsight/web/views/partials/components/_activity.haml +37 -0
- data/lib/archsight/web/views/partials/components/_git.haml +17 -0
- data/lib/archsight/web/views/partials/components/_jira.haml +18 -0
- data/lib/archsight/web/views/partials/components/_languages.haml +29 -0
- data/lib/archsight/web/views/partials/components/_owner.haml +15 -0
- data/lib/archsight/web/views/partials/components/_repositories.haml +37 -0
- data/lib/archsight/web/views/partials/components/_status.haml +23 -0
- data/lib/archsight/web/views/partials/instance/_detail.haml +99 -0
- data/lib/archsight/web/views/partials/instance/_graph.haml +6 -0
- data/lib/archsight/web/views/partials/instance/_list.haml +84 -0
- data/lib/archsight/web/views/partials/instance/_relations.haml +43 -0
- data/lib/archsight/web/views/partials/instance/_requirements.haml +41 -0
- data/lib/archsight/web/views/partials/instance/_view_detail.haml +57 -0
- data/lib/archsight/web/views/partials/layout/_content.haml +40 -0
- data/lib/archsight/web/views/partials/layout/_error.haml +22 -0
- data/lib/archsight/web/views/partials/layout/_head.haml +24 -0
- data/lib/archsight/web/views/partials/layout/_navigation.haml +20 -0
- data/lib/archsight/web/views/partials/layout/_sidebar.haml +27 -0
- data/lib/archsight/web/views/search.haml +53 -0
- data/lib/archsight.rb +17 -0
- 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
|