archsight 0.3.0 → 0.3.1

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 (131) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/docs/computed_annotations.md +7 -5
  4. data/docs/licenses.md +1 -2
  5. data/docs/modeling.md +5 -1
  6. data/docs/pages.md +63 -6
  7. data/docs/search.md +17 -0
  8. data/lib/archsight/annotations/annotation.rb +5 -4
  9. data/lib/archsight/annotations/computed.rb +5 -1
  10. data/lib/archsight/annotations/relation_resolver.rb +96 -79
  11. data/lib/archsight/cli.rb +3 -2
  12. data/lib/archsight/database.rb +2 -1
  13. data/lib/archsight/documentation.rb +2 -1
  14. data/lib/archsight/export/confluence/exporter.rb +11 -2
  15. data/lib/archsight/export/confluence/storage.rb +99 -8
  16. data/lib/archsight/export/confluence/tables.rb +78 -0
  17. data/lib/archsight/helpers/fenced_blocks.rb +61 -0
  18. data/lib/archsight/helpers/requirements_blocks.rb +90 -0
  19. data/lib/archsight/helpers/view_blocks.rb +108 -0
  20. data/lib/archsight/helpers/wiki_links.rb +39 -5
  21. data/lib/archsight/helpers.rb +3 -0
  22. data/lib/archsight/import/handlers/go_grapher.rb +4 -1
  23. data/lib/archsight/import/handlers/go_module_parser.rb +4 -1
  24. data/lib/archsight/linter.rb +25 -3
  25. data/lib/archsight/mcp/base.rb +38 -0
  26. data/lib/archsight/requirements.rb +70 -0
  27. data/lib/archsight/resources/analysis.rb +2 -1
  28. data/lib/archsight/resources/application_component.rb +5 -3
  29. data/lib/archsight/resources/application_interface.rb +4 -2
  30. data/lib/archsight/resources/application_service.rb +4 -3
  31. data/lib/archsight/resources/base.rb +27 -12
  32. data/lib/archsight/resources/business_actor.rb +5 -3
  33. data/lib/archsight/resources/business_product.rb +4 -3
  34. data/lib/archsight/resources/business_requirement.rb +4 -3
  35. data/lib/archsight/resources/compliance_evidence.rb +4 -2
  36. data/lib/archsight/resources/data_object.rb +2 -1
  37. data/lib/archsight/resources/import.rb +5 -2
  38. data/lib/archsight/resources/page.rb +4 -2
  39. data/lib/archsight/resources/technology_artifact.rb +4 -3
  40. data/lib/archsight/resources/technology_node.rb +1 -1
  41. data/lib/archsight/resources/technology_service.rb +4 -0
  42. data/lib/archsight/resources/technology_system_software.rb +4 -0
  43. data/lib/archsight/resources/view.rb +2 -1
  44. data/lib/archsight/version.rb +1 -1
  45. data/lib/archsight/view_table.rb +102 -0
  46. data/lib/archsight/web/api/json_helpers.rb +7 -5
  47. data/lib/archsight/web/api/openapi/spec.yaml +135 -2
  48. data/lib/archsight/web/api/requirements_helpers.rb +26 -0
  49. data/lib/archsight/web/api/routes.rb +19 -0
  50. data/lib/archsight/web/application.rb +6 -1
  51. data/lib/archsight/web/public/vue/ApiDocsPage-D-cPRZCT.js +1 -0
  52. data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +1 -0
  53. data/lib/archsight/web/public/vue/{DocPage-uaT8CdFm.js → DocPage-DK6vNDbF.js} +1 -1
  54. data/lib/archsight/web/public/vue/EditorPage-BoJpQaVw.js +35 -0
  55. data/lib/archsight/web/public/vue/EditorPage-CbG4mc9T.css +1 -0
  56. data/lib/archsight/web/public/vue/ErrorPage-Ck9izEUS.css +1 -0
  57. data/lib/archsight/web/public/vue/ErrorPage-PGyjdtEf.js +2 -0
  58. data/lib/archsight/web/public/vue/GraphView-BLiKR4zP.js +1 -0
  59. data/lib/archsight/web/public/vue/GraphView-BvWAbUAl.css +1 -0
  60. data/lib/archsight/web/public/vue/HomePage-C0lR8i2C.js +2 -0
  61. data/lib/archsight/web/public/vue/InstanceRouter-60Tt3ZNM.css +1 -0
  62. data/lib/archsight/web/public/vue/InstanceRouter-D3W2jJHV.js +1 -0
  63. data/lib/archsight/web/public/vue/KindList-BlsaRBNO.js +1 -0
  64. data/lib/archsight/web/public/vue/PageView-9MgHtrgl.js +1 -0
  65. data/lib/archsight/web/public/vue/QueryError-3EqjpbyV.css +1 -0
  66. data/lib/archsight/web/public/vue/QueryError-D1FL1xgA.js +1 -0
  67. data/lib/archsight/web/public/vue/ResourceList-B0FLaFR3.css +1 -0
  68. data/lib/archsight/web/public/vue/ResourceList-vkgFyeOY.js +2 -0
  69. data/lib/archsight/web/public/vue/SearchResults-Cl3O_OEr.js +1 -0
  70. data/lib/archsight/web/public/vue/SearchResults-DiW5XVYW.css +1 -0
  71. data/lib/archsight/web/public/vue/WikiPage-C-8SG67a.css +1 -0
  72. data/lib/archsight/web/public/vue/WikiPage-CeCQBTDS.js +13 -0
  73. data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BfA1TPxQ.js +1 -0
  74. data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-BmgdUKVD.js +1 -0
  75. data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-DbXDvAWF.js +1 -0
  76. data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-BkVhrKS1.js +1 -0
  77. data/lib/archsight/web/public/vue/index-D7m61Ahx.js +3 -0
  78. data/lib/archsight/web/public/vue/index-Dbx3MXWG.css +1 -0
  79. data/lib/archsight/web/public/vue/info-A6RAGUB7-BIlCVuUY.js +1 -0
  80. data/lib/archsight/web/public/vue/mermaid-BjEi5URd.js +3334 -0
  81. data/lib/archsight/web/public/vue/packet-AYTQ26CC-eoQTjY3j.js +1 -0
  82. data/lib/archsight/web/public/vue/pie-WAS4IAKB-DEOotGhh.js +1 -0
  83. data/lib/archsight/web/public/vue/radar-RG4KPBEZ-C2yucuUN.js +1 -0
  84. data/lib/archsight/web/public/vue/railroad-74A4TZTK-CzONK5VY.js +1 -0
  85. data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-9zRPsDsx.js +1 -0
  86. data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-BJ4dkz9l.js +1 -0
  87. data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DFy_rqAj.js +1 -0
  88. data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-2cq5CyD2.js +1 -0
  89. data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DK-e9Ez4.js +1 -0
  90. data/lib/archsight/web/public/vue/{useGraphviz-C71SdG-N.js → useGraphviz-DweKV7Kg.js} +1 -1
  91. data/lib/archsight/web/public/vue/wardley-WFR3VGLG-BQwqUqfB.js +1 -0
  92. data/lib/archsight/web/public/vue.html +3 -3
  93. data/lib/archsight.rb +2 -0
  94. metadata +49 -40
  95. data/lib/archsight/web/public/vue/ApiDocsPage-C0y953v0.css +0 -1
  96. data/lib/archsight/web/public/vue/ApiDocsPage-C_4tAWis.js +0 -1
  97. data/lib/archsight/web/public/vue/EditorPage-C557BJC-.js +0 -35
  98. data/lib/archsight/web/public/vue/EditorPage-df5N-p21.css +0 -1
  99. data/lib/archsight/web/public/vue/ErrorPage-Vdebmife.js +0 -2
  100. data/lib/archsight/web/public/vue/ErrorPage-uMDnfY5_.css +0 -1
  101. data/lib/archsight/web/public/vue/GraphView-BduUql2N.js +0 -1
  102. data/lib/archsight/web/public/vue/GraphView-Cj2V2stN.css +0 -1
  103. data/lib/archsight/web/public/vue/HomePage-BHUTg8Ap.js +0 -2
  104. data/lib/archsight/web/public/vue/InstanceRouter-1lSigA58.js +0 -1
  105. data/lib/archsight/web/public/vue/InstanceRouter-Di7f3Rya.css +0 -1
  106. data/lib/archsight/web/public/vue/KindList-BDZs0j6d.js +0 -1
  107. data/lib/archsight/web/public/vue/PageView-BYzJDwof.js +0 -1
  108. data/lib/archsight/web/public/vue/ResourceList-CnbhU9wm.js +0 -1
  109. data/lib/archsight/web/public/vue/ResourceList-xyBwu7fh.css +0 -1
  110. data/lib/archsight/web/public/vue/SearchResults-CNhf6VOx.js +0 -1
  111. data/lib/archsight/web/public/vue/SearchResults-DOHzqAy3.css +0 -1
  112. data/lib/archsight/web/public/vue/WikiPage-DbmWkM7W.js +0 -13
  113. data/lib/archsight/web/public/vue/WikiPage-GV67QmNJ.css +0 -1
  114. data/lib/archsight/web/public/vue/architecture-TIHT7OUA-Bf-MWFmn.js +0 -1
  115. data/lib/archsight/web/public/vue/cynefin-VYW2F7L2-CkKt8qqs.js +0 -1
  116. data/lib/archsight/web/public/vue/eventmodeling-45OFAUF4-NwSTYPHj.js +0 -1
  117. data/lib/archsight/web/public/vue/gitGraph-TEB2WS4Q-DLRegrRG.js +0 -1
  118. data/lib/archsight/web/public/vue/index-CyVWObLU.js +0 -3
  119. data/lib/archsight/web/public/vue/index-DtKeHT3S.css +0 -1
  120. data/lib/archsight/web/public/vue/info-DKCQHKI2-EV39NzoN.js +0 -1
  121. data/lib/archsight/web/public/vue/mermaid-BMkGfnhm.js +0 -3279
  122. data/lib/archsight/web/public/vue/packet-7NZHBO7P-USljV3MZ.js +0 -1
  123. data/lib/archsight/web/public/vue/pie-RZYD4A2V-DChciBW2.js +0 -1
  124. data/lib/archsight/web/public/vue/radar-I7S5WNFK-CgGJdW1t.js +0 -1
  125. data/lib/archsight/web/public/vue/railroad-3IZDKUUU-Dy43vF5c.js +0 -1
  126. data/lib/archsight/web/public/vue/railroad-abnf-AHOZXSZD-BhsTH-sE.js +0 -1
  127. data/lib/archsight/web/public/vue/railroad-ebnf-EBAXGLYW-BBTd_u0u.js +0 -1
  128. data/lib/archsight/web/public/vue/railroad-peg-LSFZ7HO6-BiNTEzf0.js +0 -1
  129. data/lib/archsight/web/public/vue/treeView-QDETBFTQ-BfnCcf-q.js +0 -1
  130. data/lib/archsight/web/public/vue/treemap-6X3UGDF4-BsAORhvk.js +0 -1
  131. data/lib/archsight/web/public/vue/wardley-OPB4EBWU-DQvnEhRj.js +0 -1
