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,707 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "ast"
|
|
5
|
+
|
|
6
|
+
class Archsight::Query::Evaluator
|
|
7
|
+
def initialize(database)
|
|
8
|
+
@database = database
|
|
9
|
+
@subquery_cache = {} # Cache for pre-computed subquery results
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Main entry point: evaluate query against instance, returns boolean
|
|
13
|
+
def matches?(query_node, instance)
|
|
14
|
+
# Check kind filter first
|
|
15
|
+
if query_node.kind_filter
|
|
16
|
+
instance_kind = instance.class.to_s.split("::").last
|
|
17
|
+
return false unless instance_kind == query_node.kind_filter
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# If no expression, kind filter alone is sufficient (match all of that kind)
|
|
21
|
+
return true if query_node.expression.nil?
|
|
22
|
+
|
|
23
|
+
# Pre-compute subqueries for this evaluation
|
|
24
|
+
@subquery_cache = {}
|
|
25
|
+
precompute_subqueries(query_node.expression)
|
|
26
|
+
|
|
27
|
+
evaluate(query_node.expression, instance)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Filter all instances matching query
|
|
31
|
+
def filter(query_node)
|
|
32
|
+
results = []
|
|
33
|
+
|
|
34
|
+
target_instances = if query_node.kind_filter
|
|
35
|
+
klass = Archsight::Resources[query_node.kind_filter]
|
|
36
|
+
return [] unless klass
|
|
37
|
+
|
|
38
|
+
@database.instances[klass]&.values || []
|
|
39
|
+
else
|
|
40
|
+
all = []
|
|
41
|
+
@database.instances.each_value { |h| all.concat(h.values) }
|
|
42
|
+
all
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# If no expression, return all target instances (kind filter only)
|
|
46
|
+
return target_instances if query_node.expression.nil?
|
|
47
|
+
|
|
48
|
+
# Pre-compute ALL subqueries before the main filter loop
|
|
49
|
+
# This is the key optimization - subqueries are evaluated once, not per-instance
|
|
50
|
+
@subquery_cache = {}
|
|
51
|
+
precompute_subqueries(query_node.expression)
|
|
52
|
+
|
|
53
|
+
target_instances.each do |instance|
|
|
54
|
+
results << instance if evaluate(query_node.expression, instance)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
results
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Recursively find and pre-compute all subqueries in the AST
|
|
63
|
+
def precompute_subqueries(node)
|
|
64
|
+
case node
|
|
65
|
+
when Archsight::Query::AST::BinaryOp
|
|
66
|
+
precompute_subqueries(node.left)
|
|
67
|
+
precompute_subqueries(node.right)
|
|
68
|
+
when Archsight::Query::AST::NotOp
|
|
69
|
+
precompute_subqueries(node.operand)
|
|
70
|
+
when Archsight::Query::AST::OutgoingDirectRelation, Archsight::Query::AST::OutgoingTransitiveRelation,
|
|
71
|
+
Archsight::Query::AST::IncomingDirectRelation, Archsight::Query::AST::IncomingTransitiveRelation
|
|
72
|
+
precompute_subquery_target(node.target)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def precompute_subquery_target(target)
|
|
77
|
+
return unless target.is_a?(Archsight::Query::AST::SubqueryTarget)
|
|
78
|
+
|
|
79
|
+
# Use the subquery's object_id as cache key
|
|
80
|
+
cache_key = target.object_id
|
|
81
|
+
return if @subquery_cache.key?(cache_key)
|
|
82
|
+
|
|
83
|
+
# First, recursively precompute any nested subqueries
|
|
84
|
+
precompute_subqueries(target.query.expression) if target.query.expression
|
|
85
|
+
|
|
86
|
+
# Then compute this subquery's results
|
|
87
|
+
@subquery_cache[cache_key] = Set.new(filter_without_cache(target.query))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Filter without using/populating the cache (used during precomputation)
|
|
91
|
+
def filter_without_cache(query_node)
|
|
92
|
+
results = []
|
|
93
|
+
|
|
94
|
+
target_instances = if query_node.kind_filter
|
|
95
|
+
klass = Archsight::Resources[query_node.kind_filter]
|
|
96
|
+
return [] unless klass
|
|
97
|
+
|
|
98
|
+
@database.instances[klass]&.values || []
|
|
99
|
+
else
|
|
100
|
+
all = []
|
|
101
|
+
@database.instances.each_value { |h| all.concat(h.values) }
|
|
102
|
+
all
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
return target_instances if query_node.expression.nil?
|
|
106
|
+
|
|
107
|
+
target_instances.each do |instance|
|
|
108
|
+
results << instance if evaluate(query_node.expression, instance)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
results
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get cached subquery results, or compute if not cached
|
|
115
|
+
def get_subquery_results(subquery_target)
|
|
116
|
+
cache_key = subquery_target.object_id
|
|
117
|
+
@subquery_cache[cache_key] ||= Set.new(filter_without_cache(subquery_target.query))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Check if a verb matches the verb filter
|
|
121
|
+
# Returns true if the verb should be included in traversal
|
|
122
|
+
def verb_matches?(verb, verbs, exclude_mode)
|
|
123
|
+
return true if verbs.nil? # nil = all verbs (no filter)
|
|
124
|
+
|
|
125
|
+
if exclude_mode
|
|
126
|
+
# Denylist: match if verb is NOT in the list
|
|
127
|
+
!verbs.include?(verb.to_s)
|
|
128
|
+
else
|
|
129
|
+
# Allowlist: match if verb IS in the list
|
|
130
|
+
verbs.include?(verb.to_s)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def evaluate(node, instance)
|
|
135
|
+
case node
|
|
136
|
+
when Archsight::Query::AST::BinaryOp
|
|
137
|
+
evaluate_binary_op(node, instance)
|
|
138
|
+
when Archsight::Query::AST::NotOp
|
|
139
|
+
evaluate_not_op(node, instance)
|
|
140
|
+
when Archsight::Query::AST::AnnotationCondition
|
|
141
|
+
evaluate_annotation_condition(node, instance)
|
|
142
|
+
when Archsight::Query::AST::AnnotationExistsCondition
|
|
143
|
+
evaluate_annotation_exists_condition(node, instance)
|
|
144
|
+
when Archsight::Query::AST::AnnotationInCondition
|
|
145
|
+
evaluate_annotation_in_condition(node, instance)
|
|
146
|
+
when Archsight::Query::AST::KindCondition
|
|
147
|
+
evaluate_kind_condition(node, instance)
|
|
148
|
+
when Archsight::Query::AST::KindInCondition
|
|
149
|
+
evaluate_kind_in_condition(node, instance)
|
|
150
|
+
when Archsight::Query::AST::NameCondition
|
|
151
|
+
evaluate_name_condition(node, instance)
|
|
152
|
+
when Archsight::Query::AST::NameInCondition
|
|
153
|
+
evaluate_name_in_condition(node, instance)
|
|
154
|
+
when Archsight::Query::AST::OutgoingDirectRelation
|
|
155
|
+
evaluate_outgoing_direct_relation(node, instance)
|
|
156
|
+
when Archsight::Query::AST::OutgoingTransitiveRelation
|
|
157
|
+
evaluate_outgoing_transitive_relation(node, instance)
|
|
158
|
+
when Archsight::Query::AST::IncomingDirectRelation
|
|
159
|
+
evaluate_incoming_direct_relation(node, instance)
|
|
160
|
+
when Archsight::Query::AST::IncomingTransitiveRelation
|
|
161
|
+
evaluate_incoming_transitive_relation(node, instance)
|
|
162
|
+
else
|
|
163
|
+
raise Archsight::Query::EvaluationError, "Unknown AST node type: #{node.class}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def evaluate_binary_op(node, instance)
|
|
168
|
+
case node.operator
|
|
169
|
+
when :and
|
|
170
|
+
# Short-circuit: if left is false, don't evaluate right
|
|
171
|
+
evaluate(node.left, instance) && evaluate(node.right, instance)
|
|
172
|
+
when :or
|
|
173
|
+
# Short-circuit: if left is true, don't evaluate right
|
|
174
|
+
evaluate(node.left, instance) || evaluate(node.right, instance)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def evaluate_not_op(node, instance)
|
|
179
|
+
!evaluate(node.operand, instance)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def evaluate_annotation_exists_condition(node, instance)
|
|
183
|
+
annotation_value = instance.annotations[node.path]
|
|
184
|
+
# Return true if annotation exists and has a non-empty value
|
|
185
|
+
!annotation_value.nil? && annotation_value.to_s.strip != ""
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def evaluate_annotation_in_condition(node, instance)
|
|
189
|
+
annotation_value = instance.annotations[node.path]
|
|
190
|
+
return false unless annotation_value
|
|
191
|
+
|
|
192
|
+
annotation = instance.class.annotation_matching(node.path)
|
|
193
|
+
is_list = annotation&.list?
|
|
194
|
+
|
|
195
|
+
query_values = node.values.map { |v| v.value.to_s }
|
|
196
|
+
|
|
197
|
+
if is_list
|
|
198
|
+
# For list annotations, check if any query value matches any annotation value
|
|
199
|
+
annotation_values = annotation_value.to_s.split(",").map(&:strip)
|
|
200
|
+
annotation_values.intersect?(query_values)
|
|
201
|
+
else
|
|
202
|
+
# For regular annotations, check if annotation value matches any query value
|
|
203
|
+
query_values.include?(annotation_value.to_s)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def evaluate_annotation_condition(node, instance)
|
|
208
|
+
annotation_value = instance.annotations[node.path]
|
|
209
|
+
annotation = instance.class.annotation_matching(node.path)
|
|
210
|
+
|
|
211
|
+
# Handle != for missing annotations (they match != condition)
|
|
212
|
+
return true if node.operator == "!=" && annotation_value.nil?
|
|
213
|
+
|
|
214
|
+
return false unless annotation_value
|
|
215
|
+
|
|
216
|
+
query_value = extract_query_value(node.value)
|
|
217
|
+
is_list = annotation&.list?
|
|
218
|
+
|
|
219
|
+
case node.operator
|
|
220
|
+
when "=="
|
|
221
|
+
if node.value.is_a?(Archsight::Query::AST::NumberValue)
|
|
222
|
+
# Numeric equality comparison
|
|
223
|
+
compare_numeric_equality(annotation_value, query_value)
|
|
224
|
+
elsif is_list
|
|
225
|
+
values = annotation_value.to_s.split(",").map(&:strip)
|
|
226
|
+
values.include?(query_value.to_s)
|
|
227
|
+
else
|
|
228
|
+
annotation_value.to_s == query_value.to_s
|
|
229
|
+
end
|
|
230
|
+
when "!="
|
|
231
|
+
if node.value.is_a?(Archsight::Query::AST::NumberValue)
|
|
232
|
+
# Numeric inequality comparison
|
|
233
|
+
!compare_numeric_equality(annotation_value, query_value)
|
|
234
|
+
elsif is_list
|
|
235
|
+
values = annotation_value.to_s.split(",").map(&:strip)
|
|
236
|
+
!values.include?(query_value.to_s)
|
|
237
|
+
else
|
|
238
|
+
annotation_value.to_s != query_value.to_s
|
|
239
|
+
end
|
|
240
|
+
when "=~"
|
|
241
|
+
regex = build_regex_from_value(node.value)
|
|
242
|
+
if is_list
|
|
243
|
+
values = annotation_value.to_s.split(",").map(&:strip)
|
|
244
|
+
values.any? { |v| regex.match?(v) }
|
|
245
|
+
else
|
|
246
|
+
regex.match?(annotation_value.to_s)
|
|
247
|
+
end
|
|
248
|
+
when ">", "<", ">=", "<="
|
|
249
|
+
compare_numeric_fallback(annotation_value, query_value, node.operator)
|
|
250
|
+
else
|
|
251
|
+
false
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def extract_query_value(value_node)
|
|
256
|
+
case value_node
|
|
257
|
+
when Archsight::Query::AST::NumberValue
|
|
258
|
+
value_node.value
|
|
259
|
+
when Archsight::Query::AST::RegexValue
|
|
260
|
+
value_node.value
|
|
261
|
+
else
|
|
262
|
+
value_node.value.to_s
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def build_regex_from_value(value_node)
|
|
267
|
+
if value_node.is_a?(Archsight::Query::AST::RegexValue)
|
|
268
|
+
value_node.to_regexp
|
|
269
|
+
else
|
|
270
|
+
Regexp.new(value_node.value.to_s, Regexp::IGNORECASE)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def compare_numeric_fallback(annotation_value, query_value, operator)
|
|
275
|
+
left = annotation_value.to_f
|
|
276
|
+
right = query_value.to_f
|
|
277
|
+
case operator
|
|
278
|
+
when ">" then left > right
|
|
279
|
+
when "<" then left < right
|
|
280
|
+
when ">=" then left >= right
|
|
281
|
+
when "<=" then left <= right
|
|
282
|
+
else false
|
|
283
|
+
end
|
|
284
|
+
rescue StandardError
|
|
285
|
+
false
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def compare_numeric_equality(annotation_value, query_value)
|
|
289
|
+
annotation_value.to_f == query_value.to_f
|
|
290
|
+
rescue StandardError
|
|
291
|
+
false
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def evaluate_kind_condition(node, instance)
|
|
295
|
+
instance_kind = instance.class.to_s.split("::").last
|
|
296
|
+
|
|
297
|
+
case node.operator
|
|
298
|
+
when "=="
|
|
299
|
+
instance_kind == node.value.value.to_s
|
|
300
|
+
when "=~"
|
|
301
|
+
regex = build_regex_from_value(node.value)
|
|
302
|
+
!!(instance_kind =~ regex)
|
|
303
|
+
else
|
|
304
|
+
false
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def evaluate_kind_in_condition(node, instance)
|
|
309
|
+
instance_kind = instance.class.to_s.split("::").last
|
|
310
|
+
query_values = node.values.map { |v| v.value.to_s }
|
|
311
|
+
query_values.include?(instance_kind)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def evaluate_name_condition(node, instance)
|
|
315
|
+
name = instance.name
|
|
316
|
+
return false unless name
|
|
317
|
+
|
|
318
|
+
case node.operator
|
|
319
|
+
when "=="
|
|
320
|
+
name == node.value.value.to_s
|
|
321
|
+
when "!="
|
|
322
|
+
name != node.value.value.to_s
|
|
323
|
+
when "=~"
|
|
324
|
+
re = if node.value.is_a?(Archsight::Query::AST::RegexValue)
|
|
325
|
+
node.value.to_regexp
|
|
326
|
+
else
|
|
327
|
+
Regexp.new(node.value.value.to_s, Regexp::IGNORECASE)
|
|
328
|
+
end
|
|
329
|
+
!!(name =~ re)
|
|
330
|
+
else
|
|
331
|
+
false
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def evaluate_name_in_condition(node, instance)
|
|
336
|
+
name = instance.name
|
|
337
|
+
return false unless name
|
|
338
|
+
|
|
339
|
+
query_values = node.values.map { |v| v.value.to_s }
|
|
340
|
+
query_values.include?(name)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Outgoing relations: what does this resource point to?
|
|
344
|
+
|
|
345
|
+
def evaluate_outgoing_direct_relation(node, instance)
|
|
346
|
+
verbs = node.verbs
|
|
347
|
+
exclude_verbs = node.exclude_verbs
|
|
348
|
+
|
|
349
|
+
case node.target
|
|
350
|
+
when Archsight::Query::AST::KindTarget
|
|
351
|
+
has_outgoing_relation_to_kind?(instance, node.target.kind_name, verbs, exclude_verbs)
|
|
352
|
+
when Archsight::Query::AST::InstanceTarget
|
|
353
|
+
has_outgoing_relation_to_instance?(instance, node.target.instance_name, verbs, exclude_verbs)
|
|
354
|
+
when Archsight::Query::AST::NothingTarget
|
|
355
|
+
!has_any_outgoing_relations?(instance, verbs, exclude_verbs)
|
|
356
|
+
when Archsight::Query::AST::SubqueryTarget
|
|
357
|
+
has_outgoing_relation_to_subquery?(instance, node.target, verbs, exclude_verbs)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def evaluate_outgoing_transitive_relation(node, instance)
|
|
362
|
+
visited = Set.new
|
|
363
|
+
max_depth = node.max_depth
|
|
364
|
+
verbs = node.verbs
|
|
365
|
+
exclude_verbs = node.exclude_verbs
|
|
366
|
+
|
|
367
|
+
case node.target
|
|
368
|
+
when Archsight::Query::AST::KindTarget
|
|
369
|
+
reaches_kind_transitively?(instance, node.target.kind_name, visited, 0, max_depth, verbs, exclude_verbs)
|
|
370
|
+
when Archsight::Query::AST::InstanceTarget
|
|
371
|
+
reaches_instance_transitively?(instance, node.target.instance_name, visited, 0, max_depth, verbs,
|
|
372
|
+
exclude_verbs)
|
|
373
|
+
when Archsight::Query::AST::NothingTarget
|
|
374
|
+
# ~> # is treated same as -> # (no outgoing relations)
|
|
375
|
+
!has_any_outgoing_relations?(instance, verbs, exclude_verbs)
|
|
376
|
+
when Archsight::Query::AST::SubqueryTarget
|
|
377
|
+
reaches_subquery_transitively?(instance, node.target, visited, 0, max_depth, nil, verbs, exclude_verbs)
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def has_outgoing_relation_to_kind?(instance, target_kind, verbs = nil, exclude_verbs = false)
|
|
382
|
+
instance.class.relations.each do |verb, kind_name, _|
|
|
383
|
+
next unless verb_matches?(verb, verbs, exclude_verbs)
|
|
384
|
+
|
|
385
|
+
rels = instance.relations(verb, kind_name)
|
|
386
|
+
rels.each do |rel|
|
|
387
|
+
rel_kind = rel.class.to_s.split("::").last
|
|
388
|
+
return true if rel_kind == target_kind
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
false
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def has_outgoing_relation_to_instance?(instance, target_name, verbs = nil, exclude_verbs = false)
|
|
395
|
+
instance.class.relations.each do |verb, kind_name, _|
|
|
396
|
+
next unless verb_matches?(verb, verbs, exclude_verbs)
|
|
397
|
+
|
|
398
|
+
rels = instance.relations(verb, kind_name)
|
|
399
|
+
return true if rels.any? { |rel| rel.name == target_name }
|
|
400
|
+
end
|
|
401
|
+
false
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def has_any_outgoing_relations?(instance, verbs = nil, exclude_verbs = false)
|
|
405
|
+
instance.class.relations.each do |verb, kind_name, _|
|
|
406
|
+
next unless verb_matches?(verb, verbs, exclude_verbs)
|
|
407
|
+
|
|
408
|
+
rels = instance.relations(verb, kind_name)
|
|
409
|
+
return true if rels.any?
|
|
410
|
+
end
|
|
411
|
+
false
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def reaches_kind_transitively?(instance, target_kind, visited, depth, max_depth, verbs = nil,
|
|
415
|
+
exclude_verbs = false)
|
|
416
|
+
return false if depth >= max_depth
|
|
417
|
+
|
|
418
|
+
key = "#{instance.class}/#{instance.name}"
|
|
419
|
+
return false if visited.include?(key)
|
|
420
|
+
|
|
421
|
+
visited.add(key)
|
|
422
|
+
|
|
423
|
+
instance.class.relations.each do |verb, kind_name, _|
|
|
424
|
+
next unless verb_matches?(verb, verbs, exclude_verbs)
|
|
425
|
+
|
|
426
|
+
rels = instance.relations(verb, kind_name)
|
|
427
|
+
rels.each do |rel|
|
|
428
|
+
rel_kind = rel.class.to_s.split("::").last
|
|
429
|
+
return true if rel_kind == target_kind
|
|
430
|
+
return true if reaches_kind_transitively?(rel, target_kind, visited.dup, depth + 1, max_depth, verbs,
|
|
431
|
+
exclude_verbs)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
false
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def reaches_instance_transitively?(instance, target_name, visited, depth, max_depth, verbs = nil,
|
|
439
|
+
exclude_verbs = false)
|
|
440
|
+
return false if depth >= max_depth
|
|
441
|
+
|
|
442
|
+
key = "#{instance.class}/#{instance.name}"
|
|
443
|
+
return false if visited.include?(key)
|
|
444
|
+
|
|
445
|
+
visited.add(key)
|
|
446
|
+
|
|
447
|
+
instance.class.relations.each do |verb, kind_name, _|
|
|
448
|
+
next unless verb_matches?(verb, verbs, exclude_verbs)
|
|
449
|
+
|
|
450
|
+
rels = instance.relations(verb, kind_name)
|
|
451
|
+
rels.each do |rel|
|
|
452
|
+
return true if rel.name == target_name
|
|
453
|
+
return true if reaches_instance_transitively?(rel, target_name, visited.dup, depth + 1, max_depth, verbs,
|
|
454
|
+
exclude_verbs)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
false
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Incoming relations: what points to this resource?
|
|
462
|
+
|
|
463
|
+
def evaluate_incoming_direct_relation(node, instance)
|
|
464
|
+
verbs = node.verbs
|
|
465
|
+
exclude_verbs = node.exclude_verbs
|
|
466
|
+
|
|
467
|
+
case node.target
|
|
468
|
+
when Archsight::Query::AST::KindTarget
|
|
469
|
+
has_incoming_relation_from_kind?(instance, node.target.kind_name, verbs, exclude_verbs)
|
|
470
|
+
when Archsight::Query::AST::InstanceTarget
|
|
471
|
+
has_incoming_relation_from_instance?(instance, node.target.instance_name, verbs, exclude_verbs)
|
|
472
|
+
when Archsight::Query::AST::NothingTarget
|
|
473
|
+
!has_any_incoming_relations?(instance, verbs, exclude_verbs)
|
|
474
|
+
when Archsight::Query::AST::SubqueryTarget
|
|
475
|
+
has_incoming_relation_from_subquery?(instance, node.target, verbs, exclude_verbs)
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def evaluate_incoming_transitive_relation(node, instance)
|
|
480
|
+
visited = Set.new
|
|
481
|
+
max_depth = node.max_depth
|
|
482
|
+
verbs = node.verbs
|
|
483
|
+
exclude_verbs = node.exclude_verbs
|
|
484
|
+
|
|
485
|
+
case node.target
|
|
486
|
+
when Archsight::Query::AST::KindTarget
|
|
487
|
+
reached_by_kind_transitively?(instance, node.target.kind_name, visited, 0, max_depth, verbs, exclude_verbs)
|
|
488
|
+
when Archsight::Query::AST::InstanceTarget
|
|
489
|
+
reached_by_instance_transitively?(instance, node.target.instance_name, visited, 0, max_depth, verbs,
|
|
490
|
+
exclude_verbs)
|
|
491
|
+
when Archsight::Query::AST::NothingTarget
|
|
492
|
+
# <~ # is treated same as <- # (no incoming relations)
|
|
493
|
+
!has_any_incoming_relations?(instance, verbs, exclude_verbs)
|
|
494
|
+
when Archsight::Query::AST::SubqueryTarget
|
|
495
|
+
reached_by_subquery_transitively?(instance, node.target, visited, 0, max_depth, nil, verbs, exclude_verbs)
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def has_incoming_relation_from_kind?(instance, source_kind, verbs = nil, exclude_verbs = false)
|
|
500
|
+
@database.instances.each_value do |instances_hash|
|
|
501
|
+
instances_hash.each_value do |other|
|
|
502
|
+
next if other == instance
|
|
503
|
+
|
|
504
|
+
other_kind = other.class.to_s.split("::").last
|
|
505
|
+
next unless other_kind == source_kind
|
|
506
|
+
|
|
507
|
+
other.class.relations.each do |verb, kind_name, _|
|
|
508
|
+
next unless verb_matches?(verb, verbs, exclude_verbs)
|
|
509
|
+
|
|
510
|
+
rels = other.relations(verb, kind_name)
|
|
511
|
+
return true if rels.include?(instance)
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
false
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def has_incoming_relation_from_instance?(instance, source_name, verbs = nil, exclude_verbs = false)
|
|
519
|
+
@database.instances.each_value do |instances_hash|
|
|
520
|
+
instances_hash.each_value do |other|
|
|
521
|
+
next if other == instance
|
|
522
|
+
next unless other.name == source_name
|
|
523
|
+
|
|
524
|
+
other.class.relations.each do |verb, kind_name, _|
|
|
525
|
+
next unless verb_matches?(verb, verbs, exclude_verbs)
|
|
526
|
+
|
|
527
|
+
rels = other.relations(verb, kind_name)
|
|
528
|
+
return true if rels.include?(instance)
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
false
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def has_any_incoming_relations?(instance, verbs = nil, exclude_verbs = false)
|
|
536
|
+
@database.instances.each_value do |instances_hash|
|
|
537
|
+
instances_hash.each_value do |other|
|
|
538
|
+
next if other == instance
|
|
539
|
+
|
|
540
|
+
other.class.relations.each do |verb, kind_name, _|
|
|
541
|
+
next unless verb_matches?(verb, verbs, exclude_verbs)
|
|
542
|
+
|
|
543
|
+
rels = other.relations(verb, kind_name)
|
|
544
|
+
return true if rels.include?(instance)
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
false
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def reached_by_kind_transitively?(instance, source_kind, visited, depth, max_depth, verbs = nil,
|
|
552
|
+
exclude_verbs = false)
|
|
553
|
+
return false if depth >= max_depth
|
|
554
|
+
|
|
555
|
+
key = "#{instance.class}/#{instance.name}"
|
|
556
|
+
return false if visited.include?(key)
|
|
557
|
+
|
|
558
|
+
visited.add(key)
|
|
559
|
+
|
|
560
|
+
@database.instances.each_value do |instances_hash|
|
|
561
|
+
instances_hash.each_value do |other|
|
|
562
|
+
next if other == instance
|
|
563
|
+
|
|
564
|
+
other.class.relations.each do |verb, kind_name, _|
|
|
565
|
+
next unless verb_matches?(verb, verbs, exclude_verbs)
|
|
566
|
+
|
|
567
|
+
rels = other.relations(verb, kind_name)
|
|
568
|
+
next unless rels.include?(instance)
|
|
569
|
+
|
|
570
|
+
other_kind = other.class.to_s.split("::").last
|
|
571
|
+
return true if other_kind == source_kind
|
|
572
|
+
return true if reached_by_kind_transitively?(other, source_kind, visited.dup, depth + 1, max_depth,
|
|
573
|
+
verbs, exclude_verbs)
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
false
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def reached_by_instance_transitively?(instance, source_name, visited, depth, max_depth, verbs = nil,
|
|
582
|
+
exclude_verbs = false)
|
|
583
|
+
return false if depth >= max_depth
|
|
584
|
+
|
|
585
|
+
key = "#{instance.class}/#{instance.name}"
|
|
586
|
+
return false if visited.include?(key)
|
|
587
|
+
|
|
588
|
+
visited.add(key)
|
|
589
|
+
|
|
590
|
+
@database.instances.each_value do |instances_hash|
|
|
591
|
+
instances_hash.each_value do |other|
|
|
592
|
+
next if other == instance
|
|
593
|
+
|
|
594
|
+
other.class.relations.each do |verb, kind_name, _|
|
|
595
|
+
next unless verb_matches?(verb, verbs, exclude_verbs)
|
|
596
|
+
|
|
597
|
+
rels = other.relations(verb, kind_name)
|
|
598
|
+
next unless rels.include?(instance)
|
|
599
|
+
|
|
600
|
+
return true if other.name == source_name
|
|
601
|
+
return true if reached_by_instance_transitively?(other, source_name, visited.dup, depth + 1, max_depth,
|
|
602
|
+
verbs, exclude_verbs)
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
false
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# Subquery relation methods
|
|
611
|
+
|
|
612
|
+
# Check if instance has any outgoing relation to any instance matching the subquery
|
|
613
|
+
def has_outgoing_relation_to_subquery?(instance, subquery_target, verbs = nil, exclude_verbs = false)
|
|
614
|
+
# Use cached subquery results for O(1) lookup
|
|
615
|
+
target_set = get_subquery_results(subquery_target)
|
|
616
|
+
return false if target_set.empty?
|
|
617
|
+
|
|
618
|
+
instance.class.relations.each do |verb, kind_name, _|
|
|
619
|
+
next unless verb_matches?(verb, verbs, exclude_verbs)
|
|
620
|
+
|
|
621
|
+
rels = instance.relations(verb, kind_name)
|
|
622
|
+
rels.each do |rel|
|
|
623
|
+
return true if target_set.include?(rel)
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
false
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# Check if instance transitively reaches any instance matching the subquery
|
|
630
|
+
def reaches_subquery_transitively?(instance, subquery_target, visited, depth, max_depth, _unused = nil,
|
|
631
|
+
verbs = nil, exclude_verbs = false)
|
|
632
|
+
return false if depth >= max_depth
|
|
633
|
+
|
|
634
|
+
key = "#{instance.class}/#{instance.name}"
|
|
635
|
+
return false if visited.include?(key)
|
|
636
|
+
|
|
637
|
+
visited.add(key)
|
|
638
|
+
|
|
639
|
+
# Use cached subquery results - computed once before filter loop
|
|
640
|
+
target_set = get_subquery_results(subquery_target)
|
|
641
|
+
return false if target_set.empty?
|
|
642
|
+
|
|
643
|
+
instance.class.relations.each do |verb, kind_name, _|
|
|
644
|
+
next unless verb_matches?(verb, verbs, exclude_verbs)
|
|
645
|
+
|
|
646
|
+
rels = instance.relations(verb, kind_name)
|
|
647
|
+
rels.each do |rel|
|
|
648
|
+
return true if target_set.include?(rel)
|
|
649
|
+
return true if reaches_subquery_transitively?(rel, subquery_target, visited.dup, depth + 1, max_depth, nil,
|
|
650
|
+
verbs, exclude_verbs)
|
|
651
|
+
end
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
false
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
# Check if any instance matching the subquery has a direct relation to this instance
|
|
658
|
+
def has_incoming_relation_from_subquery?(instance, subquery_target, verbs = nil, exclude_verbs = false)
|
|
659
|
+
# Use cached subquery results
|
|
660
|
+
source_set = get_subquery_results(subquery_target)
|
|
661
|
+
return false if source_set.empty?
|
|
662
|
+
|
|
663
|
+
source_set.each do |source|
|
|
664
|
+
source.class.relations.each do |verb, kind_name, _|
|
|
665
|
+
next unless verb_matches?(verb, verbs, exclude_verbs)
|
|
666
|
+
|
|
667
|
+
rels = source.relations(verb, kind_name)
|
|
668
|
+
return true if rels.include?(instance)
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
false
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# Check if any instance matching the subquery transitively reaches this instance
|
|
675
|
+
def reached_by_subquery_transitively?(instance, subquery_target, visited, depth, max_depth, _unused = nil,
|
|
676
|
+
verbs = nil, exclude_verbs = false)
|
|
677
|
+
return false if depth >= max_depth
|
|
678
|
+
|
|
679
|
+
key = "#{instance.class}/#{instance.name}"
|
|
680
|
+
return false if visited.include?(key)
|
|
681
|
+
|
|
682
|
+
visited.add(key)
|
|
683
|
+
|
|
684
|
+
# Use cached subquery results - computed once before filter loop
|
|
685
|
+
source_set = get_subquery_results(subquery_target)
|
|
686
|
+
return false if source_set.empty?
|
|
687
|
+
|
|
688
|
+
@database.instances.each_value do |instances_hash|
|
|
689
|
+
instances_hash.each_value do |other|
|
|
690
|
+
next if other == instance
|
|
691
|
+
|
|
692
|
+
other.class.relations.each do |verb, kind_name, _|
|
|
693
|
+
next unless verb_matches?(verb, verbs, exclude_verbs)
|
|
694
|
+
|
|
695
|
+
rels = other.relations(verb, kind_name)
|
|
696
|
+
next unless rels.include?(instance)
|
|
697
|
+
|
|
698
|
+
return true if source_set.include?(other)
|
|
699
|
+
return true if reached_by_subquery_transitively?(other, subquery_target, visited.dup, depth + 1,
|
|
700
|
+
max_depth, nil, verbs, exclude_verbs)
|
|
701
|
+
end
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
false
|
|
706
|
+
end
|
|
707
|
+
end
|