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,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