archsight 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -1
  3. data/docs/archimate.md +21 -1
  4. data/docs/configuration.md +1 -0
  5. data/docs/diagram.md +15 -3
  6. data/docs/docker.md +4 -1
  7. data/docs/icons.md +32 -0
  8. data/docs/import.md +178 -55
  9. data/docs/index.md.erb +31 -8
  10. data/docs/modeling.md +772 -24
  11. data/docs/pages.md +44 -13
  12. data/docs/search.md +21 -1
  13. data/docs/togaf.md +5 -1
  14. data/lib/archsight/analysis/result.rb +8 -2
  15. data/lib/archsight/analysis/sandbox.rb +8 -0
  16. data/lib/archsight/analysis/typst_renderer.rb +246 -0
  17. data/lib/archsight/analysis.rb +1 -0
  18. data/lib/archsight/annotations/annotation.rb +30 -3
  19. data/lib/archsight/annotations/architecture_annotations.rb +47 -0
  20. data/lib/archsight/annotations/asset_annotations.rb +34 -0
  21. data/lib/archsight/annotations/link_annotations.rb +15 -0
  22. data/lib/archsight/annotations/resource_reference.rb +12 -0
  23. data/lib/archsight/annotations/risk_annotations.rb +21 -0
  24. data/lib/archsight/cli.rb +124 -23
  25. data/lib/archsight/database.rb +40 -0
  26. data/lib/archsight/diagram/cli.rb +50 -2
  27. data/lib/archsight/diagram/formatter.rb +112 -0
  28. data/lib/archsight/diagram/routing/edge_router/orthogonal_path.rb +38 -10
  29. data/lib/archsight/diagram/routing/edge_router.rb +29 -4
  30. data/lib/archsight/diagram/routing/edge_routing.rb +61 -5
  31. data/lib/archsight/diagram/style/theme.rb +7 -2
  32. data/lib/archsight/documentation.rb +3 -2
  33. data/lib/archsight/editor/page_source.rb +13 -1
  34. data/lib/archsight/export/confluence/analysis_output.rb +101 -0
  35. data/lib/archsight/export/confluence/client.rb +13 -0
  36. data/lib/archsight/export/confluence/diagram_links.rb +1 -1
  37. data/lib/archsight/export/confluence/exporter.rb +40 -13
  38. data/lib/archsight/export/confluence/labels.rb +25 -0
  39. data/lib/archsight/export/confluence/page_properties.rb +4 -6
  40. data/lib/archsight/export/confluence/storage.rb +38 -2
  41. data/lib/archsight/helpers/embeds.rb +18 -7
  42. data/lib/archsight/helpers/resource_resolver.rb +25 -0
  43. data/lib/archsight/import/executor.rb +139 -16
  44. data/lib/archsight/import/handler.rb +55 -6
  45. data/lib/archsight/import/handler_loader.rb +64 -0
  46. data/lib/archsight/import/handlers/cache_pruner.rb +30 -7
  47. data/lib/archsight/import/handlers/command.rb +120 -0
  48. data/lib/archsight/import/handlers/github.rb +4 -1
  49. data/lib/archsight/import/handlers/gitlab.rb +4 -1
  50. data/lib/archsight/import/handlers/go_dep_resolver.rb +10 -2
  51. data/lib/archsight/import/handlers/go_grapher.rb +102 -5
  52. data/lib/archsight/import/handlers/go_module_parser.rb +88 -0
  53. data/lib/archsight/import/handlers/grapher.rb +40 -1
  54. data/lib/archsight/import/handlers/repository.rb +3 -0
  55. data/lib/archsight/import/handlers/rest_api_index.rb +3 -0
  56. data/lib/archsight/import/registry.rb +22 -1
  57. data/lib/archsight/import.rb +64 -1
  58. data/lib/archsight/linter/structure.rb +239 -0
  59. data/lib/archsight/linter.rb +217 -6
  60. data/lib/archsight/page_loader.rb +12 -1
  61. data/lib/archsight/query/evaluator.rb +13 -6
  62. data/lib/archsight/query/parser.rb +73 -6
  63. data/lib/archsight/query.rb +1 -1
  64. data/lib/archsight/references.rb +3 -3
  65. data/lib/archsight/renderer.rb +80 -23
  66. data/lib/archsight/resources/analysis.rb +3 -2
  67. data/lib/archsight/resources/application_component.rb +83 -8
  68. data/lib/archsight/resources/application_event.rb +79 -0
  69. data/lib/archsight/resources/application_service.rb +18 -2
  70. data/lib/archsight/resources/base.rb +58 -5
  71. data/lib/archsight/resources/business_actor.rb +14 -1
  72. data/lib/archsight/resources/business_contract.rb +72 -0
  73. data/lib/archsight/resources/business_control.rb +10 -2
  74. data/lib/archsight/resources/business_event.rb +79 -0
  75. data/lib/archsight/resources/business_process.rb +8 -1
  76. data/lib/archsight/resources/business_product.rb +12 -0
  77. data/lib/archsight/resources/business_role.rb +70 -0
  78. data/lib/archsight/resources/compliance_evidence.rb +8 -2
  79. data/lib/archsight/resources/data_object.rb +6 -1
  80. data/lib/archsight/resources/implementation_deliverable.rb +63 -0
  81. data/lib/archsight/resources/implementation_event.rb +52 -0
  82. data/lib/archsight/resources/implementation_gap.rb +49 -0
  83. data/lib/archsight/resources/implementation_plateau.rb +67 -0
  84. data/lib/archsight/resources/implementation_work_package.rb +84 -0
  85. data/lib/archsight/resources/import.rb +37 -11
  86. data/lib/archsight/resources/location.rb +56 -0
  87. data/lib/archsight/resources/motivation_assessment.rb +121 -0
  88. data/lib/archsight/resources/motivation_constraint.rb +7 -1
  89. data/lib/archsight/resources/motivation_driver.rb +50 -0
  90. data/lib/archsight/resources/motivation_goal.rb +13 -1
  91. data/lib/archsight/resources/motivation_principle.rb +75 -0
  92. data/lib/archsight/resources/motivation_requirement.rb +18 -2
  93. data/lib/archsight/resources/motivation_stakeholder.rb +21 -0
  94. data/lib/archsight/resources/page.rb +9 -7
  95. data/lib/archsight/resources/strategy_capability.rb +9 -0
  96. data/lib/archsight/resources/technology_artifact.rb +11 -4
  97. data/lib/archsight/resources/technology_communication_network.rb +89 -0
  98. data/lib/archsight/resources/technology_device.rb +88 -0
  99. data/lib/archsight/resources/technology_equipment.rb +75 -0
  100. data/lib/archsight/resources/technology_event.rb +80 -0
  101. data/lib/archsight/resources/technology_facility.rb +70 -0
  102. data/lib/archsight/resources/technology_node.rb +57 -1
  103. data/lib/archsight/resources/technology_service.rb +33 -0
  104. data/lib/archsight/resources/technology_system_software.rb +28 -0
  105. data/lib/archsight/resources.rb +12 -0
  106. data/lib/archsight/version.rb +1 -1
  107. data/lib/archsight/web/api/analysis_helpers.rb +55 -0
  108. data/lib/archsight/web/api/json_helpers.rb +3 -37
  109. data/lib/archsight/web/api/openapi/spec.yaml +10 -8
  110. data/lib/archsight/web/api/page_helpers.rb +1 -8
  111. data/lib/archsight/web/api/routes.rb +7 -2
  112. data/lib/archsight/web/application.rb +38 -2
  113. data/lib/archsight/web/editor/routes.rb +2 -1
  114. data/lib/archsight/web/public/vue/ApiDocsPage-COp4onwn.js +1 -0
  115. data/lib/archsight/web/public/vue/DocPage-D52OwaKc.js +1 -0
  116. data/lib/archsight/web/public/vue/EditorPage-3aqL7yVf.js +35 -0
  117. data/lib/archsight/web/public/vue/ErrorPage--b9XeSYa.js +2 -0
  118. data/lib/archsight/web/public/vue/GraphView-qh_EtRrQ.js +1 -0
  119. data/lib/archsight/web/public/vue/HomePage-C2TNh2U-.js +2 -0
  120. data/lib/archsight/web/public/vue/InstanceRouter-ZaCcjlIX.css +1 -0
  121. data/lib/archsight/web/public/vue/InstanceRouter-uzb_OMG0.js +1 -0
  122. data/lib/archsight/web/public/vue/KindList-CyJuP_uq.js +1 -0
  123. data/lib/archsight/web/public/vue/KindList-yYcRT2BV.css +1 -0
  124. data/lib/archsight/web/public/vue/PageView-B93woeN3.css +1 -0
  125. data/lib/archsight/web/public/vue/PageView-FyygrS-b.js +1 -0
  126. data/lib/archsight/web/public/vue/QueryError-B4Ex_csZ.js +1 -0
  127. data/lib/archsight/web/public/vue/ResourceList-BY6Z1Kcb.js +2 -0
  128. data/lib/archsight/web/public/vue/ResourceList-D75O87zG.css +1 -0
  129. data/lib/archsight/web/public/vue/{SearchResults-CyPUZZSC.css → SearchResults-BV6UHriJ.css} +1 -1
  130. data/lib/archsight/web/public/vue/SearchResults-CkFpHF8m.js +1 -0
  131. data/lib/archsight/web/public/vue/WikiPage-BoHNewCX.js +3 -0
  132. data/lib/archsight/web/public/vue/WikiPage-TpmM1VAF.css +1 -0
  133. data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BvzKZE4e.js +1 -0
  134. data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-D-xs_Hxh.js +1 -0
  135. data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-nbjxG01S.js +1 -0
  136. data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-CW6mw2ab.js +1 -0
  137. data/lib/archsight/web/public/vue/index-BGlYCbi7.js +3 -0
  138. data/lib/archsight/web/public/vue/index-eUHmJ7K2.css +1 -0
  139. data/lib/archsight/web/public/vue/info-A6RAGUB7-C12sjlHe.js +1 -0
  140. data/lib/archsight/web/public/vue/{mermaid-BjEi5URd.js → mermaid-DjsXQ_XN.js} +35 -35
  141. data/lib/archsight/web/public/vue/packet-AYTQ26CC-wMwKwHmA.js +1 -0
  142. data/lib/archsight/web/public/vue/pie-WAS4IAKB-w1-sAU_K.js +1 -0
  143. data/lib/archsight/web/public/vue/radar-RG4KPBEZ-DpwqIFoy.js +1 -0
  144. data/lib/archsight/web/public/vue/railroad-74A4TZTK-Ce5XC24d.js +1 -0
  145. data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-BSQO2pds.js +1 -0
  146. data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-Dq1c2YtD.js +1 -0
  147. data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DKj6dli0.js +1 -0
  148. data/lib/archsight/web/public/vue/rolldown-runtime-hePW80VL.js +1 -0
  149. data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-BmHNTWxU.js +1 -0
  150. data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DsEI7sSD.js +1 -0
  151. data/lib/archsight/web/public/vue/{useGraphviz-DweKV7Kg.js → useGraphviz-4evusK9i.js} +13 -1
  152. data/lib/archsight/web/public/vue/{useHighlight-D5Gf8DLl.js → useHighlight-DPguaxfm.js} +8 -8
  153. data/lib/archsight/web/public/vue/usePanZoom-seQE1TPk.js +12 -0
  154. data/lib/archsight/web/public/vue/wardley-WFR3VGLG-CSCW83-M.js +1 -0
  155. data/lib/archsight/web/public/vue.html +5 -5
  156. metadata +72 -38
  157. data/lib/archsight/web/public/vue/ApiDocsPage-DoOxKjG0.js +0 -1
  158. data/lib/archsight/web/public/vue/DocPage-CfsC3CeQ.js +0 -1
  159. data/lib/archsight/web/public/vue/EditorPage-CsJA0q8n.js +0 -35
  160. data/lib/archsight/web/public/vue/ErrorPage-OJpJz9df.js +0 -2
  161. data/lib/archsight/web/public/vue/GraphView-CBq6oFRV.js +0 -1
  162. data/lib/archsight/web/public/vue/HomePage-BwgMLgVC.js +0 -2
  163. data/lib/archsight/web/public/vue/InstanceRouter-DQz-SsQq.js +0 -1
  164. data/lib/archsight/web/public/vue/InstanceRouter-jeavM6PW.css +0 -1
  165. data/lib/archsight/web/public/vue/KindList-4q0K9Cll.js +0 -1
  166. data/lib/archsight/web/public/vue/PageView-DzeGnvoS.js +0 -1
  167. data/lib/archsight/web/public/vue/QueryError-kg6Yf-pW.js +0 -1
  168. data/lib/archsight/web/public/vue/ResourceList-B0FLaFR3.css +0 -1
  169. data/lib/archsight/web/public/vue/ResourceList-Df_0e0qt.js +0 -2
  170. data/lib/archsight/web/public/vue/SearchResults-CKgY6_wH.js +0 -1
  171. data/lib/archsight/web/public/vue/WikiPage-DoFJ0dbW.css +0 -1
  172. data/lib/archsight/web/public/vue/WikiPage-DrvCHwJ7.js +0 -13
  173. data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BfA1TPxQ.js +0 -1
  174. data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-BmgdUKVD.js +0 -1
  175. data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-DbXDvAWF.js +0 -1
  176. data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-BkVhrKS1.js +0 -1
  177. data/lib/archsight/web/public/vue/index-D0Q5GZRs.js +0 -3
  178. data/lib/archsight/web/public/vue/index-oF-iyZvk.css +0 -1
  179. data/lib/archsight/web/public/vue/info-A6RAGUB7-BIlCVuUY.js +0 -1
  180. data/lib/archsight/web/public/vue/packet-AYTQ26CC-eoQTjY3j.js +0 -1
  181. data/lib/archsight/web/public/vue/pie-WAS4IAKB-DEOotGhh.js +0 -1
  182. data/lib/archsight/web/public/vue/radar-RG4KPBEZ-C2yucuUN.js +0 -1
  183. data/lib/archsight/web/public/vue/railroad-74A4TZTK-CzONK5VY.js +0 -1
  184. data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-9zRPsDsx.js +0 -1
  185. data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-BJ4dkz9l.js +0 -1
  186. data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DFy_rqAj.js +0 -1
  187. data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-2cq5CyD2.js +0 -1
  188. data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DK-e9Ez4.js +0 -1
  189. data/lib/archsight/web/public/vue/wardley-WFR3VGLG-BQwqUqfB.js +0 -1