@@ -107,6 +107,8 @@ module Archsight
107
107
  @errors << "#{instance.path_ref}: Markdown syntax error in annotation '#{key}': #{e.message}"
108
108
  end
109
109
  validate_diagram_blocks(instance, key, value)
110
+ validate_view_blocks(instance, key, value)
111
+ validate_requirements_blocks(instance, key, value)
110
112
  validate_asset_images(instance, key, value)
111
113
  validate_embeds(instance, key, value)
112
114
  end
@@ -161,6 +163,25 @@ module Archsight
161
163
  end
162
164
  end
163
165
 
166
+ # Every ```view block must be a valid View, or the page shows an error box instead of the view
167
+ def validate_view_blocks(instance, key, value)
168
+ Helpers::ViewBlocks.sources(value).each_with_index do |source, index|
169
+ spec = Helpers::ViewBlocks.parse(source)
170
+ check_view_components(instance, spec[:fields])
171
+ rescue Helpers::ViewBlocks::Error => e
172
+ @errors << "#{instance.path_ref}: View error in annotation '#{key}' (view block #{index + 1}): #{e.message}"
173
+ end
174
+ end
175
+
176
+ # Every ```requirements block must be a valid filter, or the page shows an error box instead of the table
177
+ def validate_requirements_blocks(instance, key, value)
178
+ Helpers::RequirementsBlocks.sources(value).each_with_index do |source, index|
179
+ Helpers::RequirementsBlocks.parse(source)
180
+ rescue Helpers::RequirementsBlocks::Error => e
181
+ @errors << "#{instance.path_ref}: Requirements error in annotation '#{key}' (requirements block #{index + 1}): #{e.message}"
182
+ end
183
+ end
184
+
164
185
  # 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
