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,506 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "ast"
5
+
6
+ # Recursive descent parser for the architecture query language.
7
+ class Archsight::Query::Parser
8
+ def initialize(tokens)
9
+ @tokens = tokens
10
+ @position = 0
11
+ end
12
+
13
+ def parse
14
+ # Check for kind filter prefix: "Kind: expression"
15
+ kind_filter = nil
16
+ # Check it's a valid Kind (starts with capital letter)
17
+ if current_token.type == :IDENTIFIER && peek_token&.type == :COLON && (current_token.value =~ /^[A-Z]/)
18
+ kind_filter = current_token.value
19
+ advance # consume identifier
20
+ advance # consume colon
21
+ end
22
+
23
+ # Expression is optional when kind filter is present
24
+ # "TechnologyArtifact:" is valid and returns all resources of that kind
25
+ expression = if current_token.type == :EOF
26
+ nil
27
+ else
28
+ parse_or_expression
29
+ end
30
+
31
+ # Empty query (no kind filter and no expression) is an error
32
+ if kind_filter.nil? && expression.nil?
33
+ raise Archsight::Query::ParseError.new(
34
+ "Empty query: expected kind filter or expression",
35
+ position: 0,
36
+ source: nil
37
+ )
38
+ end
39
+
40
+ expect(:EOF)
41
+
42
+ Archsight::Query::AST::QueryNode.new(kind_filter, expression)
43
+ end
44
+
45
+ private
46
+
47
+ def parse_or_expression
48
+ left = parse_and_expression
49
+
50
+ while current_token.type == :OR
51
+ advance
52
+ right = parse_and_expression
53
+ left = Archsight::Query::AST::BinaryOp.new(:or, left, right)
54
+ end
55
+
56
+ left
57
+ end
58
+
59
+ def parse_and_expression
60
+ left = parse_unary_expression
61
+
62
+ while current_token.type == :AND
63
+ advance
64
+ right = parse_unary_expression
65
+ left = Archsight::Query::AST::BinaryOp.new(:and, left, right)
66
+ end
67
+
68
+ left
69
+ end
70
+
71
+ def parse_unary_expression
72
+ if current_token.type == :NOT
73
+ advance
74
+ operand = parse_primary
75
+ Archsight::Query::AST::NotOp.new(operand)
76
+ else
77
+ parse_primary
78
+ end
79
+ end
80
+
81
+ def parse_primary
82
+ case current_token.type
83
+ when :LPAREN
84
+ advance
85
+ expr = parse_or_expression
86
+ expect(:RPAREN)
87
+ expr
88
+ when :OUTGOING_DIRECT
89
+ parse_outgoing_direct_relation
90
+ when :OUTGOING_TRANSITIVE
91
+ parse_outgoing_transitive_relation
92
+ when :INCOMING_DIRECT
93
+ parse_incoming_direct_relation
94
+ when :INCOMING_TRANSITIVE
95
+ parse_incoming_transitive_relation
96
+ when :DASH
97
+ # -{...}> verb-filtered outgoing direct relation
98
+ parse_outgoing_direct_relation_with_verbs
99
+ when :TILDE
100
+ # ~{...}> verb-filtered outgoing transitive relation
101
+ parse_outgoing_transitive_relation_with_verbs
102
+ when :LT
103
+ # <{...}- or <{...}~ verb-filtered incoming relation
104
+ # Note: :LT is emitted when lexer sees <{ (verb filter start)
105
+ parse_incoming_relation_with_verbs
106
+ when :KIND
107
+ parse_kind_condition
108
+ when :NAME
109
+ parse_name_condition
110
+ when :IDENTIFIER
111
+ parse_identifier_or_shortcut
112
+ when :STRING
113
+ # Quoted annotation path: 'scc/language/C++/loc' >= 500
114
+ parse_quoted_annotation_path
115
+ else
116
+ raise Archsight::Query::ParseError.new(
117
+ "Unexpected token #{current_token.type}",
118
+ position: current_token.position,
119
+ source: nil
120
+ )
121
+ end
122
+ end
123
+
124
+ def parse_outgoing_direct_relation
125
+ advance # consume ->
126
+ target = parse_relation_target
127
+ Archsight::Query::AST::OutgoingDirectRelation.new(target)
128
+ end
129
+
130
+ def parse_outgoing_transitive_relation
131
+ advance # consume ~>
132
+ target = parse_relation_target
133
+ Archsight::Query::AST::OutgoingTransitiveRelation.new(target)
134
+ end
135
+
136
+ def parse_incoming_direct_relation
137
+ advance # consume <-
138
+ target = parse_relation_target
139
+ Archsight::Query::AST::IncomingDirectRelation.new(target)
140
+ end
141
+
142
+ def parse_incoming_transitive_relation
143
+ advance # consume <~
144
+ target = parse_relation_target
145
+ Archsight::Query::AST::IncomingTransitiveRelation.new(target)
146
+ end
147
+
148
+ # Parse verb filter: {verb1,verb2,...} or {!verb1,verb2,...}
149
+ # Returns [verbs_array, exclude_flag]
150
+ def parse_verb_filter
151
+ expect(:LBRACE)
152
+
153
+ exclude_verbs = false
154
+ verbs = []
155
+
156
+ # Check for ! prefix (exclude mode)
157
+ if current_token.type == :NOT
158
+ exclude_verbs = true
159
+ advance
160
+ end
161
+
162
+ # Parse first verb (required)
163
+ unless current_token.type == :IDENTIFIER
164
+ raise Archsight::Query::ParseError.new(
165
+ "Expected verb name in verb filter",
166
+ position: current_token.position,
167
+ source: nil
168
+ )
169
+ end
170
+ verbs << current_token.value
171
+ advance
172
+
173
+ # Parse additional verbs (comma-separated)
174
+ while current_token.type == :COMMA
175
+ advance # consume comma
176
+ unless current_token.type == :IDENTIFIER
177
+ raise Archsight::Query::ParseError.new(
178
+ "Expected verb name after comma in verb filter",
179
+ position: current_token.position,
180
+ source: nil
181
+ )
182
+ end
183
+ verbs << current_token.value
184
+ advance
185
+ end
186
+
187
+ expect(:RBRACE)
188
+ [verbs, exclude_verbs]
189
+ end
190
+
191
+ def parse_outgoing_direct_relation_with_verbs
192
+ advance # consume -
193
+ verbs, exclude_verbs = parse_verb_filter
194
+ expect(:GT) # consume >
195
+ target = parse_relation_target
196
+ Archsight::Query::AST::OutgoingDirectRelation.new(target, verbs, exclude_verbs)
197
+ end
198
+
199
+ def parse_outgoing_transitive_relation_with_verbs
200
+ advance # consume ~
201
+ verbs, exclude_verbs = parse_verb_filter
202
+ expect(:GT) # consume >
203
+ target = parse_relation_target
204
+ Archsight::Query::AST::OutgoingTransitiveRelation.new(target, verbs, exclude_verbs)
205
+ end
206
+
207
+ def parse_incoming_relation_with_verbs
208
+ advance # consume <
209
+ verbs, exclude_verbs = parse_verb_filter
210
+
211
+ # Determine if direct (<{...}-) or transitive (<{...}~) based on next token
212
+ if current_token.type == :DASH
213
+ advance # consume -
214
+ target = parse_relation_target
215
+ Archsight::Query::AST::IncomingDirectRelation.new(target, verbs, exclude_verbs)
216
+ elsif current_token.type == :TILDE
217
+ advance # consume ~
218
+ target = parse_relation_target
219
+ Archsight::Query::AST::IncomingTransitiveRelation.new(target, verbs, exclude_verbs)
220
+ else
221
+ raise Archsight::Query::ParseError.new(
222
+ "Expected - or ~ after verb filter in incoming relation",
223
+ position: current_token.position,
224
+ source: nil
225
+ )
226
+ end
227
+ end
228
+
229
+ def parse_relation_target
230
+ case current_token.type
231
+ when :STRING
232
+ name = current_token.value
233
+ advance
234
+ Archsight::Query::AST::InstanceTarget.new(name)
235
+ when :IDENTIFIER
236
+ kind = current_token.value
237
+ advance
238
+ Archsight::Query::AST::KindTarget.new(kind)
239
+ when :NONE
240
+ advance
241
+ Archsight::Query::AST::NothingTarget.new
242
+ when :DOLLAR
243
+ parse_subquery_target
244
+ else
245
+ raise Archsight::Query::ParseError.new(
246
+ "Expected kind, instance name, none, or $(subquery)",
247
+ position: current_token.position,
248
+ source: nil
249
+ )
250
+ end
251
+ end
252
+
253
+ def parse_subquery_target
254
+ advance # consume $
255
+ expect(:LPAREN)
256
+
257
+ # Parse inner query: optional kind filter + optional expression
258
+ kind_filter = nil
259
+ # Check it's a valid Kind (starts with capital letter)
260
+ if current_token.type == :IDENTIFIER && peek_token&.type == :COLON && (current_token.value =~ /^[A-Z]/)
261
+ kind_filter = current_token.value
262
+ advance # consume identifier
263
+ advance # consume colon
264
+ end
265
+
266
+ # Expression is optional when kind filter is present
267
+ expression = if current_token.type == :RPAREN
268
+ nil
269
+ else
270
+ parse_or_expression
271
+ end
272
+
273
+ # Subquery must have either kind filter or expression
274
+ if kind_filter.nil? && expression.nil?
275
+ raise Archsight::Query::ParseError.new(
276
+ "Empty subquery: expected kind filter or expression inside $()",
277
+ position: current_token.position,
278
+ source: nil
279
+ )
280
+ end
281
+
282
+ expect(:RPAREN)
283
+
284
+ inner_query = Archsight::Query::AST::QueryNode.new(kind_filter, expression)
285
+ Archsight::Query::AST::SubqueryTarget.new(inner_query)
286
+ end
287
+
288
+ def parse_kind_condition
289
+ advance # consume 'kind'
290
+
291
+ # Parse operator
292
+ op_token = current_token
293
+ unless %i[EQ MATCH IN].include?(op_token.type)
294
+ raise Archsight::Query::ParseError.new(
295
+ "Expected ==, =~, or 'in' after 'kind'",
296
+ position: op_token.position,
297
+ source: nil
298
+ )
299
+ end
300
+
301
+ if op_token.type == :IN
302
+ advance
303
+ return parse_kind_in_condition
304
+ end
305
+
306
+ operator = op_token.type == :EQ ? "==" : "=~"
307
+ advance
308
+
309
+ # Parse value
310
+ value = parse_value
311
+
312
+ Archsight::Query::AST::KindCondition.new(operator, value)
313
+ end
314
+
315
+ def parse_kind_in_condition
316
+ expect(:LPAREN)
317
+
318
+ values = []
319
+ values << parse_value
320
+
321
+ while current_token.type == :COMMA
322
+ advance # consume comma
323
+ values << parse_value
324
+ end
325
+
326
+ expect(:RPAREN)
327
+
328
+ Archsight::Query::AST::KindInCondition.new(values)
329
+ end
330
+
331
+ def parse_name_condition
332
+ advance # consume 'name'
333
+
334
+ # Parse operator
335
+ op_token = current_token
336
+ unless %i[EQ NEQ MATCH IN].include?(op_token.type)
337
+ raise Archsight::Query::ParseError.new(
338
+ "Expected ==, !=, =~, or 'in' after 'name'",
339
+ position: op_token.position,
340
+ source: nil
341
+ )
342
+ end
343
+
344
+ if op_token.type == :IN
345
+ advance
346
+ return parse_name_in_condition
347
+ end
348
+
349
+ advance
350
+
351
+ # Parse value
352
+ value = parse_value
353
+
354
+ operator = case op_token.type
355
+ when :EQ then "=="
356
+ when :NEQ then "!="
357
+ when :MATCH then "=~"
358
+ end
359
+
360
+ Archsight::Query::AST::NameCondition.new(operator, value)
361
+ end
362
+
363
+ def parse_name_in_condition
364
+ expect(:LPAREN)
365
+
366
+ values = []
367
+ values << parse_value
368
+
369
+ while current_token.type == :COMMA
370
+ advance # consume comma
371
+ values << parse_value
372
+ end
373
+
374
+ expect(:RPAREN)
375
+
376
+ Archsight::Query::AST::NameInCondition.new(values)
377
+ end
378
+
379
+ def parse_identifier_or_shortcut
380
+ path = current_token.value
381
+ advance
382
+
383
+ # Check if this is followed by an operator (annotation condition),
384
+ # a question mark (existence check), or a bare identifier (name shortcut)
385
+ if %i[EQ NEQ MATCH GT LT GTE LTE IN].include?(current_token.type)
386
+ # This is an annotation condition with comparison operator
387
+ parse_annotation_condition_with_path(path)
388
+ elsif current_token.type == :QUESTION
389
+ # Existence check: path?
390
+ advance # consume ?
391
+ Archsight::Query::AST::AnnotationExistsCondition.new(path)
392
+ else
393
+ # Bare identifier - treat as name =~ "identifier"
394
+ Archsight::Query::AST::NameCondition.new("=~", Archsight::Query::AST::StringValue.new(path))
395
+ end
396
+ end
397
+
398
+ def parse_quoted_annotation_path
399
+ # Quoted annotation path: 'scc/language/C++/loc' >= 500
400
+ path = current_token.value
401
+ advance
402
+
403
+ # Check if this is followed by an operator (annotation condition) or existence check
404
+ if %i[EQ NEQ MATCH GT LT GTE LTE IN].include?(current_token.type)
405
+ # This is an annotation condition with comparison operator
406
+ parse_annotation_condition_with_path(path)
407
+ elsif current_token.type == :QUESTION
408
+ # Existence check: 'path'?
409
+ advance # consume ?
410
+ Archsight::Query::AST::AnnotationExistsCondition.new(path)
411
+ else
412
+ raise Archsight::Query::ParseError.new(
413
+ "Expected operator or ? after quoted annotation path",
414
+ position: current_token.position,
415
+ source: nil
416
+ )
417
+ end
418
+ end
419
+
420
+ def parse_annotation_condition_with_path(path)
421
+ # Parse operator (path already consumed)
422
+ op_token = current_token
423
+ advance
424
+
425
+ # Handle IN operator specially
426
+ return parse_in_condition(path) if op_token.type == :IN
427
+
428
+ # Parse value
429
+ value = parse_value
430
+
431
+ operator = case op_token.type
432
+ when :EQ then "=="
433
+ when :NEQ then "!="
434
+ when :MATCH then "=~"
435
+ when :GT then ">"
436
+ when :LT then "<"
437
+ when :GTE then ">="
438
+ when :LTE then "<="
439
+ end
440
+
441
+ Archsight::Query::AST::AnnotationCondition.new(path, operator, value)
442
+ end
443
+
444
+ def parse_in_condition(path)
445
+ expect(:LPAREN)
446
+
447
+ values = []
448
+ values << parse_value
449
+
450
+ while current_token.type == :COMMA
451
+ advance # consume comma
452
+ values << parse_value
453
+ end
454
+
455
+ expect(:RPAREN)
456
+
457
+ Archsight::Query::AST::AnnotationInCondition.new(path, values)
458
+ end
459
+
460
+ def parse_value
461
+ case current_token.type
462
+ when :STRING
463
+ value = Archsight::Query::AST::StringValue.new(current_token.value)
464
+ advance
465
+ value
466
+ when :NUMBER
467
+ value = Archsight::Query::AST::NumberValue.new(current_token.value)
468
+ advance
469
+ value
470
+ when :REGEX
471
+ data = current_token.value
472
+ value = Archsight::Query::AST::RegexValue.new(data[:pattern], data[:flags])
473
+ advance
474
+ value
475
+ else
476
+ raise Archsight::Query::ParseError.new(
477
+ "Expected value (string, number, or regex)",
478
+ position: current_token.position,
479
+ source: nil
480
+ )
481
+ end
482
+ end
483
+
484
+ def current_token
485
+ @tokens[@position] || @tokens.last
486
+ end
487
+
488
+ def peek_token
489
+ @tokens[@position + 1]
490
+ end
491
+
492
+ def advance
493
+ @position += 1
494
+ end
495
+
496
+ def expect(type)
497
+ unless current_token.type == type
498
+ raise Archsight::Query::ParseError.new(
499
+ "Expected #{type} but got #{current_token.type}",
500
+ position: current_token.position,
501
+ source: nil
502
+ )
503
+ end
504
+ advance
505
+ end
506
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Define the Query namespace before loading query files
4
+ # (required for compact class definitions like Archsight::Query::Lexer)
5
+ module Archsight::Query; end
6
+
7
+ require_relative "query/errors"
8
+ require_relative "query/ast"
9
+ require_relative "query/lexer"
10
+ require_relative "query/parser"
11
+ require_relative "query/evaluator"
12
+
13
+ module Archsight
14
+ module Query
15
+ # Main Query class - entry point for parsing and evaluating queries
16
+ class Query
17
+ attr_reader :source, :ast
18
+
19
+ def initialize(source)
20
+ @source = source
21
+ @ast = parse(source)
22
+ end
23
+
24
+ # Check if a single instance matches this query
25
+ def matches?(instance, database:)
26
+ evaluator = Evaluator.new(database)
27
+ evaluator.matches?(@ast, instance)
28
+ end
29
+
30
+ # Filter all instances from database matching this query
31
+ def filter(database)
32
+ evaluator = Evaluator.new(database)
33
+ evaluator.filter(@ast)
34
+ end
35
+
36
+ # Return the kind filter if present (for optimization)
37
+ def kind_filter
38
+ @ast.kind_filter
39
+ end
40
+
41
+ # Pretty print for debugging
42
+ def to_s
43
+ "Query(#{@source})"
44
+ end
45
+
46
+ def inspect
47
+ "#<Query source=#{@source.inspect} kind_filter=#{kind_filter.inspect}>"
48
+ end
49
+
50
+ private
51
+
52
+ def parse(source)
53
+ lexer = Lexer.new(source)
54
+ tokens = lexer.tokenize
55
+ parser = Parser.new(tokens)
56
+ parser.parse
57
+ rescue LexerError, ParseError => e
58
+ # Re-raise with source context
59
+ raise QueryError.new(e.message, position: e.position, source: source)
60
+ end
61
+ end
62
+
63
+ # Convenience method for creating queries
64
+ def self.parse(source)
65
+ Query.new(source)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Archsight
4
+ # GraphvisRenderer renders instances of the database
5
+ module GraphvisRenderer
6
+ FONT = "Helvetica"
7
+
8
+ def gname(inst)
9
+ "#{inst.class}/#{inst.name}"
10
+ end
11
+
12
+ def css_class(inst)
13
+ base_class = inst.class.name.to_s.gsub("::", "")
14
+ layer_class = "layer-#{inst.class.layer}"
15
+ "#{base_class} #{layer_class}"
16
+ end
17
+
18
+ def has_relations?(inst, klass)
19
+ # Check outgoing relations defined on this class
20
+ klass.relations.each do |verb, kind|
21
+ return true if inst.relations(verb, kind).any?
22
+ end
23
+ false
24
+ end
25
+
26
+ # Check if any resource in the database references this instance
27
+ def has_incoming_relations?(db, inst)
28
+ db.instances.each do |other_klass, instances|
29
+ other_klass.relations.each do |_verb, target_kind|
30
+ next unless target_kind == inst.class
31
+
32
+ instances.each_value do |other_inst|
33
+ return true if other_inst.relations(_verb, target_kind).include?(inst)
34
+ end
35
+ end
36
+ end
37
+ false
38
+ end
39
+
40
+ def gnode(graph, inst, opts = {})
41
+ label = "<TABLE><TR><TD><B>#{inst.name}</B></TD></TR><TR><TD>#{inst.klass}</TD></TR></TABLE>"
42
+ label = inst.name if opts[:simple_label]
43
+ graph.node gname(inst), class: css_class(inst), shape: :box,
44
+ style: "rounded,filled", fontname: FONT, label: label,
45
+ href: "/kinds/#{inst.klass}/instances/#{inst.name}"
46
+ end
47
+
48
+ def gedge(graph, a_inst, b_inst, label)
49
+ graph.edge gname(a_inst), gname(b_inst), label: label, fontname: "#{FONT} italic"
50
+ end
51
+
52
+ def create_graph_all(db, method = :draw_dot, root_kinds: nil, max_depth: 3, allowed_kinds: nil)
53
+ root_kinds ||= [Archsight::Resources["BusinessProduct"], Archsight::Resources["BusinessProcess"]]
54
+
55
+ # Default allowed kinds for overview: Products, Processes, Services, and Teams
56
+ allowed_kinds ||= [
57
+ Archsight::Resources["BusinessProduct"],
58
+ Archsight::Resources["BusinessProcess"],
59
+ Archsight::Resources["ApplicationService"],
60
+ Archsight::Resources["BusinessActor"]
61
+ ]
62
+ allowed_kinds_set = allowed_kinds.to_set
63
+
64
+ Archsight::Graphvis.new("all").send(method) do |g|
65
+ nodes = {} # Track visited nodes
66
+ edges = {} # Track created edges
67
+
68
+ # Collect root instances
69
+ queue = [] # [instance, depth] pairs
70
+ root_kinds.each do |klass|
71
+ db.instances[klass]&.each_value do |inst|
72
+ next if inst.abandoned?
73
+
74
+ queue << [inst, 0]
75
+ end
76
+ end
77
+
78
+ # BFS traversal with depth limit
79
+ while queue.any?
80
+ inst, depth = queue.shift
81
+ next if nodes[inst]
82
+
83
+ nodes[inst] = true
84
+ gnode(g, inst, simple_label: true)
85
+
86
+ # Stop following relations at max depth
87
+ next if depth >= max_depth
88
+
89
+ inst.class.relations.each do |verb, kind|
90
+ inst.relations(verb, kind).each do |rel|
91
+ next if rel.abandoned?
92
+ next unless allowed_kinds_set.include?(rel.class)
93
+
94
+ edge_key = "#{gname(inst)}|#{verb}|#{gname(rel)}"
95
+ unless edges[edge_key]
96
+ gedge(g, inst, rel, verb)
97
+ edges[edge_key] = true
98
+ end
99
+
100
+ queue << [rel, depth + 1] unless nodes[rel]
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ def create_graph_one(db, klass_pat, name_pat, method = :draw_dot)
108
+ name = "#{klass_pat}:#{name_pat}"
109
+ nodes = {}
110
+ edges = {}
111
+ Archsight::Graphvis.new(name).send(method) do |g|
112
+ klass = Archsight::Resources[klass_pat] || raise("kind #{klass_pat} unknown")
113
+ instances = db.instances[klass]
114
+ inst = instances[name_pat] || raise("name #{name_pat} for kind #{klass_pat} not found")
115
+ create_graph_one_inst(db, g, inst, nodes, edges)
116
+ end
117
+ end
118
+
119
+ def create_graph_one_inst(db, graph, inst, nodes, edges)
120
+ return if nodes[inst] # Already visited - prevent infinite recursion
121
+
122
+ gnode(graph, inst)
123
+ nodes[inst] = true
124
+ inst.class.relations.each do |verb, kind|
125
+ inst.relations(verb, kind).each do |rel|
126
+ edge_name = "#{inst}|#{verb}|#{rel}"
127
+ gedge(graph, inst, rel, verb) unless edges[edge_name]
128
+ edges[edge_name] = true
129
+ create_graph_one_inst(db, graph, rel, nodes, edges)
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end