archsight 0.3.0 → 0.3.2
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 +4 -4
- data/CONTRIBUTING.md +1 -1
- data/README.md +7 -2
- data/docs/computed_annotations.md +7 -5
- data/docs/icons.md +3 -2
- data/docs/index.md.erb +5 -4
- data/docs/licenses.md +1 -2
- data/docs/modeling.md +127 -6
- data/docs/pages.md +72 -6
- data/docs/search.md +26 -2
- data/docs/togaf.md +4 -0
- data/lib/archsight/annotations/annotation.rb +5 -4
- data/lib/archsight/annotations/computed.rb +5 -1
- data/lib/archsight/annotations/relation_resolver.rb +109 -83
- data/lib/archsight/cli.rb +9 -2
- data/lib/archsight/database.rb +59 -4
- data/lib/archsight/diagram.rb +7 -0
- data/lib/archsight/documentation.rb +9 -5
- data/lib/archsight/editor.rb +2 -2
- data/lib/archsight/export/confluence/exporter.rb +11 -2
- data/lib/archsight/export/confluence/storage.rb +99 -8
- data/lib/archsight/export/confluence/tables.rb +78 -0
- data/lib/archsight/helpers/fenced_blocks.rb +61 -0
- data/lib/archsight/helpers/requirements_blocks.rb +90 -0
- data/lib/archsight/helpers/resource_resolver.rb +1 -1
- data/lib/archsight/helpers/view_blocks.rb +108 -0
- data/lib/archsight/helpers/wiki_links.rb +50 -5
- data/lib/archsight/helpers.rb +3 -0
- data/lib/archsight/import/handlers/go_grapher.rb +4 -1
- data/lib/archsight/import/handlers/go_module_parser.rb +4 -1
- data/lib/archsight/linter.rb +31 -3
- data/lib/archsight/mcp/analyze_resource_tool.rb +2 -2
- data/lib/archsight/mcp/base.rb +38 -0
- data/lib/archsight/mcp/resource_doc_tool.rb +2 -2
- data/lib/archsight/query/ast.rb +2 -1
- data/lib/archsight/query/evaluator.rb +2 -2
- data/lib/archsight/references.rb +129 -0
- data/lib/archsight/requirements.rb +70 -0
- data/lib/archsight/resources/analysis.rb +2 -1
- data/lib/archsight/resources/application_component.rb +8 -3
- data/lib/archsight/resources/application_interface.rb +5 -3
- data/lib/archsight/resources/application_service.rb +8 -7
- data/lib/archsight/resources/base.rb +67 -17
- data/lib/archsight/resources/business_actor.rb +5 -3
- data/lib/archsight/resources/business_control.rb +80 -0
- data/lib/archsight/resources/business_process.rb +3 -2
- data/lib/archsight/resources/business_product.rb +6 -5
- data/lib/archsight/resources/compliance_evidence.rb +40 -3
- data/lib/archsight/resources/data_object.rb +3 -2
- data/lib/archsight/resources/import.rb +5 -2
- data/lib/archsight/resources/{business_constraint.rb → motivation_constraint.rb} +4 -4
- data/lib/archsight/resources/motivation_goal.rb +1 -1
- data/lib/archsight/resources/{business_requirement.rb → motivation_requirement.rb} +19 -8
- data/lib/archsight/resources/motivation_stakeholder.rb +2 -2
- data/lib/archsight/resources/page.rb +4 -2
- data/lib/archsight/resources/strategy_capability.rb +2 -2
- data/lib/archsight/resources/technology_artifact.rb +4 -3
- data/lib/archsight/resources/technology_node.rb +2 -2
- data/lib/archsight/resources/technology_service.rb +4 -0
- data/lib/archsight/resources/technology_system_software.rb +4 -0
- data/lib/archsight/resources/view.rb +2 -1
- data/lib/archsight/resources.rb +34 -3
- data/lib/archsight/template.rb +2 -2
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/view_table.rb +102 -0
- data/lib/archsight/web/api/docs.rb +1 -1
- data/lib/archsight/web/api/json_helpers.rb +21 -15
- data/lib/archsight/web/api/openapi/spec.yaml +135 -2
- data/lib/archsight/web/api/page_helpers.rb +8 -7
- data/lib/archsight/web/api/requirements_helpers.rb +26 -0
- data/lib/archsight/web/api/routes.rb +19 -0
- data/lib/archsight/web/application.rb +12 -2
- data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +1 -0
- data/lib/archsight/web/public/vue/ApiDocsPage-DoOxKjG0.js +1 -0
- data/lib/archsight/web/public/vue/{DocPage-uaT8CdFm.js → DocPage-CfsC3CeQ.js} +1 -1
- data/lib/archsight/web/public/vue/EditorPage-CbG4mc9T.css +1 -0
- data/lib/archsight/web/public/vue/EditorPage-CsJA0q8n.js +35 -0
- data/lib/archsight/web/public/vue/ErrorPage-Ck9izEUS.css +1 -0
- data/lib/archsight/web/public/vue/ErrorPage-OJpJz9df.js +2 -0
- data/lib/archsight/web/public/vue/GraphView-BvWAbUAl.css +1 -0
- data/lib/archsight/web/public/vue/GraphView-CBq6oFRV.js +1 -0
- data/lib/archsight/web/public/vue/HomePage-BwgMLgVC.js +2 -0
- data/lib/archsight/web/public/vue/InstanceRouter-DQz-SsQq.js +1 -0
- data/lib/archsight/web/public/vue/InstanceRouter-jeavM6PW.css +1 -0
- data/lib/archsight/web/public/vue/KindList-4q0K9Cll.js +1 -0
- data/lib/archsight/web/public/vue/PageView-DzeGnvoS.js +1 -0
- data/lib/archsight/web/public/vue/QueryError-3EqjpbyV.css +1 -0
- data/lib/archsight/web/public/vue/QueryError-kg6Yf-pW.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-B0FLaFR3.css +1 -0
- data/lib/archsight/web/public/vue/ResourceList-Df_0e0qt.js +2 -0
- data/lib/archsight/web/public/vue/SearchResults-CKgY6_wH.js +1 -0
- data/lib/archsight/web/public/vue/SearchResults-CyPUZZSC.css +1 -0
- data/lib/archsight/web/public/vue/WikiPage-DoFJ0dbW.css +1 -0
- data/lib/archsight/web/public/vue/WikiPage-DrvCHwJ7.js +13 -0
- data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BfA1TPxQ.js +1 -0
- data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-BmgdUKVD.js +1 -0
- data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-DbXDvAWF.js +1 -0
- data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-BkVhrKS1.js +1 -0
- data/lib/archsight/web/public/vue/index-D0Q5GZRs.js +3 -0
- data/lib/archsight/web/public/vue/index-oF-iyZvk.css +1 -0
- data/lib/archsight/web/public/vue/info-A6RAGUB7-BIlCVuUY.js +1 -0
- data/lib/archsight/web/public/vue/mermaid-BjEi5URd.js +3334 -0
- data/lib/archsight/web/public/vue/packet-AYTQ26CC-eoQTjY3j.js +1 -0
- data/lib/archsight/web/public/vue/pie-WAS4IAKB-DEOotGhh.js +1 -0
- data/lib/archsight/web/public/vue/radar-RG4KPBEZ-C2yucuUN.js +1 -0
- data/lib/archsight/web/public/vue/railroad-74A4TZTK-CzONK5VY.js +1 -0
- data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-9zRPsDsx.js +1 -0
- data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-BJ4dkz9l.js +1 -0
- data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DFy_rqAj.js +1 -0
- data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-2cq5CyD2.js +1 -0
- data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DK-e9Ez4.js +1 -0
- data/lib/archsight/web/public/vue/{useGraphviz-C71SdG-N.js → useGraphviz-DweKV7Kg.js} +1 -1
- data/lib/archsight/web/public/vue/wardley-WFR3VGLG-BQwqUqfB.js +1 -0
- data/lib/archsight/web/public/vue.html +3 -3
- data/lib/archsight.rb +2 -0
- metadata +53 -42
- data/lib/archsight/web/public/vue/ApiDocsPage-C0y953v0.css +0 -1
- data/lib/archsight/web/public/vue/ApiDocsPage-C_4tAWis.js +0 -1
- data/lib/archsight/web/public/vue/EditorPage-C557BJC-.js +0 -35
- data/lib/archsight/web/public/vue/EditorPage-df5N-p21.css +0 -1
- data/lib/archsight/web/public/vue/ErrorPage-Vdebmife.js +0 -2
- data/lib/archsight/web/public/vue/ErrorPage-uMDnfY5_.css +0 -1
- data/lib/archsight/web/public/vue/GraphView-BduUql2N.js +0 -1
- data/lib/archsight/web/public/vue/GraphView-Cj2V2stN.css +0 -1
- data/lib/archsight/web/public/vue/HomePage-BHUTg8Ap.js +0 -2
- data/lib/archsight/web/public/vue/InstanceRouter-1lSigA58.js +0 -1
- data/lib/archsight/web/public/vue/InstanceRouter-Di7f3Rya.css +0 -1
- data/lib/archsight/web/public/vue/KindList-BDZs0j6d.js +0 -1
- data/lib/archsight/web/public/vue/PageView-BYzJDwof.js +0 -1
- data/lib/archsight/web/public/vue/ResourceList-CnbhU9wm.js +0 -1
- data/lib/archsight/web/public/vue/ResourceList-xyBwu7fh.css +0 -1
- data/lib/archsight/web/public/vue/SearchResults-CNhf6VOx.js +0 -1
- data/lib/archsight/web/public/vue/SearchResults-DOHzqAy3.css +0 -1
- data/lib/archsight/web/public/vue/WikiPage-DbmWkM7W.js +0 -13
- data/lib/archsight/web/public/vue/WikiPage-GV67QmNJ.css +0 -1
- data/lib/archsight/web/public/vue/architecture-TIHT7OUA-Bf-MWFmn.js +0 -1
- data/lib/archsight/web/public/vue/cynefin-VYW2F7L2-CkKt8qqs.js +0 -1
- data/lib/archsight/web/public/vue/eventmodeling-45OFAUF4-NwSTYPHj.js +0 -1
- data/lib/archsight/web/public/vue/gitGraph-TEB2WS4Q-DLRegrRG.js +0 -1
- data/lib/archsight/web/public/vue/index-CyVWObLU.js +0 -3
- data/lib/archsight/web/public/vue/index-DtKeHT3S.css +0 -1
- data/lib/archsight/web/public/vue/info-DKCQHKI2-EV39NzoN.js +0 -1
- data/lib/archsight/web/public/vue/mermaid-BMkGfnhm.js +0 -3279
- data/lib/archsight/web/public/vue/packet-7NZHBO7P-USljV3MZ.js +0 -1
- data/lib/archsight/web/public/vue/pie-RZYD4A2V-DChciBW2.js +0 -1
- data/lib/archsight/web/public/vue/radar-I7S5WNFK-CgGJdW1t.js +0 -1
- data/lib/archsight/web/public/vue/railroad-3IZDKUUU-Dy43vF5c.js +0 -1
- data/lib/archsight/web/public/vue/railroad-abnf-AHOZXSZD-BhsTH-sE.js +0 -1
- data/lib/archsight/web/public/vue/railroad-ebnf-EBAXGLYW-BBTd_u0u.js +0 -1
- data/lib/archsight/web/public/vue/railroad-peg-LSFZ7HO6-BiNTEzf0.js +0 -1
- data/lib/archsight/web/public/vue/treeView-QDETBFTQ-BfnCcf-q.js +0 -1
- data/lib/archsight/web/public/vue/treemap-6X3UGDF4-BsAORhvk.js +0 -1
- data/lib/archsight/web/public/vue/wardley-OPB4EBWU-DQvnEhRj.js +0 -1
|
@@ -11,12 +11,101 @@
|
|
|
11
11
|
# - Symbol: Simple kind filter (e.g., :TechnologyArtifact)
|
|
12
12
|
# - String: Query selector (e.g., 'TechnologyArtifact: activity/status == "active"')
|
|
13
13
|
class Archsight::Annotations::ComputedRelationResolver
|
|
14
|
+
# The instances that refer to a resource through relations that are written in files. Computed annotations sum up the
|
|
15
|
+
# modelled architecture (costs, teams, repositories): what a page or a description merely mentions or depicts (see
|
|
16
|
+
# Archsight::References) must not change them, so they follow modelled relations only.
|
|
17
|
+
def self.modelled_references(inst)
|
|
18
|
+
(inst.references || []).filter_map do |ref|
|
|
19
|
+
next ref unless ref.is_a?(Hash)
|
|
20
|
+
|
|
21
|
+
ref[:instance] unless Archsight::Resources::DERIVED_VERBS.include?(ref[:verb])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
14
25
|
MAX_DEPTH = 10
|
|
15
26
|
|
|
16
|
-
|
|
27
|
+
# TraversalCache holds what can be shared between all resolvers of one computation run: the unfiltered
|
|
28
|
+
# transitive neighbourhood of an instance (relations are fixed once the database is verified), parsed filter
|
|
29
|
+
# queries, one query evaluator and the short kind names. Filter results are never cached because they may
|
|
30
|
+
# depend on computed annotations that are set while the run progresses.
|
|
31
|
+
class TraversalCache
|
|
32
|
+
def initialize(database)
|
|
33
|
+
@database = database
|
|
34
|
+
@reach = {}
|
|
35
|
+
@queries = {}
|
|
36
|
+
@kind_names = {}.compare_by_identity
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Short kind name of a resource class ("ApplicationComponent")
|
|
40
|
+
def kind_name(klass)
|
|
41
|
+
@kind_names[klass] ||= klass.name.split("::").last
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Parsed query for a selector string
|
|
45
|
+
def query(selector)
|
|
46
|
+
@queries[selector] ||= begin
|
|
47
|
+
require_relative "../query/lexer"
|
|
48
|
+
require_relative "../query/parser"
|
|
49
|
+
Archsight::Query::Parser.new(Archsight::Query::Lexer.new(selector).tokenize).parse
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def evaluator
|
|
54
|
+
@evaluator ||= begin
|
|
55
|
+
require_relative "../query/evaluator"
|
|
56
|
+
Archsight::Query::Evaluator.new(@database)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# All instances reachable from `start` within max_depth hops (direction :outgoing or :incoming),
|
|
61
|
+
# each once, in breadth-first order. `start` itself is included when a cycle leads back to it.
|
|
62
|
+
def reachable(start, direction, max_depth)
|
|
63
|
+
by_instance = (@reach[[direction, max_depth]] ||= {}.compare_by_identity)
|
|
64
|
+
by_instance[start] ||= walk(start, direction, max_depth)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def walk(start, direction, max_depth)
|
|
70
|
+
results = []
|
|
71
|
+
listed = {}.compare_by_identity
|
|
72
|
+
expanded = { start => true }.compare_by_identity
|
|
73
|
+
frontier = [start]
|
|
74
|
+
depth = 0
|
|
75
|
+
while depth < max_depth && !frontier.empty?
|
|
76
|
+
following = []
|
|
77
|
+
frontier.each do |node|
|
|
78
|
+
neighbours(node, direction).each do |other|
|
|
79
|
+
unless listed.key?(other)
|
|
80
|
+
listed[other] = true
|
|
81
|
+
results << other
|
|
82
|
+
end
|
|
83
|
+
next if expanded.key?(other)
|
|
84
|
+
|
|
85
|
+
expanded[other] = true
|
|
86
|
+
following << other
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
frontier = following
|
|
90
|
+
depth += 1
|
|
91
|
+
end
|
|
92
|
+
results
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def neighbours(inst, direction)
|
|
96
|
+
if direction == :outgoing
|
|
97
|
+
inst.class.declared_relations.flat_map { |verb, kind_name, _klass_name| inst.relations(verb, kind_name) }
|
|
98
|
+
else
|
|
99
|
+
Archsight::Annotations::ComputedRelationResolver.modelled_references(inst)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @param cache [TraversalCache, nil] shared per computation run; a private one is created when omitted
|
|
105
|
+
def initialize(instance, database, cache = nil)
|
|
17
106
|
@instance = instance
|
|
18
107
|
@database = database
|
|
19
|
-
@
|
|
108
|
+
@cache = cache || TraversalCache.new(database)
|
|
20
109
|
end
|
|
21
110
|
|
|
22
111
|
# Get direct outgoing relations (-> Kind)
|
|
@@ -25,7 +114,7 @@ class Archsight::Annotations::ComputedRelationResolver
|
|
|
25
114
|
def outgoing(filter = nil)
|
|
26
115
|
results = []
|
|
27
116
|
|
|
28
|
-
@instance.class.
|
|
117
|
+
@instance.class.declared_relations.each do |_verb, kind_name, _klass_name|
|
|
29
118
|
rels = @instance.relations(_verb, kind_name)
|
|
30
119
|
rels.each do |rel|
|
|
31
120
|
results << rel if matches_filter?(rel, filter)
|
|
@@ -41,11 +130,7 @@ class Archsight::Annotations::ComputedRelationResolver
|
|
|
41
130
|
# @param max_depth [Integer] Maximum traversal depth (default 10)
|
|
42
131
|
# @return [Array] Array of transitively related instances
|
|
43
132
|
def outgoing_transitive(filter = nil, max_depth: MAX_DEPTH)
|
|
44
|
-
|
|
45
|
-
results = []
|
|
46
|
-
|
|
47
|
-
collect_transitive_outgoing(@instance, filter, visited, 0, max_depth, results)
|
|
48
|
-
results.uniq
|
|
133
|
+
filtered(@cache.reachable(@instance, :outgoing, max_depth), filter)
|
|
49
134
|
end
|
|
50
135
|
|
|
51
136
|
# Get direct incoming relations (<- Kind)
|
|
@@ -53,9 +138,7 @@ class Archsight::Annotations::ComputedRelationResolver
|
|
|
53
138
|
# @param filter [Symbol, String, nil] Optional kind filter (Symbol) or query selector (String)
|
|
54
139
|
# @return [Array] Array of instances that reference this one
|
|
55
140
|
def incoming(filter = nil)
|
|
56
|
-
|
|
57
|
-
# Extract instances from reference hashes
|
|
58
|
-
instances = refs.map { |ref| ref.is_a?(Hash) ? ref[:instance] : ref }.compact
|
|
141
|
+
instances = self.class.modelled_references(@instance).compact
|
|
59
142
|
|
|
60
143
|
if filter.nil?
|
|
61
144
|
instances
|
|
@@ -70,15 +153,23 @@ class Archsight::Annotations::ComputedRelationResolver
|
|
|
70
153
|
# @param max_depth [Integer] Maximum traversal depth (default 10)
|
|
71
154
|
# @return [Array] Array of instances that transitively reference this one
|
|
72
155
|
def incoming_transitive(filter = nil, max_depth: MAX_DEPTH)
|
|
73
|
-
|
|
74
|
-
results = []
|
|
75
|
-
|
|
76
|
-
collect_transitive_incoming(@instance, filter, visited, 0, max_depth, results)
|
|
77
|
-
results.uniq
|
|
156
|
+
filtered(@cache.reachable(@instance, :incoming, max_depth), filter)
|
|
78
157
|
end
|
|
79
158
|
|
|
80
159
|
private
|
|
81
160
|
|
|
161
|
+
def filtered(instances, filter)
|
|
162
|
+
return instances.dup if filter.nil?
|
|
163
|
+
|
|
164
|
+
if filter.is_a?(Symbol)
|
|
165
|
+
kind = filter.to_s
|
|
166
|
+
instances.select { |inst| @cache.kind_name(inst.class) == kind }
|
|
167
|
+
else
|
|
168
|
+
query_node = @cache.query(filter)
|
|
169
|
+
instances.select { |inst| @cache.evaluator.matches?(query_node, inst) }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
82
173
|
# Check if an instance matches the given filter
|
|
83
174
|
# @param instance [Object] The instance to check
|
|
84
175
|
# @param filter [Symbol, String, nil] Kind filter or query selector
|
|
@@ -86,75 +177,10 @@ class Archsight::Annotations::ComputedRelationResolver
|
|
|
86
177
|
def matches_filter?(instance, filter)
|
|
87
178
|
return true if filter.nil?
|
|
88
179
|
|
|
89
|
-
instance_kind = instance.class.name.split("::").last
|
|
90
|
-
|
|
91
180
|
if filter.is_a?(Symbol)
|
|
92
|
-
|
|
93
|
-
instance_kind == filter.to_s
|
|
181
|
+
@cache.kind_name(instance.class) == filter.to_s
|
|
94
182
|
else
|
|
95
|
-
|
|
96
|
-
query_node = parse_query(filter)
|
|
97
|
-
evaluator.matches?(query_node, instance)
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Parse a query string (with caching)
|
|
102
|
-
def parse_query(query_string)
|
|
103
|
-
@query_cache[query_string] ||= begin
|
|
104
|
-
require_relative "../query/lexer"
|
|
105
|
-
require_relative "../query/parser"
|
|
106
|
-
tokens = Archsight::Query::Lexer.new(query_string).tokenize
|
|
107
|
-
Archsight::Query::Parser.new(tokens).parse
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Get or create the query evaluator
|
|
112
|
-
def evaluator
|
|
113
|
-
@evaluator ||= begin
|
|
114
|
-
require_relative "../query/evaluator"
|
|
115
|
-
Archsight::Query::Evaluator.new(@database)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Recursively collect transitive outgoing relations
|
|
120
|
-
def collect_transitive_outgoing(inst, filter, visited, depth, max_depth, results)
|
|
121
|
-
return if depth >= max_depth
|
|
122
|
-
|
|
123
|
-
key = "#{inst.class}/#{inst.name}"
|
|
124
|
-
return if visited.include?(key)
|
|
125
|
-
|
|
126
|
-
visited.add(key)
|
|
127
|
-
|
|
128
|
-
inst.class.relations.each do |verb, kind_name, _klass_name|
|
|
129
|
-
rels = inst.relations(verb, kind_name)
|
|
130
|
-
rels.each do |rel|
|
|
131
|
-
# Add to results if matches filter (or no filter)
|
|
132
|
-
results << rel if matches_filter?(rel, filter)
|
|
133
|
-
|
|
134
|
-
# Continue traversal (regardless of whether this matched)
|
|
135
|
-
collect_transitive_outgoing(rel, filter, visited.dup, depth + 1, max_depth, results)
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# Recursively collect transitive incoming relations
|
|
141
|
-
def collect_transitive_incoming(inst, filter, visited, depth, max_depth, results)
|
|
142
|
-
return if depth >= max_depth
|
|
143
|
-
|
|
144
|
-
key = "#{inst.class}/#{inst.name}"
|
|
145
|
-
return if visited.include?(key)
|
|
146
|
-
|
|
147
|
-
visited.add(key)
|
|
148
|
-
|
|
149
|
-
refs = inst.references || []
|
|
150
|
-
# Extract instances from reference hashes
|
|
151
|
-
instances = refs.map { |ref| ref.is_a?(Hash) ? ref[:instance] : ref }.compact
|
|
152
|
-
instances.each do |ref|
|
|
153
|
-
# Add to results if matches filter (or no filter)
|
|
154
|
-
results << ref if matches_filter?(ref, filter)
|
|
155
|
-
|
|
156
|
-
# Continue traversal (regardless of whether this matched)
|
|
157
|
-
collect_transitive_incoming(ref, filter, visited.dup, depth + 1, max_depth, results)
|
|
183
|
+
@cache.evaluator.matches?(@cache.query(filter), instance)
|
|
158
184
|
end
|
|
159
185
|
end
|
|
160
186
|
end
|
data/lib/archsight/cli.rb
CHANGED
|
@@ -127,6 +127,12 @@ module Archsight
|
|
|
127
127
|
linter = Archsight::Linter.new(db)
|
|
128
128
|
errors = linter.validate
|
|
129
129
|
|
|
130
|
+
warnings = linter.warnings
|
|
131
|
+
if warnings.any?
|
|
132
|
+
puts "Deprecations (#{warnings.count}):"
|
|
133
|
+
warnings.each { |warning| puts " #{warning}" }
|
|
134
|
+
end
|
|
135
|
+
|
|
130
136
|
if errors.any?
|
|
131
137
|
puts "Validation Errors (#{errors.count}):"
|
|
132
138
|
errors.each { |error| display_error_with_context(error) }
|
|
@@ -282,7 +288,7 @@ module Archsight
|
|
|
282
288
|
desc "export [PAGE...]", "Export wiki pages to another system"
|
|
283
289
|
long_desc <<~DESC
|
|
284
290
|
Publishes wiki pages to the system named by --to. With no PAGE, every page that links to a target
|
|
285
|
-
page (for confluence: `confluence: <page URL>` in its frontmatter) is exported.
|
|
291
|
+
page (for confluence: `confluence: <page URL>` in its frontmatter) is exported. --tag TAG limits the export to pages carrying one of the given tags.
|
|
286
292
|
|
|
287
293
|
confluence: a page is only overwritten when Confluence still holds what the last export wrote. Pages
|
|
288
294
|
never exported before, or edited in Confluence since, are reported as blocked and not exported until
|
|
@@ -295,6 +301,7 @@ module Archsight
|
|
|
295
301
|
option :force, type: :boolean, default: false, desc: "Overwrite pages that were edited in the target since the last export"
|
|
296
302
|
option :lock, type: :boolean, default: true, desc: "Restrict editing of exported pages to the exporting user (--no-lock to skip)"
|
|
297
303
|
option :dry_run, type: :boolean, default: false, desc: "Show what would be exported without writing anything"
|
|
304
|
+
option :tag, type: :array, default: [], desc: "Only export pages with at least one of these tags (repeatable, case-insensitive)"
|
|
298
305
|
option :config, type: :string, desc: "Configuration file (default: ARCHSIGHT_CONFIG or ~/.config/archsight/archsight.yaml)"
|
|
299
306
|
option :drawio, type: :boolean, desc: "The target has the draw.io app: export diagrams as draw.io macros, else as images (default: `confluence.drawio` of the configuration)"
|
|
300
307
|
def export(*names)
|
|
@@ -317,7 +324,7 @@ module Archsight
|
|
|
317
324
|
end
|
|
318
325
|
exporter = Archsight::Export.exporter_for(options[:to]).new(
|
|
319
326
|
database: db, resources_dir: Archsight.resources_dir, force: options[:force], lock: options[:lock],
|
|
320
|
-
dry_run: options[:dry_run], settings: settings, drawio: options[:drawio]
|
|
327
|
+
dry_run: options[:dry_run], settings: settings, drawio: options[:drawio], tags: options[:tag]
|
|
321
328
|
)
|
|
322
329
|
results = exporter.run(names)
|
|
323
330
|
print_export_results(results)
|
data/lib/archsight/database.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "graph"
|
|
|
5
5
|
require_relative "resources"
|
|
6
6
|
require_relative "page_loader"
|
|
7
7
|
require_relative "query"
|
|
8
|
+
require_relative "references"
|
|
8
9
|
|
|
9
10
|
module Archsight
|
|
10
11
|
# LineReference combines a path and line reference
|
|
@@ -40,12 +41,19 @@ module Archsight
|
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
|
|
44
|
+
# Deprecation is a use of a renamed kind or relation key that was accepted under its old name
|
|
45
|
+
Deprecation = Struct.new(:ref, :message) do
|
|
46
|
+
def to_s
|
|
47
|
+
"#{ref}: #{message}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
43
51
|
# Database loads yaml files and folders to create an in-memory representation
|
|
44
52
|
# of the structure. The loading and parsing of files will raise errors
|
|
45
53
|
# if invalid data is passed.
|
|
46
54
|
class Database
|
|
47
55
|
attr_accessor :instances, :verbose, :verify, :compute_annotations, :only_kinds
|
|
48
|
-
attr_reader :path
|
|
56
|
+
attr_reader :path, :deprecations
|
|
49
57
|
|
|
50
58
|
def initialize(path, verbose: false, verify: true, compute_annotations: true, only_kinds: nil)
|
|
51
59
|
@path = path
|
|
@@ -54,10 +62,12 @@ module Archsight
|
|
|
54
62
|
@compute_annotations = compute_annotations
|
|
55
63
|
@only_kinds = only_kinds
|
|
56
64
|
@instances = {}
|
|
65
|
+
@deprecations = []
|
|
57
66
|
end
|
|
58
67
|
|
|
59
68
|
def reload!
|
|
60
69
|
@instances = {}
|
|
70
|
+
@deprecations = []
|
|
61
71
|
|
|
62
72
|
# load all resources
|
|
63
73
|
Dir.glob(File.join(@path, "**/*.{yaml,md}")).each do |path|
|
|
@@ -67,6 +77,7 @@ module Archsight
|
|
|
67
77
|
end
|
|
68
78
|
|
|
69
79
|
verify! if @verify
|
|
80
|
+
derive_references! if @verify
|
|
70
81
|
compute_all_annotations! if @verify && @compute_annotations
|
|
71
82
|
rescue Psych::SyntaxError => e
|
|
72
83
|
# Wrap YAML syntax errors in ResourceError for consistent handling
|
|
@@ -118,11 +129,49 @@ module Archsight
|
|
|
118
129
|
|
|
119
130
|
kind = obj["kind"] || raise("kind not defined")
|
|
120
131
|
klass = Archsight::Resources[kind] || raise("#{kind} is not a valid kind")
|
|
132
|
+
accept_old_names(obj)
|
|
121
133
|
inst = klass.new(obj, @current_ref)
|
|
122
|
-
|
|
134
|
+
raise("metadata name of #{kind} not present") if inst.name.to_s.strip.empty?
|
|
135
|
+
|
|
123
136
|
inst
|
|
124
137
|
end
|
|
125
138
|
|
|
139
|
+
# Documents may still use the old name of a renamed kind and the old relation keys. They are rewritten to the
|
|
140
|
+
# current names here, so everything after loading only sees those, and each rewrite is kept as a deprecation
|
|
141
|
+
# (reported by `archsight lint`).
|
|
142
|
+
def accept_old_names(obj)
|
|
143
|
+
kind = obj["kind"]
|
|
144
|
+
current = Archsight::Resources.canonical(kind)
|
|
145
|
+
if current != kind
|
|
146
|
+
deprecate("kind '#{kind}' is renamed to '#{current}'")
|
|
147
|
+
obj["kind"] = current
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
return unless obj["spec"].is_a?(Hash)
|
|
151
|
+
|
|
152
|
+
obj["spec"].each do |verb, keys|
|
|
153
|
+
next unless keys.is_a?(Hash)
|
|
154
|
+
|
|
155
|
+
Archsight::Resources::RELATION_KEY_ALIASES.each do |old_key, new_key|
|
|
156
|
+
next unless keys.key?(old_key)
|
|
157
|
+
|
|
158
|
+
deprecate("relation key '#{old_key}' under '#{verb}' is renamed to '#{new_key}'")
|
|
159
|
+
keys[new_key] = (Array(keys[new_key]) + Array(keys.delete(old_key))).uniq
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def deprecate(message)
|
|
165
|
+
@deprecations << Deprecation.new(@current_ref, "#{message} (the old name will be removed in a future release)")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Whether a document of this kind is wanted by the only_kinds filter, whichever of its names either side uses
|
|
169
|
+
def kind_wanted?(kind)
|
|
170
|
+
return true unless @only_kinds
|
|
171
|
+
|
|
172
|
+
@only_kinds.map { |k| Archsight::Resources.canonical(k) }.include?(Archsight::Resources.canonical(kind))
|
|
173
|
+
end
|
|
174
|
+
|
|
126
175
|
def load_file(path)
|
|
127
176
|
File.open(path, "r") do |f|
|
|
128
177
|
YAML.parse_stream(f) do |node|
|
|
@@ -131,7 +180,7 @@ module Archsight
|
|
|
131
180
|
next unless obj # skip empty / unknown documents
|
|
132
181
|
|
|
133
182
|
# Skip resources that don't match only_kinds filter
|
|
134
|
-
next
|
|
183
|
+
next unless kind_wanted?(obj["kind"])
|
|
135
184
|
|
|
136
185
|
self << create_valid_instance(obj)
|
|
137
186
|
end
|
|
@@ -185,7 +234,7 @@ module Archsight
|
|
|
185
234
|
end
|
|
186
235
|
|
|
187
236
|
def verify_instance_relations!(inst)
|
|
188
|
-
inst.class.
|
|
237
|
+
inst.class.declared_relations.each do |verb, kind, klass_name|
|
|
189
238
|
rels = inst.relations(verb, kind).map do |rel_name|
|
|
190
239
|
rel_klass = Archsight::Resources[klass_name] || raise_for(inst, "#{klass_name} is not a valid relation kind")
|
|
191
240
|
kind_display = rel_klass.to_s.sub(/^Archsight::Resources::/, "")
|
|
@@ -196,6 +245,12 @@ module Archsight
|
|
|
196
245
|
end
|
|
197
246
|
end
|
|
198
247
|
|
|
248
|
+
# Adds the relations that the text and the diagrams of the resources imply (`mentions`, `depicts`), after all
|
|
249
|
+
# written relations are resolved and before the computed annotations are calculated, which follow relations.
|
|
250
|
+
def derive_references!
|
|
251
|
+
Archsight::References.derive!(self)
|
|
252
|
+
end
|
|
253
|
+
|
|
199
254
|
# Compute all computed annotations for all instances
|
|
200
255
|
def compute_all_annotations!
|
|
201
256
|
manager = Archsight::Annotations::ComputedManager.new(self)
|
data/lib/archsight/diagram.rb
CHANGED
|
@@ -44,6 +44,13 @@ module Archsight
|
|
|
44
44
|
time(profile, :render) { Renderer.render(graph, layout, relation_filter: relation_filter, style: style, id_prefix: id_prefix) }
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# The `resource "..."` references of the nodes of a diagram source, in order and without duplicates. Parses and
|
|
48
|
+
# builds the graph but does not lay it out, so it is cheap enough to run over every diagram of a model.
|
|
49
|
+
# @raise [Archsight::Diagram::Error] if the source is not a valid diagram
|
|
50
|
+
def self.resource_references(source)
|
|
51
|
+
Graph.build(Parser.parse(source)).nodes_by_id.values.filter_map { |node| node.attrs["resource"] }.uniq
|
|
52
|
+
end
|
|
53
|
+
|
|
47
54
|
def self.time(profile, key)
|
|
48
55
|
return yield unless profile
|
|
49
56
|
|
|
@@ -104,7 +104,7 @@ module Archsight
|
|
|
104
104
|
# Skip kinds not in a displayed layer
|
|
105
105
|
next unless LAYER_ORDER.include?(klass.layer)
|
|
106
106
|
|
|
107
|
-
klass.
|
|
107
|
+
klass.declared_relations.each do |verb, _relation_kind, target_klass|
|
|
108
108
|
# Skip relations to excluded kinds
|
|
109
109
|
next if EXCLUDED_KINDS.include?(target_klass.to_s)
|
|
110
110
|
|
|
@@ -142,19 +142,23 @@ module Archsight
|
|
|
142
142
|
rows = ["| Annotation | Description | Values |", "|------------|-------------|--------|"]
|
|
143
143
|
annotations.each do |a|
|
|
144
144
|
values = format_values(a)
|
|
145
|
-
|
|
145
|
+
description = a.summary? ? "#{a.description || "-"} _(summary: shown with search hits)_" : (a.description || "-")
|
|
146
|
+
rows << "| `#{a.key}` | #{description} | #{values} |"
|
|
146
147
|
end
|
|
147
148
|
rows.join("\n")
|
|
148
149
|
end
|
|
149
150
|
|
|
150
151
|
def generate_relations_table(klass)
|
|
151
|
-
|
|
152
|
+
derived = "Every resource also has the derived relations #{Archsight::Resources::DERIVED_VERBS.map { |v| "`#{v}`" }.join(" and ")} " \
|
|
153
|
+
"towards any kind: they follow from links in its text and from its diagrams, so they are not written in files " \
|
|
154
|
+
"(see the Modeling Guide)."
|
|
155
|
+
return "_No relations defined._\n\n#{derived}" if klass.declared_relations.empty?
|
|
152
156
|
|
|
153
157
|
rows = ["| Relation | Target | Kind |", "|----------|--------|------|"]
|
|
154
|
-
klass.
|
|
158
|
+
klass.declared_relations.each do |verb, kind, target_klass|
|
|
155
159
|
rows << "| #{verb} | #{target_klass} | #{kind} |"
|
|
156
160
|
end
|
|
157
|
-
rows.join("\n")
|
|
161
|
+
"#{rows.join("\n")}\n\n#{derived}"
|
|
158
162
|
end
|
|
159
163
|
|
|
160
164
|
def format_values(annotation)
|
data/lib/archsight/editor.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Archsight
|
|
|
21
21
|
def build_resource(kind:, name:, annotations: {}, relations: [])
|
|
22
22
|
resource = {
|
|
23
23
|
"apiVersion" => "architecture/v1alpha1",
|
|
24
|
-
"kind" => kind,
|
|
24
|
+
"kind" => Archsight::Resources.canonical(kind), # saving a resource that had the old name migrates it
|
|
25
25
|
"metadata" => {
|
|
26
26
|
"name" => name
|
|
27
27
|
}
|
|
@@ -129,7 +129,7 @@ module Archsight
|
|
|
129
129
|
klass = Archsight::Resources[kind]
|
|
130
130
|
return [] unless klass
|
|
131
131
|
|
|
132
|
-
klass.relations
|
|
132
|
+
klass.declared_relations # derived relations are not written in files
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
# Get unique verbs for a resource kind's relations
|
|
@@ -31,8 +31,9 @@ module Archsight
|
|
|
31
31
|
# @param database [Archsight::Database]
|
|
32
32
|
# @param settings [Credentials::Settings, nil] token and draw.io support; default: Credentials.load
|
|
33
33
|
# @param drawio [Boolean, nil] overrides the settings' draw.io flag
|
|
34
|
+
# @param tags [Array<String>] only export pages that have at least one of these `page/tags` (case-insensitive)
|
|
34
35
|
# @param client_factory [#call, nil] `(base_url, token) -> Client`, for tests
|
|
35
|
-
def initialize(database:, resources_dir:, force: false, lock: true, dry_run: false, settings: nil, drawio: nil, client_factory: nil)
|
|
36
|
+
def initialize(database:, resources_dir:, force: false, lock: true, dry_run: false, settings: nil, drawio: nil, tags: [], client_factory: nil)
|
|
36
37
|
@database = database
|
|
37
38
|
@resources_dir = resources_dir
|
|
38
39
|
@force = force
|
|
@@ -40,6 +41,7 @@ module Archsight
|
|
|
40
41
|
@dry_run = dry_run
|
|
41
42
|
@settings = settings
|
|
42
43
|
@drawio = drawio
|
|
44
|
+
@tags = Array(tags).map { |t| t.to_s.strip.downcase }.reject(&:empty?)
|
|
43
45
|
@client_factory = client_factory || ->(base, secret) { Client.new(base: base, token: secret) }
|
|
44
46
|
@clients = {}
|
|
45
47
|
end
|
|
@@ -55,17 +57,24 @@ module Archsight
|
|
|
55
57
|
# @return [Array<Array(Page|String, Result|nil)>]
|
|
56
58
|
def select(names)
|
|
57
59
|
all = @database.instances_by_kind("Page")
|
|
58
|
-
return all.values.select { |p| link(p) }.sort_by(&:name).map { |p| [p, nil] } if names.empty?
|
|
60
|
+
return all.values.select { |p| link(p) && tagged?(p) }.sort_by(&:name).map { |p| [p, nil] } if names.empty?
|
|
59
61
|
|
|
60
62
|
names.map do |name|
|
|
61
63
|
page = all[name]
|
|
62
64
|
if page.nil? then [name, Result.new(page: name, status: :failed, message: "no such page")]
|
|
63
65
|
elsif link(page).nil? then [page, Result.new(page: name, status: :skipped, message: "no `confluence:` link in the frontmatter")]
|
|
66
|
+
elsif !tagged?(page) then [page, Result.new(page: name, status: :skipped, message: "has none of the tags #{@tags.join(", ")}")]
|
|
64
67
|
else [page, nil]
|
|
65
68
|
end
|
|
66
69
|
end
|
|
67
70
|
end
|
|
68
71
|
|
|
72
|
+
def tagged?(page)
|
|
73
|
+
return true if @tags.empty?
|
|
74
|
+
|
|
75
|
+
page.annotations["page/tags"].to_s.split(",").map { |t| t.strip.downcase }.intersect?(@tags)
|
|
76
|
+
end
|
|
77
|
+
|
|
69
78
|
def link(page)
|
|
70
79
|
value = page.annotations["page/confluence"].to_s.strip
|
|
71
80
|
value.empty? ? nil : value
|