186
  def render_diagram_source(instance, where, source)
166
187
  unresolved = []
@@ -173,10 +194,11 @@ module Archsight
173
194
  end
174
195
 
175
196
  def validate_view_fields(instance)
176
- fields = instance.annotations["view/fields"]
177
- return unless fields
197
+ check_view_components(instance, instance.annotations["view/fields"].to_s.split(",").map(&:strip))
198
+ end
178
199
 
179
- fields.split(",").map(&:strip).each do |field|
200
+ def check_view_components(instance, fields)
201
+ fields.each do |field|
180
202
  next unless field.start_with?("@")
181
203
 
182
204
  component_name = field[1..]
@@ -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
 
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Archsight
4
+ # The business requirements of a selection of resources, as the "Business Requirements" table of an instance page
5
+ # shows them, merged over all resources of the selection: one entry per BusinessRequirement, 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 = :businessRequirements
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? ? "Business 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 do
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
- list: true,
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
@@ -34,11 +34,13 @@ 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",
@@ -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
- list: true
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 do
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
- list: true,
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
@@ -23,13 +23,29 @@ module Archsight
23
23
  @relations || []
24
24
  end
25
25
 
26
+ # A kind may mark at most this many annotations as summary (see Base.annotation)
27
+ MAX_SUMMARY_ANNOTATIONS = 3
28
+
26
29
  # Define an annotation using the Annotation class