@@ -52,7 +52,7 @@ module Archsight
52
52
  def parse(source)
53
53
  lexer = Lexer.new(source)
54
54
  tokens = lexer.tokenize
55
- parser = Parser.new(tokens)
55
+ parser = Parser.new(tokens, source: source)
56
56
  parser.parse
57
57
  rescue LexerError, ParseError, InvalidRegexError => e
58
58
  # Re-raise with source context
@@ -32,7 +32,7 @@ module Archsight
32
32
  end
33
33
  end
34
34
 
35
- # The resources a markdown text mentions with `[[...]]` links and `![[View/...]]` embeds, outside code.
35
+ # The resources a markdown text mentions with `[[...]]` links and `![[Kind/...]]` embeds, outside code.
36
36
  # @return [Array<Base>]
37
37
  def mentions(markdown, finder)
38
38
  visible = markdown.to_s.gsub(FENCE, "").gsub(INLINE_CODE, "")
@@ -111,10 +111,10 @@ module Archsight
111
111
  @links.exact_target_for(target)
112
112
  end
113
113
 
114
- # An `![[Kind/Name]]` embed: only views and analyses can be embedded
114
+ # An `![[Kind/Name]]` embed: any resource but a page
115
115
  def embed(reference)
116
116
  kind = reference.split("/", 2).first
