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