30
+ # `summary: true` marks it as a summary attribute: it is returned with every search hit of this kind.
27
31
  def self.annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil, sidebar: true,
28
- type: nil, list: false, editor: true, validator: nil)
29
- @annotations ||= [] #: Array[Archsight::Annotations::Annotation]
32
+ type: nil, summary: false, editor: true, validator: nil)
30
33
  options = { description: description, filter: filter, title: title, format: format, enum: enum,
31
- sidebar: sidebar, type: type, list: list, editor: editor, validator: validator }
32
- @annotations << Archsight::Annotations::Annotation.new(key, options)
34
+ sidebar: sidebar, type: type, summary: summary, editor: editor, validator: validator }
35
+ register_annotation(Archsight::Annotations::Annotation.new(key, options))
36
+ end
37
+
38
+ # Append an annotation definition, enforcing the limit of summary annotations per kind.
39
+ # Annotations of included modules (include_annotations) come through here as well.
40
+ def self.register_annotation(annotation)
41
+ @annotations ||= [] #: Array[Archsight::Annotations::Annotation]
42
+ if annotation.summary? && @annotations.count(&:summary?) >= MAX_SUMMARY_ANNOTATIONS
43
+ raise ArgumentError,
44
+ "#{name}: at most #{MAX_SUMMARY_ANNOTATIONS} annotations can be summary attributes, " \
45
+ "cannot add #{annotation.key}"
46
+ end
47
+
48
+ @annotations << annotation
33
49
  end
34
50
 
35
51
  # Get all annotation definitions
@@ -48,19 +64,18 @@ module Archsight
48
64
  # @param enum [Array, nil] Allowed values
49
65
  # @param sidebar [Boolean] Show in sidebar (default false for computed)
50
66
  # @param type [Class, nil] Type for value coercion (Integer, Float, String)
51
- # @param list [Boolean] Whether values are lists (default false)
67
+ # @param summary [Boolean] Return the value with every search hit of this kind (default false, max 3 per kind)
52
68
  # @yield Block that computes the annotation value, evaluated in Evaluator context