117
- return unless Helpers::Embeds::KINDS.include?(kind)
117
+ return unless Helpers::Embeds.embeddable_kind?(kind)
118
118
 
119
119
  reference(reference)
120
120
  end
@@ -40,9 +40,11 @@ module Archsight
40
40
  def gnode(graph, inst, opts = {})
41
41
  label = "<TABLE><TR><TD><B>#{inst.name}</B></TD></TR><TR><TD>#{inst.klass}</TD></TR></TABLE>"
42
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}"
43
+ attrs = { class: css_class(inst), shape: :box, style: "rounded,filled", fontname: FONT, label: label,
44
+ href: "/kinds/#{inst.klass}/instances/#{inst.name}" }
45
+ # A resource at the depth limit with more dependencies: dashed, its own page shows them
46
+ attrs.merge!(style: "rounded,filled,dashed", tooltip: "More dependencies: open it to explore them") if opts[:truncated]
47
+ graph.node gname(inst), attrs
46
48
  end
47
49
 
48
50
  def gedge(graph, a_inst, b_inst, label)
@@ -104,29 +106,84 @@ module Archsight
104
106
  end
105
107
  end
106
108
 
107
- def create_graph_one(db, klass_pat, name_pat)
108
- name = "#{klass_pat}:#{name_pat}"
109
- nodes = {}
110
- edges = {}
111
- Archsight::Graphvis.new(name).draw_dot 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)
109
+ # The layer an instance graph also follows by default, below the instance's own layer (technology is the bottom
110
+ # of the stack; implementation points at what it changes)
111
+ NEXT_LAYER = { "motivation" => "strategy", "strategy" => "business", "business" => "application",
112
+ "application" => "technology", "implementation" => "application" }.freeze
113
+
114
+ # The layers the graph of a resource of this layer follows when its page opens: its own and the next one below,
115
+ # every layer for `other` (pages, views, locations, ... refer to anything)
116
+ def self.default_graph_layers(layer)
117
+ return Archsight::Resources::LAYERS.dup unless Archsight::Resources::LAYERS.include?(layer) && layer != "other"
118
+
119
+ [layer, NEXT_LAYER[layer]].compact
120
+ end
121
+
122
+ # The dependency graph of one instance: the resources its relations lead to, breadth first, so a resource gets
123
+ # the depth of its shortest path.
124
+ # @param max_depth [Integer, nil] follow relations this many steps from the instance (nil: all the way). A
125
+ # resource at the limit that has more relations is drawn dashed.
126
+ # @param layers [Array<String>, nil] follow only relations to resources of these layers (nil: every layer); the
127
+ # instance itself is always shown
128
+ def create_graph_one(db, klass_pat, name_pat, max_depth: nil, layers: nil)
129
+ klass = Archsight::Resources[klass_pat] || raise("kind #{klass_pat} unknown")
130
+ root = db.instances[klass]&.[](name_pat) || raise("name #{name_pat} for kind #{klass_pat} not found")
131
+
132
+ Archsight::Graphvis.new("#{klass_pat}:#{name_pat}").draw_dot do |g|
133
+ nodes = {}
134
+ edges = {}
135
+ queue = [[root, 0]]
136
+ while queue.any?
137
+ inst, depth = queue.shift
138
+ next if nodes[inst]
139
+
140
+ nodes[inst] = true
141
+ targets = graph_targets(inst, layers)
142
+ if max_depth && depth >= max_depth
143
+ gnode(g, inst, truncated: targets.any?)
144
+ next
145
+ end
146
+
147
+ gnode(g, inst)
148
+ targets.each do |verb, rel|
149
+ edge_name = "#{gname(inst)}|#{verb}|#{gname(rel)}"
150
+ gedge(g, inst, rel, verb) unless edges[edge_name]
151
+ edges[edge_name] = true
152
+ queue << [rel, depth + 1] unless nodes[rel]
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ # The relations of a kind to its own kind (a team's sub-teams, a service's dependencies on other services) as
159
+ # [verb, key] pairs
160
+ def self.self_relations(klass)
161
+ kind = klass.name.to_s.split("::").last
162
+ klass.declared_relations.filter_map { |verb, key, target| [verb, key] if target.to_s == kind }
163
+ end
164
+
165
+ # The graph of a kind: its resources that are related to each other by its relations to its own kind (an org
166
+ # chart for BusinessActor). Resources without such a relation are left out, the kind's list shows them.
167
+ def create_graph_kind(db, klass_pat)
168
+ klass = Archsight::Resources[klass_pat] || raise("kind #{klass_pat} unknown")
169
+ relations = Archsight::GraphvisRenderer.self_relations(klass)
170
+ sources = (db.instances[klass] || {}).values.reject(&:abandoned?)
171
+ edges = sources.flat_map do |inst|
172
+ relations.flat_map { |verb, key| inst.relations(verb, key).reject(&:abandoned?).map { |rel| [inst, verb, rel] } }
173
+ end
174
+ edges.uniq! { |inst, verb, rel| "#{gname(inst)}|#{verb}|#{gname(rel)}" }
175
+
176
+ Archsight::Graphvis.new(klass_pat).draw_dot do |g|
177
+ edges.flat_map { |inst, _, rel| [inst, rel] }.uniq.each { |inst| gnode(g, inst, simple_label: true) }
178
+ edges.each { |inst, verb, rel| gedge(g, inst, rel, verb) }
116
179
  end
