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
|
@@ -133,9 +133,12 @@ module Archsight::Import::Handlers::GoModuleParser
|
|
|
133
133
|
# Convert a Go module path to an ApplicationComponent name.
|
|
134
134
|
# Strips the SCM host segment and joins remaining path segments with ":".
|
|
135
135
|
# "github.com/example-org/billing-service/pkg" → "example-org:billing-service:pkg"
|
|
136
|
+
# A module path without a host ("module foo") has nothing left after stripping
|
|
137
|
+
# it and gets no component, rather than one with an empty name.
|
|
138
|
+
# @return [String, nil]
|
|
136
139
|
def component_name(mod_name)
|
|
137
140
|
parts = mod_name.split("/")
|
|
138
141
|
parts.shift
|
|
139
|
-
parts.join(":")
|
|
142
|
+
parts.join(":") unless parts.empty?
|
|
140
143
|
end
|
|
141
144
|
end
|
data/lib/archsight/linter.rb
CHANGED
|
@@ -26,6 +26,12 @@ module Archsight
|
|
|
26
26
|
@errors
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Things that still work but should be changed: renamed kinds and relation keys that the files still use.
|
|
30
|
+
# They are not errors, so they do not make `archsight lint` fail.
|
|
31
|
+
def warnings
|
|
32
|
+
@database.respond_to?(:deprecations) ? @database.deprecations.map(&:to_s) : []
|
|
33
|
+
end
|
|
34
|
+
|
|
29
35
|
private
|
|
30
36
|
|
|
31
37
|
# Pages need exactly one menu and their [[links]] must resolve
|
|
@@ -107,6 +113,8 @@ module Archsight
|
|
|
107
113
|
@errors << "#{instance.path_ref}: Markdown syntax error in annotation '#{key}': #{e.message}"
|
|
108
114
|
end
|
|
109
115
|
validate_diagram_blocks(instance, key, value)
|
|
116
|
+
validate_view_blocks(instance, key, value)
|
|
117
|
+
validate_requirements_blocks(instance, key, value)
|
|
110
118
|
validate_asset_images(instance, key, value)
|
|
111
119
|
validate_embeds(instance, key, value)
|
|
112
120
|
end
|
|
@@ -161,6 +169,25 @@ module Archsight
|
|
|
161
169
|
end
|
|
162
170
|
end
|
|
163
171
|
|
|
172
|
+
# Every ```view block must be a valid View, or the page shows an error box instead of the view
|
|
173
|
+
def validate_view_blocks(instance, key, value)
|
|
174
|
+
Helpers::ViewBlocks.sources(value).each_with_index do |source, index|
|
|
175
|
+
spec = Helpers::ViewBlocks.parse(source)
|
|
176
|
+
check_view_components(instance, spec[:fields])
|
|
177
|
+
rescue Helpers::ViewBlocks::Error => e
|
|
178
|
+
@errors << "#{instance.path_ref}: View error in annotation '#{key}' (view block #{index + 1}): #{e.message}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Every ```requirements block must be a valid filter, or the page shows an error box instead of the table
|
|
183
|
+
def validate_requirements_blocks(instance, key, value)
|
|
184
|
+
Helpers::RequirementsBlocks.sources(value).each_with_index do |source, index|
|
|
185
|
+
Helpers::RequirementsBlocks.parse(source)
|
|
186
|
+
rescue Helpers::RequirementsBlocks::Error => e
|
|
187
|
+
@errors << "#{instance.path_ref}: Requirements error in annotation '#{key}' (requirements block #{index + 1}): #{e.message}"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
164
191
|
# Renders with the same resolver the web UI uses, so a `resource` reference that would show as a broken link there is reported here
|
|
165
192
|
def render_diagram_source(instance, where, source)
|
|
166
193
|
unresolved = []
|
|
@@ -173,10 +200,11 @@ module Archsight
|
|
|
173
200
|
end
|
|
174
201
|
|
|
175
202
|
def validate_view_fields(instance)
|
|
176
|
-
|
|
177
|
-
|
|
203
|
+
check_view_components(instance, instance.annotations["view/fields"].to_s.split(",").map(&:strip))
|
|
204
|
+
end
|
|
178
205
|
|
|
179
|
-
|
|
206
|
+
def check_view_components(instance, fields)
|
|
207
|
+
fields.each do |field|
|
|
180
208
|
next unless field.start_with?("@")
|
|
181
209
|
|
|
182
210
|
component_name = field[1..]
|
|
@@ -24,13 +24,13 @@ class Archsight::MCP::AnalyzeResourceTool < FastMcp::Tool
|
|
|
24
24
|
Example: kind="ApplicationInterface", name="Kubernetes:RestAPI", impact=true
|
|
25
25
|
|
|
26
26
|
RESOURCE KINDS: TechnologyArtifact, ApplicationComponent, ApplicationInterface,
|
|
27
|
-
ApplicationService,
|
|
27
|
+
ApplicationService, MotivationRequirement, ComplianceEvidence, and more.
|
|
28
28
|
DESC
|
|
29
29
|
arguments do
|
|
30
30
|
required(:kind).filled(:string).description(
|
|
31
31
|
"Resource type to analyze. Common kinds: TechnologyArtifact (repos, code), " \
|
|
32
32
|
"ApplicationComponent (services), ApplicationInterface (APIs), " \
|
|
33
|
-
"
|
|
33
|
+
"MotivationRequirement (compliance and legal requirements), ComplianceEvidence (compliance proof)"
|
|
34
34
|
)
|
|
35
35
|
required(:name).filled(:string).description(
|
|
36
36
|
"Exact name of the resource (case-sensitive). Use QueryTool first if unsure of exact name."
|
data/lib/archsight/mcp/base.rb
CHANGED
|
@@ -38,6 +38,20 @@ module Archsight::MCP
|
|
|
38
38
|
result
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# The summary attributes of a resource's kind (annotations marked `summary: true`) that have a value,
|
|
42
|
+
# as [{key:, title:, value:, format:, type:}]. Computed annotations are included; they are precomputed
|
|
43
|
+
# at load and absent when empty. Lists (filter: :list) come back as arrays, Integer/Float typed values
|
|
44
|
+
# as numbers, `type` is the short type name ("Integer", "Time", "Person", ...) or nil.
|
|
45
|
+
def highlights(resource)
|
|
46
|
+
resource.class.summary_annotations.filter_map do |annotation|
|
|
47
|
+
value = highlight_value(annotation, resource)
|
|
48
|
+
next if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
49
|
+
|
|
50
|
+
{ key: annotation.key, title: annotation.title, value: value, format: annotation.format,
|
|
51
|
+
type: short_type_name(annotation.type) }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
41
55
|
def extract_description(resource)
|
|
42
56
|
description = resource.annotations["architecture/description"]
|
|
43
57
|
return "No description" if description.nil?
|
|
@@ -45,6 +59,30 @@ module Archsight::MCP
|
|
|
45
59
|
description.split("\n").first
|
|
46
60
|
end
|
|
47
61
|
|
|
62
|
+
def short_type_name(type)
|
|
63
|
+
type&.name.to_s.split("::").last
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The hit's kind name ("TechnologyArtifact") and the number of hits per kind
|
|
67
|
+
def kind_of(resource)
|
|
68
|
+
resource.class.to_s.split("::").last
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def count_by_kind(results)
|
|
72
|
+
results.group_by { |r| kind_of(r) }.transform_values(&:length)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def highlight_value(annotation, resource)
|
|
76
|
+
value = annotation.value_for(resource)
|
|
77
|
+
return value unless value.is_a?(String)
|
|
78
|
+
|
|
79
|
+
case annotation.type&.name
|
|
80
|
+
when "Integer" then value.match?(/\A-?\d+\z/) ? value.to_i : value
|
|
81
|
+
when "Float" then value.match?(/\A-?\d+(\.\d+)?\z/) ? value.to_f : value
|
|
82
|
+
else value
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
48
86
|
def extract_relations(instance)
|
|
49
87
|
relations = {}
|
|
50
88
|
|
|
@@ -23,7 +23,7 @@ class Archsight::MCP::ResourceDocTool < FastMcp::Tool
|
|
|
23
23
|
• ApplicationComponent - Services and application building blocks
|
|
24
24
|
• ApplicationInterface - APIs and interfaces exposed by components
|
|
25
25
|
• ApplicationService - Business services provided by applications
|
|
26
|
-
•
|
|
26
|
+
• MotivationRequirement - Statements of need, such as compliance and legal requirements
|
|
27
27
|
• ComplianceEvidence - Evidence linking artifacts to compliance requirements
|
|
28
28
|
DESC
|
|
29
29
|
arguments do
|
|
@@ -61,7 +61,7 @@ class Archsight::MCP::ResourceDocTool < FastMcp::Tool
|
|
|
61
61
|
kind: kind_symbol.to_s,
|
|
62
62
|
description: klass.description || "No description available",
|
|
63
63
|
annotation_count: klass.annotations.count,
|
|
64
|
-
relation_count: klass.
|
|
64
|
+
relation_count: klass.declared_relations.count
|
|
65
65
|
}
|
|
66
66
|
end
|
|
67
67
|
|
data/lib/archsight/query/ast.rb
CHANGED
|
@@ -218,7 +218,8 @@ module Archsight::Query::AST
|
|
|
218
218
|
attr_reader :kind_name
|
|
219
219
|
|
|
220
220
|
def initialize(kind_name)
|
|
221
|
-
|
|
221
|
+
# a renamed kind keeps matching under its old name
|
|
222
|
+
@kind_name = Archsight::Resources.canonical(kind_name)
|
|
222
223
|
end
|
|
223
224
|
end
|
|
224
225
|
|
|
@@ -292,7 +292,7 @@ class Archsight::Query::Evaluator
|
|
|
292
292
|
|
|
293
293
|
case node.operator
|
|
294
294
|
when "=="
|
|
295
|
-
instance_kind == node.value.value
|
|
295
|
+
instance_kind == Archsight::Resources.canonical(node.value.value)
|
|
296
296
|
when "=~"
|
|
297
297
|
regex = build_regex_from_value(node.value)
|
|
298
298
|
!!(instance_kind =~ regex)
|
|
@@ -303,7 +303,7 @@ class Archsight::Query::Evaluator
|
|
|
303
303
|
|
|
304
304
|
def evaluate_kind_in_condition(node, instance)
|
|
305
305
|
instance_kind = instance.class.to_s.split("::").last
|
|
306
|
-
query_values = node.values.map { |v| v.value
|
|
306
|
+
query_values = node.values.map { |v| Archsight::Resources.canonical(v.value) }
|
|
307
307
|
query_values.include?(instance_kind)
|
|
308
308
|
end
|
|
309
309
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archsight
|
|
4
|
+
# References derives relations from what the text and the diagrams of resources say, so that nobody has to repeat in
|
|
5
|
+
# YAML what is already written:
|
|
6
|
+
#
|
|
7
|
+
# - `mentions`: `[[Target]]`, `[[Kind/Name]]` and `![[View/Name]]` in a markdown text (a page's content, the
|
|
8
|
+
# description of any resource, any annotation a kind defines as markdown)
|
|
9
|
+
# - `depicts`: the `resource "..."` of the nodes of a diagram: an ```asd block in such a text, an `.asd` file embedded
|
|
10
|
+
# with ``, and the `architecture/diagram` annotation of any resource
|
|
11
|
+
#
|
|
12
|
+
# The relations are added to the resources like written ones (see Base#add_derived_relation), after the written ones
|
|
13
|
+
# are resolved, and are rebuilt on every load. Only what names a resource exactly counts: a target that is missing,
|
|
14
|
+
# ambiguous or only matches part of a name is skipped (the linter reports the broken ones), and so are links in code.
|
|
15
|
+
# Views and requirements blocks select resources with a query when they run, so they name nothing.
|
|
16
|
+
module References
|
|
17
|
+
# A fenced code block, and an inline code span: links in them are shown as written
|
|
18
|
+
FENCE = /^[ \t]*(?:```|~~~).*?^[ \t]*(?:```|~~~)[ \t]*$/m
|
|
19
|
+
INLINE_CODE = /`[^`\n]*`/
|
|
20
|
+
|
|
21
|
+
# Annotations that are markdown besides those a kind defines with `format: :markdown`
|
|
22
|
+
MARKDOWN_KEYS = %w[page/content architecture/description].freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# Adds the derived relations to every resource of the database.
|
|
27
|
+
def derive!(database)
|
|
28
|
+
finder = Finder.new(database)
|
|
29
|
+
database.instances.values.flat_map(&:values).each do |inst|
|
|
30
|
+
text_sources(inst).each { |markdown| mentions(markdown, finder).each { |target| link(inst, :mentions, target) } }
|
|
31
|
+
diagram_sources(inst, database).each { |source| depictions(source, finder).each { |target| link(inst, :depicts, target) } }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The resources a markdown text mentions with `[[...]]` links and `![[View/...]]` embeds, outside code.
|
|
36
|
+
# @return [Array<Base>]
|
|
37
|
+
def mentions(markdown, finder)
|
|
38
|
+
visible = markdown.to_s.gsub(FENCE, "").gsub(INLINE_CODE, "")
|
|
39
|
+
embeds = visible.scan(Helpers::Embeds::PATTERN).flatten.map(&:strip).filter_map { |reference| finder.embed(reference) }
|
|
40
|
+
targets = visible.gsub(Helpers::Embeds::PATTERN, "").scan(Helpers::WikiLinks::PATTERN).map { |match| match.first.strip }
|
|
41
|
+
(embeds + targets.filter_map { |target| finder.link(target) }).uniq
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The resources the nodes of a diagram source name with `resource "..."`; a source that is not a valid diagram
|
|
45
|
+
# names none.
|
|
46
|
+
# @return [Array<Base>]
|
|
47
|
+
def depictions(source, finder)
|
|
48
|
+
Diagram.resource_references(source).filter_map { |reference| finder.reference(reference) }
|
|
49
|
+
rescue Diagram::Error
|
|
50
|
+
[]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The markdown texts of a resource: a page's content, the description, and every annotation its kind defines as
|
|
54
|
+
# markdown.
|
|
55
|
+
def text_sources(inst)
|
|
56
|
+
inst.annotations.filter_map do |key, value|
|
|
57
|
+
value if value.is_a?(String) && value.include?("[[") && markdown_annotation?(inst, key)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The diagram sources of a resource: ```asd blocks and embedded `.asd` files of its markdown texts, and its
|
|
62
|
+
# annotations that are diagrams (`architecture/diagram`).
|
|
63
|
+
def diagram_sources(inst, database)
|
|
64
|
+
sources = [] #: Array[String]
|
|
65
|
+
inst.annotations.each do |key, value|
|
|
66
|
+
next unless value.is_a?(String)
|
|
67
|
+
|
|
68
|
+
if inst.class.annotation_format(key) == :asd
|
|
69
|
+
sources << value
|
|
70
|
+
elsif markdown_annotation?(inst, key)
|
|
71
|
+
sources.concat(Helpers::DiagramBlocks.sources(value))
|
|
72
|
+
sources.concat(embedded_diagrams(inst, value, database)) if value.include?(".asd")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
sources
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def markdown_annotation?(inst, key)
|
|
79
|
+
MARKDOWN_KEYS.include?(key) || inst.class.annotation_format(key) == :markdown
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The sources of the `.asd` files a markdown text embeds, found next to the file the resource comes from
|
|
83
|
+
def embedded_diagrams(inst, markdown, database)
|
|
84
|
+
resources_dir = database.path.to_s
|
|
85
|
+
base_dir = Assets.base_dir_for(inst, resources_dir: resources_dir)
|
|
86
|
+
return [] unless base_dir
|
|
87
|
+
|
|
88
|
+
Helpers::AssetImages.asd_files(markdown, base_dir: base_dir, resources_dir: resources_dir).filter_map do |_path, file|
|
|
89
|
+
File.read(file) if File.size(file) <= Helpers::AssetImages::MAX_ASD
|
|
90
|
+
end
|
|
91
|
+
rescue SystemCallError
|
|
92
|
+
[]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def link(inst, verb, target)
|
|
96
|
+
return if target.equal?(inst)
|
|
97
|
+
|
|
98
|
+
inst.add_derived_relation(verb, target)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Finds the resource a reference in a text names, exactly (see WikiLinks#exact_target_for)
|
|
102
|
+
class Finder
|
|
103
|
+
def initialize(database)
|
|
104
|
+
@database = database
|
|
105
|
+
@resolver = Helpers::ResourceResolver.new(database)
|
|
106
|
+
@links = Helpers::WikiLinks.new(database, resolver: @resolver)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# A `[[...]]` target: a page by name or title, `Kind/Name`, or a resource name
|
|
110
|
+
def link(target)
|
|
111
|
+
@links.exact_target_for(target)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# An `![[Kind/Name]]` embed: only views and analyses can be embedded
|
|
115
|
+
def embed(reference)
|
|
116
|
+
kind = reference.split("/", 2).first
|
|
117
|
+
return unless Helpers::Embeds::KINDS.include?(kind)
|
|
118
|
+
|
|
119
|
+
reference(reference)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A `resource "..."` of a diagram or an embed: `Name` across all kinds, or `Kind/Name`
|
|
123
|
+
def reference(reference)
|
|
124
|
+
found = @resolver.find(reference)
|
|
125
|
+
found.is_a?(Array) ? @database.instances_by_kind(found.first)[found.last] : nil
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archsight
|
|
4
|
+
# The requirements of a selection of resources, as the "Requirements" table of an instance page
|
|
5
|
+
# shows them, merged over all resources of the selection: one entry per MotivationRequirement, with the status the
|
|
6
|
+
# resources give it (`realizes` = implemented, `partiallyRealizes` = partial, `plans` = planned).
|
|
7
|
+
#
|
|
8
|
+
# Requirements.collect(db, of: 'ApplicationService: name =~ "Backup"', priority: ["must"])
|
|
9
|
+
# # => [{ name: "Requirement:X", status: "partial", priority: "must", story: "...",
|
|
10
|
+
# # by: [{ kind: "ApplicationService", name: "Backup", status: "partial" }] }]
|
|
11
|
+
module Requirements
|
|
12
|
+
# Relation verb -> status, best first
|
|
13
|
+
STATUSES = { "realizes" => "implemented", "partiallyRealizes" => "partial", "plans" => "planned" }.freeze
|
|
14
|
+
PRIORITIES = %w[must should may].freeze
|
|
15
|
+
RELATION = :motivationRequirements
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# @param of [String] query selecting the resources whose requirements are listed
|
|
20
|
+
# @param priority [Array<String>] only these priorities (empty: all)
|
|
21
|
+
# @param status [Array<String>] only requirements with one of these statuses (empty: all)
|
|
22
|
+
# @return [Array<Hash>] sorted by priority (must, should, may, none), then name
|
|
23
|
+
# @raise [Archsight::Query::QueryError] if `of` is not a valid query
|
|
24
|
+
def collect(database, of:, priority: [], status: [])
|
|
25
|
+
found = {}
|
|
26
|
+
Archsight::Query.parse(of).filter(database).each do |resource|
|
|
27
|
+
STATUSES.each do |verb, resource_status|
|
|
28
|
+
resource.relations(verb, RELATION).each do |requirement|
|
|
29
|
+
entry = (found[requirement.name] ||= { requirement: requirement, by: [] })
|
|
30
|
+
entry[:by] << { kind: resource.class.name.split("::").last, name: resource.name, status: resource_status }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
entries = found.values.map { |entry| entry(entry[:requirement], entry[:by]) }
|
|
36
|
+
entries = entries.select { |e| priority.include?(e[:priority]) } unless priority.empty?
|
|
37
|
+
entries = entries.select { |e| status.include?(e[:status]) } unless status.empty?
|
|
38
|
+
entries.sort_by { |e| [PRIORITIES.index(e[:priority]) || PRIORITIES.size, e[:name]] }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The requirements of a ```requirements block as a plain table (see ViewTable), for places that cannot run the frontend.
|
|
42
|
+
# "Realized by" is only there when more than one resource contributes.
|
|
43
|
+
# @param spec [Hash] the parsed block: `{ title:, of:, priority:, status: }` (Helpers::RequirementsBlocks.parse)
|
|
44
|
+
# @raise [Archsight::Query::QueryError]
|
|
45
|
+
def table(database, spec)
|
|
46
|
+
entries = collect(database, of: spec[:of], priority: spec[:priority], status: spec[:status])
|
|
47
|
+
with_by = entries.flat_map { |e| e[:by].map { |b| [b[:kind], b[:name]] } }.uniq.length > 1
|
|
48
|
+
cell = ViewTable::Cell
|
|
49
|
+
rows = entries.first(ViewTable::LIMIT).map do |e|
|
|
50
|
+
row = [cell.new(text: e[:status], as: :status), cell.new(text: e[:name]), cell.new(text: e[:priority].to_s, as: :priority),
|
|
51
|
+
cell.new(text: e[:story].to_s, as: :markdown)]
|
|
52
|
+
row << cell.new(text: e[:by].map { |b| b[:name] }.join("\n")) if with_by
|
|
53
|
+
row
|
|
54
|
+
end
|
|
55
|
+
ViewTable::Table.new(title: spec[:title].to_s.empty? ? "Requirements" : spec[:title],
|
|
56
|
+
columns: ["Status", "Name", "Priority", "Story", ("Realized by" if with_by)].compact, rows: rows, total: entries.length)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def entry(requirement, by)
|
|
60
|
+
by = by.sort_by { |b| [STATUSES.values.index(b[:status]), b[:kind], b[:name]] }
|
|
61
|
+
{
|
|
62
|
+
name: requirement.name,
|
|
63
|
+
status: by.first[:status],
|
|
64
|
+
priority: requirement.annotations["requirement/priority"],
|
|
65
|
+
story: requirement.annotations["requirement/story"],
|
|
66
|
+
by: by
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -45,7 +45,8 @@ class Archsight::Resources::Analysis < Archsight::Resources::Base
|
|
|
45
45
|
annotation "analysis/handler",
|
|
46
46
|
description: "Script handler type (only 'ruby' currently supported)",
|
|
47
47
|
title: "Handler",
|
|
48
|
-
enum: %w[ruby]
|
|
48
|
+
enum: %w[ruby],
|
|
49
|
+
summary: true
|
|
49
50
|
|
|
50
51
|
# Script content
|
|
51
52
|
annotation "analysis/script",
|
|
@@ -141,13 +141,15 @@ class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
|
|
|
141
141
|
annotation "deployment/cluster",
|
|
142
142
|
description: "Target cluster name",
|
|
143
143
|
title: "Cluster",
|
|
144
|
-
filter: :word
|
|
144
|
+
filter: :word,
|
|
145
|
+
summary: true
|
|
145
146
|
|
|
146
147
|
# Computed Annotations
|
|
147
148
|
computed_annotation "repository/artifacts/total",
|
|
148
149
|
title: "Total Git Repositories",
|
|
149
150
|
description: "Number of related git repositories",
|
|
150
|
-
type: Integer
|
|
151
|
+
type: Integer,
|
|
152
|
+
summary: true do
|
|
151
153
|
count(outgoing_transitive('TechnologyArtifact: artifact/type == "repo"'))
|
|
152
154
|
end
|
|
153
155
|
|
|
@@ -190,7 +192,7 @@ class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
|
|
|
190
192
|
|
|
191
193
|
computed_annotation "scc/language",
|
|
192
194
|
title: "Primary Language",
|
|
193
|
-
|
|
195
|
+
summary: true,
|
|
194
196
|
description: "Programming language with most lines of code across related artifacts",
|
|
195
197
|
filter: :word,
|
|
196
198
|
sidebar: true do
|
|
@@ -344,4 +346,7 @@ class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
|
|
|
344
346
|
relation :exposes, :applicationInterfaces, :ApplicationInterface
|
|
345
347
|
relation :dependsOn, :applicationInterfaces, :ApplicationInterface
|
|
346
348
|
relation :dependsOn, :applicationComponents, :ApplicationComponent
|
|
349
|
+
relation :realizes, :motivationRequirements, :MotivationRequirement
|
|
350
|
+
relation :plans, :motivationRequirements, :MotivationRequirement
|
|
351
|
+
relation :evidencedBy, :complianceEvidences, :ComplianceEvidence
|
|
347
352
|
end
|
|
@@ -34,17 +34,19 @@ class Archsight::Resources::ApplicationInterface < Archsight::Resources::Base
|
|
|
34
34
|
annotation "api/responsiveness",
|
|
35
35
|
description: "API 99th percentile responsiveness target",
|
|
36
36
|
title: "API Responsiveness (99p)",
|
|
37
|
-
enum: %w[10ms 100ms 1000ms 2s 5s 10s unresponsive]
|
|
37
|
+
enum: %w[10ms 100ms 1000ms 2s 5s 10s unresponsive],
|
|
38
|
+
summary: true
|
|
38
39
|
annotation "api/authenticationMethod",
|
|
39
40
|
description: "API authentication method",
|
|
40
41
|
title: "API Authentication Method",
|
|
41
|
-
enum: ["none", "hard coded", "token", "oidc"]
|
|
42
|
+
enum: ["none", "hard coded", "token", "oidc"],
|
|
43
|
+
summary: true
|
|
42
44
|
annotation "api/authorization",
|
|
43
45
|
description: "API authorization mechanism",
|
|
44
46
|
title: "API Authorization",
|
|
45
47
|
enum: ["none", "hard coded", "pbac", "abac", "rbac"]
|
|
46
48
|
|
|
47
49
|
relation :servedBy, :technologyComponents, :TechnologyInterface
|
|
48
|
-
relation :realizes, :
|
|
50
|
+
relation :realizes, :motivationConstraints, :MotivationConstraint
|
|
49
51
|
relation :serves, :dataObjects, :DataObject
|
|
50
52
|
end
|
|
@@ -34,13 +34,14 @@ class Archsight::Resources::ApplicationService < Archsight::Resources::Base
|
|
|
34
34
|
description: "Service plane classification (control manages resources, data handles traffic)",
|
|
35
35
|
title: "Service Plane",
|
|
36
36
|
enum: %w[control data],
|
|
37
|
-
|
|
37
|
+
summary: true
|
|
38
38
|
|
|
39
39
|
# Computed Annotations
|
|
40
40
|
computed_annotation "repository/artifacts/total",
|
|
41
41
|
title: "Total Git Repositories",
|
|
42
42
|
description: "Number of related git repositories",
|
|
43
|
-
type: Integer
|
|
43
|
+
type: Integer,
|
|
44
|
+
summary: true do
|
|
44
45
|
count(outgoing_transitive('TechnologyArtifact: artifact/type == "repo"'))
|
|
45
46
|
end
|
|
46
47
|
|
|
@@ -83,7 +84,7 @@ class Archsight::Resources::ApplicationService < Archsight::Resources::Base
|
|
|
83
84
|
|
|
84
85
|
computed_annotation "scc/languages",
|
|
85
86
|
title: "Primary Languages",
|
|
86
|
-
|
|
87
|
+
summary: true,
|
|
87
88
|
description: "Top 4 programming languages by lines of code across related artifacts",
|
|
88
89
|
filter: :list,
|
|
89
90
|
sidebar: true do
|
|
@@ -213,10 +214,10 @@ class Archsight::Resources::ApplicationService < Archsight::Resources::Base
|
|
|
213
214
|
relation :realizedThrough, :applicationComponents, :ApplicationComponent
|
|
214
215
|
relation :servedBy, :businessActors, :BusinessActor
|
|
215
216
|
relation :servedBy, :technologyServices, :TechnologyService
|
|
216
|
-
relation :realizes, :
|
|
217
|
-
relation :realizes, :
|
|
218
|
-
relation :partiallyRealizes, :
|
|
219
|
-
relation :plans, :
|
|
217
|
+
relation :realizes, :motivationConstraints, :MotivationConstraint
|
|
218
|
+
relation :realizes, :motivationRequirements, :MotivationRequirement
|
|
219
|
+
relation :partiallyRealizes, :motivationRequirements, :MotivationRequirement
|
|
220
|
+
relation :plans, :motivationRequirements, :MotivationRequirement
|
|
220
221
|
relation :realizes, :dataObjects, :DataObject
|
|
221
222
|
relation :evidencedBy, :complianceEvidences, :ComplianceEvidence
|
|
222
223
|
end
|
|
@@ -15,21 +15,46 @@ module Archsight
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def self.relation(verb, kind, klass_name)
|
|
18
|
-
@
|
|
19
|
-
@
|
|
18
|
+
@declared_relations ||= [] #: Array[[Symbol, Symbol, String]]
|
|
19
|
+
@declared_relations << [verb, kind, klass_name]
|
|
20
|
+
@relations = nil
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
# The relations a resource of this kind may be written with in a file (`spec`)
|
|
24
|
+
def self.declared_relations
|
|
25
|
+
@declared_relations || []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Every relation a resource of this kind can have: the declared ones and the derived ones (`mentions`,
|
|
29
|
+
# `depicts`, see Archsight::References). Everything that follows relations (queries, graphs, impact analysis)
|
|
30
|
+
# uses this; what lets a user write or choose relations uses declared_relations.
|
|
22
31
|
def self.relations
|
|
23
|
-
@relations
|
|
32
|
+
@relations ||= (declared_relations + Archsight::Resources::DERIVED_RELATIONS).freeze
|
|
24
33
|
end
|
|
25
34
|
|
|
35
|
+
# A kind may mark at most this many annotations as summary (see Base.annotation)
|
|
36
|
+
MAX_SUMMARY_ANNOTATIONS = 3
|
|
37
|
+
|
|
26
38
|
# Define an annotation using the Annotation class
|
|
39
|
+
# `summary: true` marks it as a summary attribute: it is returned with every search hit of this kind.
|
|
27
40
|
def self.annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil, sidebar: true,
|
|
28
|
-
type: nil,
|
|
29
|
-
@annotations ||= [] #: Array[Archsight::Annotations::Annotation]
|
|
41
|
+
type: nil, summary: false, editor: true, validator: nil)
|
|
30
42
|
options = { description: description, filter: filter, title: title, format: format, enum: enum,
|
|
31
|
-
sidebar: sidebar, type: type,
|
|
32
|
-
|
|
43
|
+
sidebar: sidebar, type: type, summary: summary, editor: editor, validator: validator }
|
|
44
|
+
register_annotation(Archsight::Annotations::Annotation.new(key, options))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Append an annotation definition, enforcing the limit of summary annotations per kind.
|
|
48
|
+
# Annotations of included modules (include_annotations) come through here as well.
|
|
49
|
+
def self.register_annotation(annotation)
|
|
50
|
+
@annotations ||= [] #: Array[Archsight::Annotations::Annotation]
|
|
51
|
+
if annotation.summary? && @annotations.count(&:summary?) >= MAX_SUMMARY_ANNOTATIONS
|
|
52
|
+
raise ArgumentError,
|
|
53
|
+
"#{name}: at most #{MAX_SUMMARY_ANNOTATIONS} annotations can be summary attributes, " \
|
|
54
|
+
"cannot add #{annotation.key}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@annotations << annotation
|
|
33
58
|
end
|
|
34
59
|
|
|
35
60
|
# Get all annotation definitions
|
|
@@ -48,19 +73,18 @@ module Archsight
|
|
|
48
73
|
# @param enum [Array, nil] Allowed values
|
|
49
74
|
# @param sidebar [Boolean] Show in sidebar (default false for computed)
|
|
50
75
|
# @param type [Class, nil] Type for value coercion (Integer, Float, String)
|
|
51
|
-
# @param
|
|
76
|
+
# @param summary [Boolean] Return the value with every search hit of this kind (default false, max 3 per kind)
|
|
52
77
|
# @yield Block that computes the annotation value, evaluated in Evaluator context
|
|
53
78
|
def self.computed_annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil,
|
|
54
|
-
sidebar: false, type: nil,
|
|
79
|
+
sidebar: false, type: nil, summary: false, editor: true, &)
|
|
55
80
|
require_relative "../annotations/computed"
|
|
56
81
|
@computed_annotations ||= [] #: Array[Archsight::Annotations::Computed]
|
|
57
82
|
@computed_annotations << Archsight::Annotations::Computed.new(key, description: description, type: type, &)
|
|
58
83
|
|
|
59
84
|
# Also register as a regular annotation so it passes validation and is recognized
|
|
60
|
-
@annotations ||= [] #: Array[Archsight::Annotations::Annotation]
|
|
61
85
|
options = { description: description, filter: filter, title: title, format: format, enum: enum,
|
|
62
|
-
sidebar: sidebar, type: type,
|
|
63
|
-
|
|
86
|
+
sidebar: sidebar, type: type, summary: summary, editor: editor }
|
|
87
|
+
register_annotation(Archsight::Annotations::Annotation.new(key, options))
|
|
64
88
|
end
|
|
65
89
|
|
|
66
90
|
# Get all computed annotation definitions
|
|
@@ -83,9 +107,9 @@ module Archsight
|
|
|
83
107
|
annotations.select(&:filterable?).reject(&:pattern?)
|
|
84
108
|
end
|
|
85
109
|
|
|
86
|
-
# Get annotations marked
|
|
87
|
-
def self.
|
|
88
|
-
annotations.select(&:
|
|
110
|
+
# Get the annotations marked as summary attributes (returned with search hits)
|
|
111
|
+
def self.summary_annotations
|
|
112
|
+
annotations.select(&:summary?).reject(&:pattern?)
|
|
89
113
|
end
|
|
90
114
|
|
|
91
115
|
def self.annotation_title(key)
|
|
@@ -96,6 +120,17 @@ module Archsight
|
|
|
96
120
|
annotation_matching(key)&.format
|
|
97
121
|
end
|
|
98
122
|
|
|
123
|
+
# The formats this kind defines for the given annotation keys, `{ "evidence/gaps" => "markdown" }` (keys without
|
|
124
|
+
# a format are left out). The frontend shows each value accordingly.
|
|
125
|
+
def self.annotation_formats(keys)
|
|
126
|
+
formats = {} #: Hash[String, String]
|
|
127
|
+
keys.each do |key|
|
|
128
|
+
format = annotation_format(key)
|
|
129
|
+
formats[key] = format.to_s if format
|
|
130
|
+
end
|
|
131
|
+
formats
|
|
132
|
+
end
|
|
133
|
+
|
|
99
134
|
def self.annotation_enum(key)
|
|
100
135
|
annotation_matching(key)&.enum
|
|
101
136
|
end
|
|
@@ -216,17 +251,32 @@ module Archsight
|
|
|
216
251
|
end
|
|
217
252
|
|
|
218
253
|
def verb_allowed?(verb)
|
|
219
|
-
self.class.
|
|
254
|
+
self.class.declared_relations.any? { |v, _, _| v.to_s == verb.to_s }
|
|
220
255
|
end
|
|
221
256
|
|
|
222
257
|
def verb_kind_allowed?(verb, kind)
|
|
223
|
-
self.class.
|
|
258
|
+
self.class.declared_relations.any? { |v, k, _| v.to_s == verb.to_s && k.to_s == kind.to_s }
|
|
224
259
|
end
|
|
225
260
|
|
|
226
261
|
def relations(verb, kind)
|
|
227
262
|
(spec[verb.to_s] || {})[kind.to_s] || []
|
|
228
263
|
end
|
|
229
264
|
|
|
265
|
+
# Records a relation that is derived from this resource's text or diagram, not written in its file: the target
|
|
266
|
+
# goes into `spec` where the relations of this verb are read (so queries and graphs see it like any other) and
|
|
267
|
+
# learns about this resource as one that refers to it.
|
|
268
|
+
def add_derived_relation(verb, target)
|
|
269
|
+
key = Archsight::Resources::DERIVED_KEY.to_s
|
|
270
|
+
spec_root = (@raw["spec"] ||= {}) #: Hash[String, untyped]
|
|
271
|
+
by_key = (spec_root[verb.to_s] ||= {}) #: Hash[String, Array[Base]]
|
|
272
|
+
by_key[key] ||= [] #: Array[Base]
|
|
273
|
+
targets = by_key.fetch(key)
|
|
274
|
+
return if targets.any? { |known| known.equal?(target) }
|
|
275
|
+
|
|
276
|
+
targets << target
|
|
277
|
+
target.referenced_by(self, verb.to_sym)
|
|
278
|
+
end
|
|
279
|
+
|
|
230
280
|
def set_relations(verb, kind, rels)
|
|
231
281
|
spec[verb.to_s][kind.to_s] = rels
|
|
232
282
|
rels.each { |rel| rel.referenced_by(self, verb) }
|