53
69
  def self.computed_annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil,
54
- sidebar: false, type: nil, list: false, editor: true, &)
70
+ sidebar: false, type: nil, summary: false, editor: true, &)
55
71
  require_relative "../annotations/computed"
56
72
  @computed_annotations ||= [] #: Array[Archsight::Annotations::Computed]
57
73
  @computed_annotations << Archsight::Annotations::Computed.new(key, description: description, type: type, &)
58
74
 
59
75
  # Also register as a regular annotation so it passes validation and is recognized
60
- @annotations ||= [] #: Array[Archsight::Annotations::Annotation]
61
76
  options = { description: description, filter: filter, title: title, format: format, enum: enum,
62
- sidebar: sidebar, type: type, list: list, editor: editor }
63
- @annotations << Archsight::Annotations::Annotation.new(key, options)
77
+ sidebar: sidebar, type: type, summary: summary, editor: editor }
78
+ register_annotation(Archsight::Annotations::Annotation.new(key, options))
64
79
  end
65
80
 
66
81
  # Get all computed annotation definitions
@@ -83,9 +98,9 @@ module Archsight
83
98
  annotations.select(&:filterable?).reject(&:pattern?)
84
99
  end
85
100
 
86
- # Get annotations marked for list display
87
- def self.list_annotations
88
- annotations.select(&:list_display?).reject(&:pattern?)
101
+ # Get the annotations marked as summary attributes (returned with search hits)
102
+ def self.summary_annotations
103
+ annotations.select(&:summary?).reject(&:pattern?)
89
104
  end
90
105
 
91
106
  def self.annotation_title(key)
@@ -36,7 +36,8 @@ class Archsight::Resources::BusinessActor < Archsight::Resources::Base
36
36
  sidebar: false,
37
37
  filter: :word,
38
38
  format: :tag_word,
39
- type: Archsight::Annotations::EmailRecipient
39
+ type: Archsight::Annotations::EmailRecipient,
40
+ summary: true
40
41
 
41
42
  annotation "team/members",
42
43
  description: 'Team members (format: "Name <email>" or "email")',
@@ -108,7 +109,7 @@ class Archsight::Resources::BusinessActor < Archsight::Resources::Base
108
109
  computed_annotation "team/size",
109
110
  title: "Team Size",
110
111
  description: "Number of team members (including sub-teams)",
111
- list: true,
112
+ summary: true,
112
113
  type: Integer do
113
114
  # Count members from this team
114
115
  members = @instance.annotations["team/members"]
@@ -147,7 +148,8 @@ class Archsight::Resources::BusinessActor < Archsight::Resources::Base
147
148
  computed_annotation "repository/artifacts/total",
148
149
  title: "Maintained Repositories",
149
150
  description: "Number of git repositories maintained by this team",
150
- type: Integer do
151
+ type: Integer,
152
+ summary: true do
151
153
  count(incoming_transitive('TechnologyArtifact: artifact/type == "repo"'))
152
154
  end
153
155
 
@@ -33,7 +33,8 @@ class Archsight::Resources::BusinessProduct < Archsight::Resources::Base
33
33
  computed_annotation "repository/artifacts/total",
34
34
  title: "Total Git Repositories",
35
35
  description: "Number of related git repositories",
36
- type: Integer do
36
+ type: Integer,
37
+ summary: true do
37
38
  count(outgoing_transitive('TechnologyArtifact: artifact/type == "repo"'))
38
39
  end
39
40
 
@@ -110,7 +111,7 @@ class Archsight::Resources::BusinessProduct < Archsight::Resources::Base
110
111
  computed_annotation "activity/createdAt",
111
112
  title: "Created",
112
113
  description: "Earliest repository creation date across all related application services",
113
- list: true,
114
+ summary: true,
114
115
  type: Time do
115
116
  services = outgoing_transitive(:ApplicationService)
116
117
  next nil if services.empty?
@@ -170,7 +171,7 @@ class Archsight::Resources::BusinessProduct < Archsight::Resources::Base
170
171
  computed_annotation "activity/contributors/6m",