117
180
  end
118
181
 
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)
182
+ # The [verb, target] pairs of an instance's relations the graph follows: those to resources of the layers
183
+ def graph_targets(inst, layers)
184
+ inst.class.relations.flat_map do |verb, kind|
185
+ inst.relations(verb, kind).filter_map do |rel|
186
+ [verb, rel] if layers.nil? || layers.include?(rel.class.layer)
130
187
  end
131
188
  end
132
189
  end
@@ -57,8 +57,9 @@ class Archsight::Resources::Analysis < Archsight::Resources::Base
57
57
 
58
58
  # Description
59
59
  annotation "analysis/description",
60
- description: "Human-readable description of what this analysis validates",
61
- title: "Description"
60
+ description: "Human-readable description of what this analysis validates (markdown)",
61
+ title: "Description",
62
+ format: :markdown
62
63
 
63
64
  # Execution configuration
64
65
  annotation "analysis/timeout",
@@ -2,7 +2,7 @@
2
2
 
3
3
  # ApplicationComponent a part of the ApplicationService
4
4
  class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
5
- include_annotations :git, :architecture, :generated, :backup
5
+ include_annotations :git, :architecture, :generated, :backup, :asset, :risk
6
6
 
7
7
  description <<~MD
8
8
  Represents a logical part of an application service that can be deployed independently.
@@ -24,6 +24,53 @@ class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
24
24
  - Backend components
25
25
  - Frontend applications
26
26
  - Background workers
27
+
28
+ ## Choosing this kind
29
+
30
+ **Question:** What is built, deployed and versioned?
31
+ **Test:** It has artifacts and a `component/type`, and can be deployed or linked.
32
+ **Name:** The part, by runtime role (API, Operator, Worker) as in "Payments:API", not by repository category
33
+ ("Testing", "Tools", "Infrastructure"). Test, tooling and infrastructure-as-code repositories belong to the
34
+ component they build, test or deploy.
35
+
36
+ A component belongs to exactly one service. See the modeling guide, "Choosing the Kind".
37
+
38
+ ## Component types
39
+
40
+ Set `component/type` to say what the component is built as; importers set it from the manifests:
41
+
42
+ - `executable`: has an entry point and is run or deployed (services, workers, operators, command-line tools)
43
+ - `library`: imported by other components and not run on its own (shared libraries, SDKs)
44
+ - `module`: a unit of versioning or dependency that bundles executables and importable libraries (a Go module
45
+ with `cmd/` and shared packages, a Maven parent, an npm workspace root)
46
+ - `plugin`: loaded by a host program (kubectl, Terraform, Grafana)
47
+ - `frontend`: runs in the browser
48
+ - `other`
49
+
50
+ `component/role` refines an executable or plugin (`service`, `cli`, `job`, `operator`, `agent`); importers cannot
51
+ tell these apart, so it is set by hand and never overwritten. Importers also add machine tags
52
+ (`namespace:value`) to the regular `architecture/tags`: `ecosystem:go`, `packaging:go-module`,
53
+ `linkage:shared`; the tags a person set stay, and a re-import only replaces the machine tags of these
54
+ namespaces. The entry points (main packages such as `cmd/api`) go to `component/entrypoints`, a list that
55
+ works in queries like a tag but is not a sidebar filter, as there are too many distinct values. Deployable forms (container image, chart, package) are `TechnologyArtifact`s, not
56
+ component types.
57
+
58
+ ## Where it runs
59
+
60
+ A component is `servedBy` the `TechnologyService` it is deployed on (a product's cluster, a managed platform) and
61
+ `servedBy` the `TechnologySystemSoftware` it runs on (the runtime, such as a Kubernetes distribution). Deployment
62
+ importers write the service per component, so a sidecar or agent deployed into many clusters shows where it runs
63
+ without making its own service look like it runs in all of them.
64
+
65
+ The computed `component/dependents` counts the components that depend on this one, which tells shared libraries
66
+ and modules from components nobody consumes.
67
+
68
+ ## Security and risk modelling
69
+
70
+ - **Asset at risk:** set `asset/value` and the protection needs; `assesses` from a vulnerability or risk.
71
+ - **Control implementation:** a component that implements a control measure `realizes` the requirement and is
72
+ `evidencedBy` evidence.
73
+ - **Threat agent:** a component can be the `causedBy` of an event (a compromised service).
27
74
  MD
28
75
 
29
76
  icon "component"
@@ -39,6 +86,18 @@ class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
39
86
  title: "Architecture Size",
40
87
  enum: %w[microservice monolith]
41
88
 
89
+ # Classification
90
+ annotation "component/type",
91
+ description: "What the component is built as (executable, library, module, ...)",
92
+ title: "Component Type",
93
+ enum: %w[executable library module plugin frontend other],
94
+ filter: :word
95
+ annotation "component/role",
96
+ description: "What an executable or plugin does; set by hand",
97
+ title: "Component Role",
98
+ enum: %w[service cli job operator agent],
99
+ filter: :word
100
+
42
101
  # Availability
43
102
  annotation "availability/quality",
44
103
  description: "Service availability quality target per year",
@@ -122,6 +181,12 @@ class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
122
181
  enum: ["no support", "support by dev", "support by ops", "support by api",
123
182
  "self service support via a frontend"]
124
183
 
184
+ annotation "component/entrypoints",
185
+ description: "Comma-separated entry points (main packages or commands such as cmd/api), set by importers",
186
+ title: "Entry points",
187
+ sidebar: false,
188
+ filter: :list
189
+
125
190
  # Deployment (from ArgoCD/cluster)
126
191
  annotation "deployment/images",
127
192
  description: "Deployed container images",
@@ -129,20 +194,21 @@ class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
129
194
  sidebar: false,
130
195
  filter: :list
131
196
  annotation "deployment/chart",
132
- description: "Deployed Helm chart name",
197
+ description: "Deployed Helm charts, comma separated",
133
198
  title: "Helm Chart",
134
199
  sidebar: false,
135
- filter: :word
200
+ filter: :list
136
201
  annotation "deployment/namespace",
137
- description: "Kubernetes namespace",
202
+ description: "Kubernetes namespaces, comma separated",
138
203
  title: "Namespace",
139
204
  sidebar: false,
140
- filter: :word
205
+ filter: :list
141
206
  annotation "deployment/cluster",
142
- description: "Target cluster name",
207
+ description: "Target clusters, comma separated",
143
208
  title: "Cluster",
144
- filter: :word,
145
- summary: true
209
+ filter: :list,
210
+ summary: true,
211
+ deprecated: "list the clusters as spec.servedBy.technologyServices"
146
212
 
147
213
  # Computed Annotations
148
214
  computed_annotation "repository/artifacts/total",
@@ -190,6 +256,14 @@ class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
190
256
  end
191
257
  end
192
258
 
259
+ computed_annotation "component/dependents",
260
+ title: "Dependents",
261
+ description: "Number of components that depend directly on this one",
262
+ type: Integer,
263
+ sidebar: true do
264
+ count(incoming(:ApplicationComponent))
265
+ end
266
+
193
267
  computed_annotation "scc/language",
194
268
  title: "Primary Language",
195
269
  summary: true,
@@ -343,6 +417,7 @@ class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
343
417
  relation :realizedThrough, :technologyArtifacts, :TechnologyArtifact
344
418
  relation :realizedBy, :technologyComponents, :TechnologyNode
345
419
  relation :servedBy, :technologyComponents, :TechnologySystemSoftware
420
+ relation :servedBy, :technologyServices, :TechnologyService
346
421
  relation :exposes, :applicationInterfaces, :ApplicationInterface
347
422
  relation :dependsOn, :applicationInterfaces, :ApplicationInterface
348
423
  relation :dependsOn, :applicationComponents, :ApplicationComponent
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ApplicationEvent represents an application event: something that happens in an application and triggers or interrupts application behavior
4
+ class Archsight::Resources::ApplicationEvent < Archsight::Resources::Base
5
+ include_annotations :git, :architecture, :risk
6
+
7
+ description <<~MD
8
+ Represents an application event: something that happens in an application and triggers or interrupts application behavior.
9
+
10
+ ## ArchiMate Definition
11
+
12
+ **Layer:** Application
13
+ **Aspect:** Behavior
14
+
15
+ An event is something that happens and influences behavior. It does not last: it triggers or interrupts
16
+ processes and services. In the risk and security overlay of the Open Group paper (*Modeling Enterprise Risk
17
+ Management and Security with the ArchiMate Language*) a threat event and a loss event are events that carry a
18
+ type; set `event/type` to say which.
19
+
20
+ ## Usage
21
+
22
+ Use ApplicationEvent for events on the application level:
23
+
24
+ - Threat events and loss events of applications (an exploit of a service, a burst of failed logins, a data leak)
25
+ - Security alerts raised by an application
26
+ - Triggers of application services (a message arrives, a job is scheduled)
27
+
28
+ ## Event types
29
+
30
+ - `threat-event`: an event with the potential to harm an asset; it can trigger a loss event
31
+ - `attack`: a threat event caused by intentional malicious activity
32
+ - `loss-event`: an event that harms an asset (a hazard materialises, a vulnerability is exploited)
33
+ - `incident`: a loss event that has happened
34
+ - `opportunity-event`: an event that can add value
35
+ - `audit`, `scan`, `change`: events that produce assessments or alter the architecture
36
+ - `other`
37
+
38
+ Filter, group and query by `event/type`, `risk/domain` and `risk/category` to see, for example, every loss
39
+ event of a risk domain.
40
+
41
+ ## How it connects
42
+
43
+ - A threat event `triggers` a loss event or a process or service; threat and loss events may sit on different
44
+ layers (a technology event triggers an application or business event)
45
+ - `causedBy` the threat agent (an actor, component or node)
46
+ - `affects` the assets it harms
47
+ - A `MotivationDriver` (the threat) `triggers` it; a vulnerability `MotivationAssessment` `influences` it
48
+ MD
49
+
50
+ icon "bell-notification"
51
+ layer "application"
52
+
53
+ annotation "event/type",
54
+ description: "What kind of event this is (specialization of the event, see the risk and security overlay)",
55
+ enum: %w[threat-event attack loss-event incident opportunity-event audit scan change other],
56
+ filter: :word,
57
+ summary: true
58
+
59
+ annotation "event/severity",
60
+ description: "Severity of the event",
61
+ enum: %w[info low medium high critical],
62
+ filter: :word,
63
+ summary: true
64
+
65
+ annotation "event/occurred",
66
+ description: "When the event happened or is expected (ISO 8601 date or time)",
67
+ title: "Occurred",
68
+ validator: ->(value) { Archsight::Resources::Page.timestamp_error(value) }
69
+
70
+ relation :triggers, :applicationServices, :ApplicationService
71
+ relation :triggers, :applicationEvents, :ApplicationEvent
72
+ relation :triggers, :businessEvents, :BusinessEvent
73
+ relation :causedBy, :businessActors, :BusinessActor
74
+ relation :causedBy, :applicationComponents, :ApplicationComponent
75
+ relation :causedBy, :technologyNodes, :TechnologyNode
76
+ relation :affects, :applicationServices, :ApplicationService
77
+ relation :affects, :applicationComponents, :ApplicationComponent
78
+ relation :affects, :dataObjects, :DataObject
79
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  # ApplicationService represents the high level application service that implements capabilities
4
4
  class Archsight::Resources::ApplicationService < Archsight::Resources::Base
5
- include_annotations :git, :architecture, :generated, :backup
5
+ include_annotations :git, :architecture, :generated, :backup, :asset, :risk
6
6
 
7
7
  description <<~MD
8
8
  Represents a high-level application service that implements business capabilities.
@@ -20,10 +20,25 @@ class Archsight::Resources::ApplicationService < Archsight::Resources::Base
20
20
 
21
21
  Use ApplicationService to represent:
22
22
 
23
- - Complete product offerings (ManagedKubernetes, DBaaS PostgreSQL)
23
+ - Behaviour software offers to a user or another system ("Payment Authorization", "Document Search")
24
24
  - Logical groupings of application components
25
25
  - Services exposed to business processes
26
26
  - APIs and their implementations as a cohesive unit
27
+
28
+ ## Choosing this kind
29
+
30
+ **Question:** What behaviour does software offer a consumer?
31
+ **Test:** It can be described without naming repositories, deployments or runtime objects, and has one owning actor.
32
+ **Name:** The function, not the technology or a deployment object ("Payment Authorization").
33
+
34
+ A component belongs to exactly one service. One service using another is `dependsOn` between the two services, not
35
+ the other service's components listed again; a service that many others use is modeled once. A service whose only component has the same name is a smell. See
36
+ the modeling guide, "Choosing the Kind".
37
+
38
+ ## Security and risk modelling
39
+
40
+ A service is an asset at risk (`asset/*` profile, protection needs) that loss events `affects`, a trigger target
41
+ for `ApplicationEvent`s, and the place where requirements are realized and evidenced.
27
42
  MD
28
43
 
29
44
  icon "cube"
@@ -212,6 +227,7 @@ class Archsight::Resources::ApplicationService < Archsight::Resources::Base
212
227
  end
213
228
 
214
229
  relation :realizedThrough, :applicationComponents, :ApplicationComponent
230
+ relation :dependsOn, :applicationServices, :ApplicationService
215
231
  relation :servedBy, :businessActors, :BusinessActor
216
232
  relation :servedBy, :technologyServices, :TechnologyService
217
233
  relation :realizes, :motivationConstraints, :MotivationConstraint
@@ -35,12 +35,20 @@ module Archsight
35
35
  # A kind may mark at most this many annotations as summary (see Base.annotation)
36
36
  MAX_SUMMARY_ANNOTATIONS = 3
37
37
 
38
+ # The options `annotation` accepts (see Archsight::Annotations::Annotation)
39
+ ANNOTATION_OPTIONS = %i[description filter title format enum sidebar type summary editor validator unique
40
+ deprecated default].freeze
41
+
38
42
  # Define an annotation using the Annotation class
39
43
  # `summary: true` marks it as a summary attribute: it is returned with every search hit of this kind.
40
- def self.annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil, sidebar: true,
41
- type: nil, summary: false, editor: true, validator: nil)
42
- options = { description: description, filter: filter, title: title, format: format, enum: enum,
43
- sidebar: sidebar, type: type, summary: summary, editor: editor, validator: validator }
44
+ # `unique: true` makes it an identifier: its values (each element of a list) are unique among the resources
45
+ # of the kind, which `archsight lint` checks, and its values are shown as chips without links (not tags).
46
+ # `deprecated: "what to use instead"` keeps it working while `archsight lint` lists every use.
47
+ # `default: "value"` is the value of a resource that does not set it (see #apply_annotation_defaults!).
48
+ def self.annotation(key, **options)
49
+ unknown = options.keys - ANNOTATION_OPTIONS
50
+ raise ArgumentError, "unknown annotation option(s) #{unknown.join(", ")} for #{key}" if unknown.any?
51
+
44
52
  register_annotation(Archsight::Annotations::Annotation.new(key, options))