171
172
  title: "Contributors (6 months)",
172
173
  description: "Sum of unique contributors in the last 6 months across related services",
173
- list: true,
174
+ summary: true,
174
175
  type: Integer do
175
176
  services = outgoing_transitive(:ApplicationService)
176
177
  next nil if services.empty?
@@ -32,20 +32,21 @@ class Archsight::Resources::BusinessRequirement < Archsight::Resources::Base
32
32
 
33
33
  annotation "requirement/type",
34
34
  description: "Type of requirement (business or legal)",
35
- enum: %w[business legal compliance functional non-functional]
35
+ enum: %w[business legal compliance functional non-functional],
36
+ summary: true
36
37
 
37
38
  annotation "requirement/reference",
38
39
  description: "Regulatory or standard reference (comma-separated for multiple)",
39
40
  filter: :list,
40
41
  enum: %w[c5-2020 itgs-2023 gdpr-2018 nis1 nis2 iso27001 sox pci-dss hipaa eu-data-act-2025 ens
41
42
  iso27001-2022],
42
- list: true
43
+ summary: true
43
44
 
44
45
  annotation "requirement/priority",
45
46
  description: "Implementation priority (must, should, may)",
46
47
  filter: :word,
47
48
  enum: %w[must should may],
48
- list: true
49
+ summary: true
49
50
 
50
51
  annotation "requirement/story",
51
52
  description: "One-line business value statement explaining what the requirement enables",
@@ -32,11 +32,13 @@ class Archsight::Resources::ComplianceEvidence < Archsight::Resources::Base
32
32
 
33
33
  annotation "evidence/type",
34
34
  description: "Type of evidence",
35
- enum: %w[documentation process configuration audit-log technical-control]
35
+ enum: %w[documentation process configuration audit-log technical-control],
36
+ summary: true
36
37
 
37
38
  annotation "evidence/status",
38
39
  description: "Current status of evidence",
39
- enum: %w[implemented partial not-implemented]
40
+ enum: %w[implemented partial not-implemented],
41
+ summary: true
40
42
 
41
43
  relation :satisfies, :businessRequirements, :BusinessRequirement
42
44
  end
@@ -38,7 +38,8 @@ class Archsight::Resources::DataObject < Archsight::Resources::Base
38
38
  annotation "data/visibility",
39
39
  description: "API visibility level",
40
40
  title: "Visibility",
41
- enum: %w[public private internal]
41
+ enum: %w[public private internal],
42
+ summary: true
42
43
 
43
44
  annotation "generated/variants",
44
45
  description: "OpenAPI schema variants compacted into this DataObject",
@@ -38,7 +38,9 @@ class Archsight::Resources::Import < Archsight::Resources::Base
38
38
  # Handler selection
39
39
  annotation "import/handler",
40
40
  description: "Handler class name to execute this import",