45
53
  end
46
54
 
@@ -143,8 +151,11 @@ module Archsight
143
151
  end
144
152
  end
145
153
 
154
+ # The layer of the kind, one of Archsight::Resources::LAYERS (default `other`)
146
155
  def self.layer(layer_name = nil)
147
156
  if layer_name
157
+ raise ArgumentError, "unknown layer #{layer_name.inspect} for #{name}" unless Archsight::Resources::LAYERS.include?(layer_name)
158
+
148
159
  @layer = layer_name
149
160
  else
150
161
  @layer || "other" # default layer
@@ -223,6 +234,26 @@ module Archsight
223
234
  @raw["metadata"]["annotations"][key] = value
224
235
  end
225
236
 
237
+ # Gives every annotation with a `default:` that the file does not set its default, in the annotations hash like
238
+ # a written value, so queries, filters and the page see it. The editor leaves these out (#defaulted?), so a
239
+ # save does not write them into the file.
240
+ def apply_annotation_defaults!
241
+ self.class.annotations.each do |annotation|
242
+ next if annotation.default.nil? || annotation.pattern? || annotations.key?(annotation.key)
243
+
244
+ defaulted = (@defaulted ||= []) #: Array[String]
245
+ defaulted << annotation.key
246
+ @raw["metadata"] ||= {}
247
+ @raw["metadata"]["annotations"] = @raw["metadata"]["annotations"] || {} #: Hash[String, untyped]
248
+ @raw["metadata"]["annotations"][annotation.key] = annotation.default
249
+ end
250
+ end
251
+
252
+ # Whether the value of the annotation is its default, not written in the file
253
+ def defaulted?(key)
254
+ @defaulted&.include?(key) || false
255
+ end
256
+
226
257
  # Get a computed annotation value from the cache
227
258
  # @param key [String] The annotation key
228
259
  # @return [Object, nil] The computed value or nil
@@ -338,7 +369,29 @@ module Archsight
338
369
 
339
370
  def merge!(inst)
340
371
  # NOTE: path reference is preserved from the original instance
341
- @raw = Archsight::Helpers.deep_merge(@raw, inst.raw)
372
+ merged = Archsight::Helpers.deep_merge(@raw, inst.raw)
373
+ union_list_annotations!(merged, inst.raw)
374
+ @raw = merged
375
+ end
376
+
377
+ # Annotations hold strings, which replace each other on merge. The values of a list annotation (tags, aliases)
378
+ # are joined instead, so two documents of one resource can each add to it.
379
+ def union_list_annotations!(merged, other_raw)
380
+ mine = @raw.dig("metadata", "annotations")
381
+ theirs = other_raw.dig("metadata", "annotations")
382
+ return unless mine.is_a?(Hash) && theirs.is_a?(Hash)
383
+
384
+ theirs.each do |key, value|
385
+ next unless mine[key].is_a?(String) && value.is_a?(String)
386
+ next unless self.class.annotation_matching(key)&.list?
387
+
388
+ values = (split_list(mine[key]) + split_list(value)).uniq
389
+ merged["metadata"]["annotations"][key] = values.join(",")
390
+ end
391
+ end
392
+
393
+ def split_list(text)
394
+ text.split(/,|\n/).map(&:strip).reject(&:empty?)
342
395
  end