41
- title: "Handler", enum: %w[
41
+ title: "Handler",
42
+ summary: true,
43
+ enum: %w[
42
44
  gitlab github repository
43
45
  rest-api rest-api-index
44
46
  jira-discover jira-metrics
@@ -57,7 +59,8 @@ class Archsight::Resources::Import < Archsight::Resources::Base
57
59
  annotation "import/enabled",
58
60
  description: "Whether this import is enabled",
59
61
  title: "Enabled",
60
- enum: %w[true false]
62
+ enum: %w[true false],
63
+ summary: true
61
64
 
62
65
  annotation "import/priority",
63
66
  description: "Execution priority (lower runs first among ready imports)",
@@ -38,11 +38,13 @@ class Archsight::Resources::Page < Archsight::Resources::Base
38
38
  description: "Owner responsible for keeping the page up to date (Name <email@domain.com>, or just a name)",
39
39
  title: "Owner",
40
40
  type: Archsight::Annotations::Person,
41
- sidebar: false
41
+ sidebar: false,
42
+ summary: true
42
43
  annotation "page/status",
43
44
  description: "Lifecycle status (e.g. rfc, wip, approved)",
44
45
  filter: :word,
45
- title: "Status"
46
+ title: "Status",
47
+ summary: true
46
48
  annotation "page/tags",
47
49
  description: "Comma-separated tags",
48
50
  filter: :list,
@@ -74,7 +74,7 @@ class Archsight::Resources::TechnologyArtifact < Archsight::Resources::Base
74
74
  description: "Repository activity status",
75
75
  title: "Activity Status",
76
76
  enum: %w[active abandoned bot-only archived inaccessible empty no-code],
77
- list: true
77
+ summary: true
78
78
  annotation "activity/reason",
79
79
  description: "Reason for activity status (for non-standard statuses)",
80
80
  title: "Status Reason",
@@ -83,7 +83,7 @@ class Archsight::Resources::TechnologyArtifact < Archsight::Resources::Base
83
83
  description: "Bus factor assessment",
84
84
  title: "Bus Factor",
85
85
  enum: %w[high medium low unknown],
86
- list: true
86
+ summary: true
87
87
  annotation "activity/createdAt",
88
88
  description: "Date of first commit (repository creation)",
89
89
  title: "Created",
@@ -130,7 +130,8 @@ class Archsight::Resources::TechnologyArtifact < Archsight::Resources::Base
130
130
  annotation "repository/visibility",
131
131
  description: "Repository visibility classification",
132
132
  title: "Visibility",
133
- enum: %w[private internal open-source public]
133
+ enum: %w[private internal open-source public],
134
+ summary: true
134
135
  annotation "repository/recentTags",
135
136
  description: "Recent git tags (releases)",
136
137
  title: "Recent Tags",
@@ -34,7 +34,7 @@ class Archsight::Resources::TechnologyNode < Archsight::Resources::Base
34
34
  description: "Type of infrastructure node",
35
35
  title: "Infrastructure Type",
36
36
  enum: %w[vm bare-metal kubernetes-node network-appliance storage-array],
37
- list: true
37
+ summary: true
38
38
 
39
39
  relation :realizes, :businessConstraints, :BusinessConstraint
40
40
  relation :servedBy, :technologyServices, :TechnologyService
@@ -37,4 +37,8 @@ class Archsight::Resources::TechnologyService < Archsight::Resources::Base
37
37
 
38
38
  relation :suppliedBy, :technologyComponents, :TechnologySystemSoftware
39
39
  relation :servedBy, :businessActors, :BusinessActor
40
+ relation :realizes, :businessRequirements, :BusinessRequirement
41
+ relation :partiallyRealizes, :businessRequirements, :BusinessRequirement
42
+ relation :plans, :businessRequirements, :BusinessRequirement
43
+ relation :evidencedBy, :complianceEvidences, :ComplianceEvidence
40
44
  end
@@ -34,4 +34,8 @@ class Archsight::Resources::TechnologySystemSoftware < Archsight::Resources::Bas
34
34
  relation :realizedThrough, :technologyArtifacts, :TechnologyArtifact
35
35
  relation :exposes, :applicationInterfaces, :ApplicationInterface
36
36
  relation :dependsOn, :applicationInterfaces, :ApplicationInterface
37
+ relation :realizes, :businessRequirements, :BusinessRequirement
38
+ relation :partiallyRealizes, :businessRequirements, :BusinessRequirement
39
+ relation :plans, :businessRequirements, :BusinessRequirement
40
+ relation :evidencedBy, :complianceEvidences, :ComplianceEvidence
37
41
  end
@@ -42,7 +42,8 @@ class Archsight::Resources::View < Archsight::Resources::Base
42
42
  description: "Display type for results",
43
43
  title: "Display Type",
44
44
  enum: %w[list:name list:name+kind],
45
- sidebar: false
45
+ sidebar: false,
46
+ summary: true
46
47
 
47
48
  annotation "view/sort",
48
49
  description: 'Comma-separated list of fields to sort by. Prefix with - for descending (e.g., "-scc/language/Go/loc,name"). Special fields: name, kind',
@@ -4,5 +4,5 @@
4
4
  # Do not edit manually.
5
5
 
6
6
  module Archsight
7
- VERSION = "0.3.0"
7
+ VERSION = "0.3.1"
8
8
  end