343
396
 
344
397
  # raise provides a helper for better error messages including current path and line no
@@ -2,7 +2,7 @@
2
2
 
3
3
  # BusinessActor represents teams or organizational units
4
4
  class Archsight::Resources::BusinessActor < Archsight::Resources::Base
5
- include_annotations :git, :architecture, :generated
5
+ include_annotations :git, :architecture, :generated, :asset, :risk
6
6
 
7
7
  description <<~MD
8
8
  Represents a team, organizational unit, or external entity that performs business behavior.
@@ -25,6 +25,19 @@ class Archsight::Resources::BusinessActor < Archsight::Resources::Base
25
25
  - External vendors or partners
26
26
  - Support organizations
27
27
  - Cross-functional groups
28
+ - Customers and providers: the parties a `BusinessContract` is `agreedBy`
29
+ - The party behind a `MotivationStakeholder`: the stakeholder is `representedBy` it
30
+ - The team behind infrastructure: a `TechnologyDevice`, `TechnologyEquipment`, `TechnologyNode` or
31
+ `TechnologyCommunicationNetwork` is `ownedBy` the team that operates it, and a device, equipment or node is
32
+ `servedBy` the team it is dedicated to (a test server, a cluster partition)
33
+
34
+ ## Security and risk modelling
35
+
36
+ - **Threat agent:** an actor (also an external one, such as an attacker group or a supplier) can be the
37
+ `causedBy` of a threat or loss event.
38
+ - **Owner and executor:** actors own risks (`MotivationAssessment` `ownedBy`), policies and controls, directly or through a `BusinessRole` they `performedBy`.
39
+ - **Asset:** a supplier or a team can be an asset at risk; set the `asset/*` profile and `assesses` it from a
40
+ supplier assessment.
28
41
  MD
29
42
 
30
43
  icon "community"
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # BusinessContract represents an agreement between a provider and a consumer
4
+ class Archsight::Resources::BusinessContract < Archsight::Resources::Base
5
+ include_annotations :git, :architecture, :risk
6
+
7
+ description <<~MD
8
+ Represents an agreement between a provider and a consumer: the rights and obligations both agreed to.
9
+
10
+ ## ArchiMate Definition
11
+
12
+ **Layer:** Business
13
+ **Aspect:** Passive Structure
14
+
15
+ A contract is a formal or informal specification of an agreement between a provider and a consumer that
16
+ specifies rights and obligations. It is a specialization of a business object. A product is accompanied by a
17
+ contract, and a contract can consist of parts.
18
+
19
+ ## Usage
20
+
21
+ Use BusinessContract to represent:
22
+
23
+ - Framework contracts with a customer
24
+ - Call-offs under a framework contract (an order for one more block of capacity)
25
+ - Service level agreements
26
+ - Amendments of a contract
27
+
28
+ A contract is a `BusinessContract`, not a `DataObject` and not text in a description: only then do the graph,
29
+ views and analyses see what it agrees, who agreed it and what delivered it.
30
+
31
+ ## How it connects
32
+
33
+ - A `BusinessProduct` `aggregates` the contract that comes with it
34
+ - It is `agreedBy` its parties: the actual organisations (the customer, the provider), not a generic role
35
+ - It is `composedOf` its parts: call-offs, SLAs and amendments, which are contracts themselves (no cycles)
36
+ - It `covers` what it obliges the provider to deliver: equipment, nodes, technology and application services
37
+ - It is `realizedThrough` the `ImplementationDeliverable` that fulfils it; the `ImplementationWorkPackage` that
38
+ `realizes` the deliverable `affects` the equipment it installs
39
+
40
+ The other side shows these as incoming relations: the contracts an actor agreed, what covers a piece of
41
+ equipment or a service, the contract a deliverable fulfils.
42
+ MD
43
+
44
+ icon "journal-page"
45
+ layer "business"
46
+
47
+ annotation "contract/type",
48
+ description: "What kind of contract this is",
49
+ enum: %w[framework call-off sla amendment other],
50
+ filter: :word,
51
+ summary: true
52
+ annotation "contract/reference",
53
+ description: "The contract number",
54
+ title: "Reference",
55
+ summary: true
56
+ annotation "contract/from",
57
+ description: "When the contract starts (ISO 8601 date or time)",
58
+ title: "From",
59
+ validator: ->(value) { Archsight::Resources::Page.timestamp_error(value) }
60
+ annotation "contract/until",
61
+ description: "When the contract ends (ISO 8601 date or time)",
62
+ title: "Until",
63
+ validator: ->(value) { Archsight::Resources::Page.timestamp_error(value) }
64
+
65
+ relation :agreedBy, :businessActors, :BusinessActor
66
+ relation :composedOf, :businessContracts, :BusinessContract
67
+ relation :covers, :technologyEquipment, :TechnologyEquipment
68
+ relation :covers, :technologyNodes, :TechnologyNode
69
+ relation :covers, :technologyServices, :TechnologyService
70
+ relation :covers, :applicationServices, :ApplicationService
71
+ relation :realizedThrough, :implementationDeliverables, :ImplementationDeliverable
72
+ end