archsight 0.4.0 → 0.4.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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/docs/configuration.md +1 -1
  3. data/docs/modeling.md +8 -0
  4. data/docs/pages.md +2 -2
  5. data/lib/archsight/analysis/typst_renderer.rb +2 -3
  6. data/lib/archsight/export/confluence/analysis_output.rb +3 -4
  7. data/lib/archsight/export/confluence/exporter.rb +3 -1
  8. data/lib/archsight/export/confluence/page_properties.rb +2 -3
  9. data/lib/archsight/export/confluence/storage.rb +25 -12
  10. data/lib/archsight/export/confluence/tables.rb +2 -3
  11. data/lib/archsight/git_source.rb +81 -0
  12. data/lib/archsight/helpers/asset_images.rb +3 -4
  13. data/lib/archsight/import/executor.rb +1 -0
  14. data/lib/archsight/import.rb +27 -2
  15. data/lib/archsight/linter.rb +2 -2
  16. data/lib/archsight/markdown.rb +48 -0
  17. data/lib/archsight/openapi.rb +169 -0
  18. data/lib/archsight/page_tree.rb +2 -3
  19. data/lib/archsight/resources/application_interface.rb +12 -0
  20. data/lib/archsight/resources/technology_communication_network.rb +2 -0
  21. data/lib/archsight/version.rb +1 -1
  22. data/lib/archsight/web/api/docs.rb +2 -1
  23. data/lib/archsight/web/api/openapi/spec.yaml +136 -27
  24. data/lib/archsight/web/api/routes.rb +2 -2
  25. data/lib/archsight/web/application.rb +2 -2
  26. data/lib/archsight/web/public/vendor/redoc/LICENSE +22 -0
  27. data/lib/archsight/web/public/vendor/redoc/VERSION +1 -0
  28. data/lib/archsight/web/public/vendor/redoc/api.html +27 -0
  29. data/lib/archsight/web/public/vendor/redoc/api.js +19 -0
  30. data/lib/archsight/web/public/vendor/redoc/redoc.standalone.js +1851 -0
  31. data/lib/archsight/web/public/vendor/redoc/redoc.standalone.js.LICENSE.txt +102 -0
  32. data/lib/archsight/web/public/vue/{DocPage-D52OwaKc.js → DocPage-C2ubTXH7.js} +1 -1
  33. data/lib/archsight/web/public/vue/EditorPage-Dso6PLMn.js +35 -0
  34. data/lib/archsight/web/public/vue/ErrorPage-DnZ6YJLi.js +2 -0
  35. data/lib/archsight/web/public/vue/GraphView-CTl03Yre.js +1 -0
  36. data/lib/archsight/web/public/vue/HomePage-C339nihQ.js +2 -0
  37. data/lib/archsight/web/public/vue/InstanceRouter-DFW7tOrD.js +1 -0
  38. data/lib/archsight/web/public/vue/KindList-C0oaM3CF.js +1 -0
  39. data/lib/archsight/web/public/vue/PageView-OZB6G9Wu.js +1 -0
  40. data/lib/archsight/web/public/vue/QueryError-C2xLkCEm.js +1 -0
  41. data/lib/archsight/web/public/vue/ResourceList-DooWFwQP.js +2 -0
  42. data/lib/archsight/web/public/vue/SearchResults-BK-HjFnv.js +1 -0
  43. data/lib/archsight/web/public/vue/WikiPage-B2FYmeHL.js +3 -0
  44. data/lib/archsight/web/public/vue/architecture-WOLXFQ4H-BGN-TV5r.js +1 -0
  45. data/lib/archsight/web/public/vue/cynefin-EF2NZ3EQ-CZd6g_tq.js +1 -0
  46. data/lib/archsight/web/public/vue/eventmodeling-K75KTNOO-CYnbEx8O.js +1 -0
  47. data/lib/archsight/web/public/vue/gitGraph-VSP46ZUC-DialfQSk.js +1 -0
  48. data/lib/archsight/web/public/vue/graphviz-1lWBhpEX.js +3 -0
  49. data/lib/archsight/web/public/vue/{index-eUHmJ7K2.css → index-BLSE5DSv.css} +1 -1
  50. data/lib/archsight/web/public/vue/index-CLuLTE5I.js +3 -0
  51. data/lib/archsight/web/public/vue/info-OHQRW6UA-BuhOW7ss.js +1 -0
  52. data/lib/archsight/web/public/vue/katex-B_JcIwqj.js +257 -0
  53. data/lib/archsight/web/public/vue/mermaid-C3bOamfn.js +3999 -0
  54. data/lib/archsight/web/public/vue/packet-JDAUHWVQ-CUeRz7w4.js +1 -0
  55. data/lib/archsight/web/public/vue/pie-XZMESJXO-DnuewJI6.js +1 -0
  56. data/lib/archsight/web/public/vue/radar-ABXABTNO-Bi9l8bcv.js +1 -0
  57. data/lib/archsight/web/public/vue/railroad-I3PHUGI6-DSlxFbxk.js +1 -0
  58. data/lib/archsight/web/public/vue/railroad-abnf-LNEFI6M7-D6Kqu8wu.js +1 -0
  59. data/lib/archsight/web/public/vue/railroad-ebnf-DWYD2UWJ-MR7uCYT0.js +1 -0
  60. data/lib/archsight/web/public/vue/railroad-peg-JR7BVNR7-BL7aKIc-.js +1 -0
  61. data/lib/archsight/web/public/vue/rolldown-runtime-COiEfhsA.js +1 -0
  62. data/lib/archsight/web/public/vue/treeView-D4JQ5SDB-CamtB04D.js +1 -0
  63. data/lib/archsight/web/public/vue/treemap-SAJKECNS-DeQxu_HM.js +1 -0
  64. data/lib/archsight/web/public/vue/{useGraphviz-4evusK9i.js → useGraphviz-Dnm2pkon.js} +2 -2
  65. data/lib/archsight/web/public/vue/useHighlight-BH32C7yt.js +10 -0
  66. data/lib/archsight/web/public/vue/usePanZoom-HxgEsqq2.js +12 -0
  67. data/lib/archsight/web/public/vue/wardley-7MLQ67FV-BvKIB6Tv.js +1 -0
  68. data/lib/archsight/web/public/vue.html +4 -4
  69. metadata +46 -39
  70. data/lib/archsight/web/public/vue/ApiDocsPage-COp4onwn.js +0 -1
  71. data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +0 -1
  72. data/lib/archsight/web/public/vue/EditorPage-3aqL7yVf.js +0 -35
  73. data/lib/archsight/web/public/vue/ErrorPage--b9XeSYa.js +0 -2
  74. data/lib/archsight/web/public/vue/GraphView-qh_EtRrQ.js +0 -1
  75. data/lib/archsight/web/public/vue/HomePage-C2TNh2U-.js +0 -2
  76. data/lib/archsight/web/public/vue/InstanceRouter-uzb_OMG0.js +0 -1
  77. data/lib/archsight/web/public/vue/KindList-CyJuP_uq.js +0 -1
  78. data/lib/archsight/web/public/vue/PageView-FyygrS-b.js +0 -1
  79. data/lib/archsight/web/public/vue/QueryError-B4Ex_csZ.js +0 -1
  80. data/lib/archsight/web/public/vue/ResourceList-BY6Z1Kcb.js +0 -2
  81. data/lib/archsight/web/public/vue/SearchResults-CkFpHF8m.js +0 -1
  82. data/lib/archsight/web/public/vue/WikiPage-BoHNewCX.js +0 -3
  83. data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BvzKZE4e.js +0 -1
  84. data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-D-xs_Hxh.js +0 -1
  85. data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-nbjxG01S.js +0 -1
  86. data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-CW6mw2ab.js +0 -1
  87. data/lib/archsight/web/public/vue/graphviz-SXWTgZH5.js +0 -3
  88. data/lib/archsight/web/public/vue/index-BGlYCbi7.js +0 -3
  89. data/lib/archsight/web/public/vue/info-A6RAGUB7-C12sjlHe.js +0 -1
  90. data/lib/archsight/web/public/vue/katex-CXMH3UgJ.js +0 -257
  91. data/lib/archsight/web/public/vue/mermaid-DjsXQ_XN.js +0 -3334
  92. data/lib/archsight/web/public/vue/packet-AYTQ26CC-wMwKwHmA.js +0 -1
  93. data/lib/archsight/web/public/vue/pie-WAS4IAKB-w1-sAU_K.js +0 -1
  94. data/lib/archsight/web/public/vue/radar-RG4KPBEZ-DpwqIFoy.js +0 -1
  95. data/lib/archsight/web/public/vue/railroad-74A4TZTK-Ce5XC24d.js +0 -1
  96. data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-BSQO2pds.js +0 -1
  97. data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-Dq1c2YtD.js +0 -1
  98. data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DKj6dli0.js +0 -1
  99. data/lib/archsight/web/public/vue/rolldown-runtime-hePW80VL.js +0 -1
  100. data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-BmHNTWxU.js +0 -1
  101. data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DsEI7sSD.js +0 -1
  102. data/lib/archsight/web/public/vue/useHighlight-DPguaxfm.js +0 -10
  103. data/lib/archsight/web/public/vue/usePanZoom-seQE1TPk.js +0 -12
  104. data/lib/archsight/web/public/vue/wardley-WFR3VGLG-CSCW83-M.js +0 -1
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+ require_relative "resources"
6
+
7
+ module Archsight
8
+ # The OpenAPI document of the REST API: the hand-written base (`web/api/openapi/spec.yaml`: the fixed endpoints such
9
+ # as pages, assets, search and the editor, with their prose, examples and error shapes) plus what the model knows and
10
+ # nobody should type twice: one schema per kind with its annotations (type, enum, description, default), its `spec`
11
+ # relations and its relations, the list of kinds, and the instance detail schema that is one of them. A new kind, a
12
+ # changed enum or another relation shows up in the documentation without touching the YAML.
13
+ #
14
+ # Archsight::OpenAPI.document # => Hash, `.to_yaml` is what GET /api/v1/openapi.yaml serves
15
+ module OpenAPI
16
+ BASE = File.join(__dir__, "web", "api", "openapi", "spec.yaml")
17
+
18
+ # The operations that answer with the detail of one instance, whose kind the path names
19
+ DETAIL_PATHS = ["/api/v1/kinds/{kind}/instances/{name}", "/kinds/{kind}/instances/{name}.json"].freeze
20
+
21
+ # What an annotation of a type is in the JSON. The value is the one of the YAML file, so a number may also be its
22
+ # string ("100"); only a computed annotation is always a number. Everything else is a string.
23
+ TYPES = {
24
+ Integer => { "type" => %w[integer string] },
25
+ Float => { "type" => %w[number string] }
26
+ }.freeze
27
+
28
+ NAMES = { "type" => "array", "items" => { "type" => "string" } }.freeze
29
+
30
+ class << self
31
+ # @return [Hash] the complete document; built once, the model does not change while the server runs
32
+ def document
33
+ @document ||= build
34
+ end
35
+
36
+ # Forget the built document (the model was changed, in a test)
37
+ def reset!
38
+ @document = nil
39
+ end
40
+
41
+ # The name of the schema of a kind
42
+ def schema_name(kind) = "#{kind}Instance"
43
+
44
+ # The schema of one kind: the detail of an instance as GET /api/v1/kinds/{kind}/instances/{name} returns it
45
+ # @param kind [String] name of a kind
46
+ # @return [Hash]
47
+ def kind_schema(kind)
48
+ klass = Archsight::Resources[kind]
49
+ schema = {
50
+ "type" => "object",
51
+ "required" => %w[kind name metadata spec relations references],
52
+ "x-layer" => klass.layer,
53
+ "properties" => {
54
+ "kind" => { "type" => "string", "const" => kind },
55
+ "name" => { "type" => "string", "description" => "Name of the instance" },
56
+ "metadata" => { "type" => "object", "properties" => { "annotations" => annotations_schema(klass) } },
57
+ "annotation_formats" => { "type" => "object", "additionalProperties" => { "type" => "string" },
58
+ "description" => "How the UI shows each annotation that is set (markdown, link, tag_list, ...)" },
59
+ "annotation_links" => { "type" => "object", "additionalProperties" => true,
60
+ "description" => "Annotations whose value names resources, with the resources they resolve to" },
61
+ "spec" => spec_schema(klass),
62
+ "relations" => relations_schema(klass),
63
+ "references" => references_schema,
64
+ "diagram" => { "type" => %w[string null], "description" => "The `architecture/diagram` annotation as inline SVG" }
65
+ }
66
+ }
67
+ schema["description"] = klass.description.strip if klass.description
68
+ schema
69
+ end
70
+
71
+ private
72
+
73
+ def build
74
+ document = YAML.safe_load_file(BASE, permitted_classes: [], aliases: true)
75
+ kinds = Archsight::Resources.resource_classes.keys.sort
76
+ schemas = document.fetch("components").fetch("schemas")
77
+ add_schemas(schemas, kinds)
78
+ document.dig("components", "parameters", "kind")["schema"] = { "$ref" => "#/components/schemas/KindName" }
79
+ DETAIL_PATHS.each { |path| use_detail_schema(document.fetch("paths").fetch(path)) }
80
+ # the schemas share small hashes: a copy of each keeps YAML from writing anchors and aliases for them
81
+ JSON.parse(JSON.generate(document))
82
+ end
83
+
84
+ def add_schemas(schemas, kinds)
85
+ kinds.each do |kind|
86
+ name = schema_name(kind)
87
+ raise ArgumentError, "OpenAPI: the schema #{name} of #{kind} clashes with a schema of the base spec" if schemas.key?(name)
88
+
89
+ schemas[name] = kind_schema(kind)
90
+ end
91
+ schemas["KindName"] = {
92
+ "type" => "string", "enum" => kinds,
93
+ "description" => "Name of a resource kind. A kind that was renamed is still accepted under its old name."
94
+ }
95
+ schemas["InstanceDetail"] = {
96
+ "description" => "An instance with its relations and references: the schema depends on the kind",
97
+ "oneOf" => kinds.map { |kind| { "$ref" => "#/components/schemas/#{schema_name(kind)}" } },
98
+ "discriminator" => { "propertyName" => "kind", "mapping" => kinds.to_h { |kind| [kind, "#/components/schemas/#{schema_name(kind)}"] } }
99
+ }
100
+ end
101
+
102
+ def use_detail_schema(path_item)
103
+ path_item.dig("get", "responses", "200", "content", "application/json")["schema"] = { "$ref" => "#/components/schemas/InstanceDetail" }
104
+ end
105
+
106
+ # The annotations of the kind that are set; every annotation is optional. Other annotations are allowed.
107
+ def annotations_schema(klass)
108
+ fixed, patterns = klass.annotations.partition { |annotation| !annotation.pattern? }
109
+ schema = {
110
+ "type" => "object", "additionalProperties" => true,
111
+ "properties" => fixed.to_h { |annotation| [annotation.key, annotation_schema(annotation)] }
112
+ }
113
+ schema["patternProperties"] = patterns.to_h { |annotation| [pattern_of(annotation.key), annotation_schema(annotation)] } unless patterns.empty?
114
+ schema
115
+ end
116
+
117
+ def annotation_schema(annotation)
118
+ schema = (TYPES[annotation.type] || { "type" => "string" }).dup
119
+ description = annotation.description.to_s.strip
120
+ description = "#{description}\n\nRendered to HTML in the response." if annotation.format == :markdown && !description.empty?
121
+ schema["description"] = description unless description.empty?
122
+ if annotation.enum && annotation.list?
123
+ # a list is one string of comma separated values, each of them one of the enum
124
+ schema["description"] = "#{description}\n\nComma separated list of: #{annotation.enum.join(", ")}.".strip
125
+ schema["x-items-enum"] = annotation.enum
126
+ elsif annotation.enum
127
+ schema["enum"] = annotation.enum
128
+ end
129
+ schema["default"] = annotation.default unless annotation.default.nil?
130
+ schema["deprecated"] = true if annotation.deprecated
131
+ schema
132
+ end
133
+
134
+ # `link/*` -> a regular expression for the keys it matches
135
+ def pattern_of(key) = "^#{Regexp.escape(key).gsub('\*', ".+")}$"
136
+
137
+ # What the file says: spec.<verb>.<key>: [names]
138
+ def spec_schema(klass)
139
+ properties = {}
140
+ klass.declared_relations.each do |verb, key, target|
141
+ verb_schema = (properties[verb.to_s] ||= { "type" => "object", "properties" => {} })
142
+ verb_schema["properties"][key.to_s] = NAMES.merge("description" => "Names of #{target} resources")
143
+ end
144
+ { "type" => "object", "additionalProperties" => true, "properties" => properties,
145
+ "description" => "The relations as written in the resource: verb, then the kind's collection, then the names" }
146
+ end
147
+
148
+ # The outgoing relations that exist: verb, then the kind of the target, then the names
149
+ def relations_schema(klass)
150
+ properties = {}
151
+ klass.relations.each do |verb, _key, target|
152
+ verb_schema = (properties[verb.to_s] ||= { "type" => "object" })
153
+ if Archsight::Resources::DERIVED_VERBS.include?(verb)
154
+ verb_schema["additionalProperties"] = NAMES # targets of any kind, grouped by the kind they are
155
+ else
156
+ (verb_schema["properties"] ||= {})[target.to_s] = NAMES
157
+ end
158
+ end
159
+ { "type" => "object", "additionalProperties" => false, "properties" => properties,
160
+ "description" => "Outgoing relations that have targets, grouped by verb and the kind of the target" }
161
+ end
162
+
163
+ def references_schema
164
+ { "type" => "object", "additionalProperties" => { "type" => "object", "additionalProperties" => NAMES },
165
+ "description" => "Incoming relations grouped by the kind of the source and the verb" }
166
+ end
167
+ end
168
+ end
169
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "kramdown"
4
- require "kramdown-parser-gfm"
3
+ require_relative "markdown"
5
4
 
6
5
  module Archsight
7
6
  # PageTree builds the sidebar tree, breadcrumbs and tables of contents for Page resources
@@ -63,7 +62,7 @@ module Archsight
63
62
 
64
63
  # Flat table of contents [{level:, id:, text:}] of a markdown body
65
64
  def self.toc(markdown)
66
- doc = Kramdown::Document.new(markdown.to_s, input: "GFM")
65
+ doc = Archsight::Markdown.document(markdown)
67
66
  entries = [] #: Array[Hash[String, untyped]]
68
67
  collect_toc(doc.to_toc, entries)
69
68
  entries
@@ -25,6 +25,17 @@ class Archsight::Resources::ApplicationInterface < Archsight::Resources::Base
25
25
  - gRPC services
26
26
  - Message queues (producer/consumer interfaces)
27
27
  - WebSocket endpoints
28
+
29
+ ## Reachability
30
+
31
+ An interface is `servedBy` the `TechnologyInterface` that carries it (a protocol, an authentication method) and
32
+ by the `TechnologyCommunicationNetwork`s on which it can be reached, just as a `TechnologyNode` attaches to the
33
+ networks it is on. Together with `api/authenticationMethod` this decides the exposure of the interface: an
34
+ interface without authentication on an internal network is a different risk than on a network that
35
+ internet-facing components attach to. The network page lists the interfaces as incoming "served by".
36
+
37
+ Write the networks only when they are known. An interface without a network makes no claim about where it can
38
+ be reached.
28
39
  MD
29
40
 
30
41
  icon "usb"
@@ -47,6 +58,7 @@ class Archsight::Resources::ApplicationInterface < Archsight::Resources::Base
47
58
  enum: ["none", "hard coded", "pbac", "abac", "rbac"]
48
59
 
49
60
  relation :servedBy, :technologyComponents, :TechnologyInterface
61
+ relation :servedBy, :technologyCommunicationNetworks, :TechnologyCommunicationNetwork
50
62
  relation :realizes, :motivationConstraints, :MotivationConstraint
51
63
  relation :serves, :dataObjects, :DataObject
52
64
  end
@@ -50,6 +50,8 @@ class Archsight::Resources::TechnologyCommunicationNetwork < Archsight::Resource
50
50
  - An overlay that software builds is `realizedBy` that `ApplicationComponent` or `ApplicationService` (an SDN agent,
51
51
  a network operator, a load-balancer controller), also when the software runs on the nodes the network serves
52
52
  - A `TechnologyNode` is `servedBy` the networks it is attached to; the network page lists them
53
+ - An `ApplicationInterface` is `servedBy` the networks on which it can be reached (only when that is known); the
54
+ network page lists the interfaces, which shows what a network exposes
53
55
  - A network is `realizedBy` the `TechnologyDevice`s that build it (its switches and routers) and `connects` the
54
56
  devices attached to it (servers, firewalls, appliances). Both are written on the network, so a switch that is
55
57
  also attached to the network it builds closes no cycle
@@ -4,5 +4,5 @@
4
4
  # Do not edit manually.
5
5
 
6
6
  module Archsight
7
- VERSION = "0.4.0"
7
+ VERSION = "0.4.1"
8
8
  end
@@ -11,8 +11,9 @@ module Archsight::Web::API; end
11
11
  module Archsight::Web::API::Docs
12
12
  extend Sinatra::Extension
13
13
 
14
+ # A page of its own, not the single page app: ReDoc must not get the app's CSS (Pico restyles every <code>)
14
15
  get "/docs/api" do
15
- serve_vue
16
+ send_file File.join(settings.public_folder, "vendor", "redoc", "api.html"), type: :html
16
17
  end
17
18
 
18
19
  # GET /api/v1/docs/resources/:filename — auto-generated resource docs
@@ -623,12 +623,10 @@ paths:
623
623
  type: object
624
624
  properties:
625
625
  html:
626
- type: string
627
- nullable: true
626
+ type: [string, "null"]
628
627
  description: '`<figure class="asd-diagram">` with the SVG'
629
628
  error:
630
- type: string
631
- nullable: true
629
+ type: [string, "null"]
632
630
  '400':
633
631
  description: Body is not JSON with a `source` string
634
632
  content:
@@ -708,9 +706,11 @@ paths:
708
706
  summary: Requirements of a selection of resources
709
707
  description: |
710
708
  The requirements that the resources selected by `of` realize (`implemented`), partially realize
711
- (`partial`) or plan (`planned`), merged over all of them: one entry per requirement with the best status and
712
- the resources it comes from (`by`). Sorted by priority (must, should, may, none), then name. `story` is
713
- rendered HTML.
709
+ (`partial`) or plan (`planned`), merged over all of them: one entry per requirement, with the best status
710
+ any of the resources gives it and the resources it comes from (`by`, each with its own status). Sorted by
711
+ priority (must, should, may, requirements without a priority last), then name. `story` is rendered HTML.
712
+ `priority` and `status` filter the merged entries, so `status=partial` lists requirements whose best
713
+ status is partial.
714
714
  operationId: getRequirements
715
715
  tags:
716
716
  - Search
@@ -725,14 +725,14 @@ paths:
725
725
  - name: priority
726
726
  in: query
727
727
  required: false
728
- description: Comma separated list, only requirements with one of these priorities
728
+ description: Comma separated list, only requirements with one of these priorities (must, should, may)
729
729
  schema:
730
730
  type: string
731
731
  example: must,should
732
732
  - name: status
733
733
  in: query
734
734
  required: false
735
- description: Comma separated list, only requirements with one of these statuses
735
+ description: Comma separated list, only requirements with one of these statuses (implemented, partial, planned)
736
736
  schema:
737
737
  type: string
738
738
  example: implemented,partial
@@ -743,33 +743,42 @@ paths:
743
743
  application/json:
744
744
  schema:
745
745
  type: object
746
+ required: [query, total, query_time_ms, requirements]
746
747
  properties:
747
748
  query:
748
749
  type: string
750
+ description: The `of` query as given
749
751
  total:
750
752
  type: integer
753
+ description: Number of requirements returned
751
754
  query_time_ms:
752
755
  type: number
753
756
  requirements:
754
757
  type: array
755
758
  items:
756
759
  type: object
760
+ required: [name, status, priority, story, by]
757
761
  properties:
758
762
  name:
759
763
  type: string
764
+ description: Name of the MotivationRequirement
760
765
  status:
761
766
  type: string
762
767
  enum: [implemented, partial, planned]
768
+ description: The best status among the resources in `by`
763
769
  priority:
764
- type: string
765
- nullable: true
770
+ type: [string, "null"]
771
+ enum: [must, should, may, null]
772
+ description: Priority of the requirement, null if it has none
766
773
  story:
767
- type: string
768
- nullable: true
774
+ type: [string, "null"]
775
+ description: The requirement's story as rendered HTML, null if it has none
769
776
  by:
770
777
  type: array
778
+ description: The selected resources that realize, partially realize or plan the requirement, best status first
771
779
  items:
772
780
  type: object
781
+ required: [kind, name, status]
773
782
  properties:
774
783
  kind:
775
784
  type: string
@@ -778,12 +787,118 @@ paths:
778
787
  status:
779
788
  type: string
780
789
  enum: [implemented, partial, planned]
790
+ description: The status this resource gives the requirement
791
+ example:
792
+ query: 'ApplicationService: name =~ "Backup"'
793
+ total: 1
794
+ query_time_ms: 1.4
795
+ requirements:
796
+ - name: "Requirement:Restore"
797
+ status: partial
798
+ priority: must
799
+ story: "<p>As an operator I can restore a backup.</p>"
800
+ by:
801
+ - kind: ApplicationService
802
+ name: Backup
803
+ status: partial
781
804
  '400':
782
- description: Invalid query, or an unknown priority or status
805
+ description: |
806
+ `of` is missing or `priority` or `status` holds an unknown value (`BadRequest`), or `of` is not a valid
807
+ query (`QueryError`, which also returns the query)
783
808
  content:
784
809
  application/json:
785
810
  schema:
786
- $ref: '#/components/schemas/QueryError'
811
+ oneOf:
812
+ - $ref: '#/components/schemas/Error'
813
+ - $ref: '#/components/schemas/QueryError'
814
+
815
+ /api/v1/openapi.yaml:
816
+ get:
817
+ summary: This OpenAPI document
818
+ description: |
819
+ The OpenAPI 3.1 description of this API. The fixed endpoints are written by hand; the schema of every kind
820
+ (its annotations with type, enum and description, its `spec` and its relations), the list of kinds and the
821
+ instance detail are generated from the model, so they always describe the kinds this server has.
822
+ operationId: getOpenApi
823
+ tags:
824
+ - Operations
825
+ responses:
826
+ '200':
827
+ description: The OpenAPI document
828
+ content:
829
+ text/yaml:
830
+ schema:
831
+ type: string
832
+
833
+ /maintenance/restart:
834
+ post:
835
+ summary: Restart the server
836
+ description: |
837
+ Stops the server so that its supervisor (for example a Kubernetes Deployment) starts it again with fresh
838
+ data. Only available when the server was started with `--enable-restart` (or
839
+ `ARCHSIGHT_RESTART_ENABLED=true`); otherwise it answers 404. When a restart token is configured it must be
840
+ sent in the `X-Restart-Token` header.
841
+ operationId: restart
842
+ tags:
843
+ - Operations
844
+ parameters:
845
+ - name: X-Restart-Token
846
+ in: header
847
+ required: false
848
+ description: The configured restart token (required when the server has one)
849
+ schema:
850
+ type: string
851
+ responses:
852
+ '200':
853
+ description: The server shuts down
854
+ content:
855
+ application/json:
856
+ schema:
857
+ type: object
858
+ required: [ok, message]
859
+ properties:
860
+ ok:
861
+ type: boolean
862
+ message:
863
+ type: string
864
+ example:
865
+ ok: true
866
+ message: Server shutting down
867
+ '401':
868
+ description: The restart token is missing or wrong
869
+ content:
870
+ text/html:
871
+ schema:
872
+ type: string
873
+ '404':
874
+ description: The restart endpoint is not enabled
875
+ content:
876
+ text/html:
877
+ schema:
878
+ type: string
879
+
880
+ /kinds/{kind}/dot:
881
+ get:
882
+ summary: Get the graph of a kind as DOT
883
+ description: The GraphViz DOT graph of the resources of a kind and the relations between them.
884
+ operationId: getKindDot
885
+ tags:
886
+ - Graph
887
+ parameters:
888
+ - $ref: '#/components/parameters/kind'
889
+ responses:
890
+ '200':
891
+ description: GraphViz DOT source
892
+ content:
893
+ text/plain:
894
+ schema:
895
+ type: string
896
+ '404':
897
+ description: Unknown kind
898
+ content:
899
+ text/plain:
900
+ schema:
901
+ type: string
787
902
 
788
903
  /dot:
789
904
  get:
@@ -1343,13 +1458,11 @@ components:
1343
1458
  items:
1344
1459
  type: string
1345
1460
  format:
1346
- type: string
1347
- nullable: true
1461
+ type: [string, "null"]
1348
1462
  description: Rendering hint of the annotation (tag_word, tag_list, markdown, link, ...)
1349
1463
  example: tag_word
1350
1464
  type:
1351
- type: string
1352
- nullable: true
1465
+ type: [string, "null"]
1353
1466
  description: Short name of the value type when the annotation declares one (Integer, Float, Time, Person, ...)
1354
1467
  example: Integer
1355
1468
 
@@ -1684,26 +1797,22 @@ components:
1684
1797
  type: object
1685
1798
  properties:
1686
1799
  yaml:
1687
- type: string
1688
- nullable: true
1800
+ type: [string, "null"]
1689
1801
  description: Generated source text, YAML or (for pages) markdown with frontmatter (null if validation failed)
1690
1802
  format:
1691
1803
  type: string
1692
1804
  enum: [yaml, markdown]
1693
1805
  description: Format of `yaml`. Pages (kind Page) are stored as markdown files and replaced as a whole file
1694
1806
  errors:
1695
- type: array
1696
- nullable: true
1807
+ type: [array, "null"]
1697
1808
  items:
1698
1809
  type: string
1699
1810
  description: Validation errors (null if valid)
1700
1811
  path_ref:
1701
- type: string
1702
- nullable: true
1812
+ type: [string, "null"]
1703
1813
  description: "File path reference (edit mode only, e.g., path/file.yaml:5; just the file for markdown pages)"
1704
1814
  content_hash:
1705
- type: string
1706
- nullable: true
1815
+ type: [string, "null"]
1707
1816
  description: Content hash (edit mode only, for optimistic locking)
1708
1817
 
1709
1818
  SaveRequest:
@@ -7,6 +7,7 @@ require_relative "page_helpers"
7
7
  require_relative "requirements_helpers"
8
8
  require_relative "analysis_helpers"
9
9
  require_relative "../../assets"
10
+ require_relative "../../openapi"
10
11
 
11
12
  module Archsight; end
12
13
  module Archsight::Web; end
@@ -220,8 +221,7 @@ module Archsight::Web::API::Routes
220
221
  # GET /api/v1/openapi.yaml - OpenAPI specification
221
222
  get "/api/v1/openapi.yaml" do
222
223
  content_type "text/yaml"
223
- spec_path = File.join(__dir__, "openapi", "spec.yaml")
224
- File.read(spec_path)
224
+ Archsight::OpenAPI.document.to_yaml
225
225
  end
226
226
 
227
227
  # Convenience aliases with .json suffix
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sinatra/base"
4
- require "kramdown"
4
+ require_relative "../markdown"
5
5
  require "erb"
6
6
  require "fast_mcp"
7
7
 
@@ -158,7 +158,7 @@ class Archsight::Web::Application < Sinatra::Base
158
158
  # @param base [String, nil] directory of the file the markdown comes from, relative to the resources
159
159
  # directory ("" for the root): its relative images become assets (see Archsight::Assets)
160
160
  def markdown(data, git_url: nil, base: nil, page: nil)
161
- html = Kramdown::Document.new(data, input: "GFM").to_html
161
+ html = Archsight::Markdown.document(data).to_html
162
162
  html = Archsight::Helpers::AssetImages.rewrite(html, base_dir: base, resources_dir: Archsight.resources_dir) if base
163
163
  # ```asd blocks become placeholders until the text passes below are done (see DiagramBlocks)
164
164
  html, diagrams = Archsight::Helpers::DiagramBlocks.extract(html, resolver: Archsight::Helpers::ResourceResolver.new(db))
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-present, Rebilly, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1 @@
1
+ 2.5.4
@@ -0,0 +1,27 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>API - Archsight</title>
7
+ <link rel="icon" href="/favicon.ico">
8
+ <style>
9
+ /* ReDoc brings its own styles. This page is not part of the single page app on purpose: none of the app's CSS
10
+ (Pico styles every <code>, headings, tables) reaches it. */
11
+ html, body { margin: 0; padding: 0; background: #fff; }
12
+ #back {
13
+ position: fixed; top: 12px; right: 12px; z-index: 100; padding: 6px 14px;
14
+ background: #fff; border: 1px solid #ddd; border-radius: 4px; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
15
+ font: 13px/1.4 system-ui, sans-serif; color: #0172ad; text-decoration: none;
16
+ }
17
+ #error { margin: 24px; font: 14px/1.4 system-ui, sans-serif; color: #c62828; }
18
+ </style>
19
+ </head>
20
+ <body>
21
+ <a id="back" href="/">&larr; Back</a>
22
+ <p id="error" role="alert" hidden></p>
23
+ <div id="redoc"></div>
24
+ <script src="/vendor/redoc/redoc.standalone.js"></script>
25
+ <script src="/vendor/redoc/api.js"></script>
26
+ </body>
27
+ </html>
@@ -0,0 +1,19 @@
1
+ // The API reference (/docs/api): ReDoc, vendored next to this file, renders the OpenAPI document of this server.
2
+ (function () {
3
+ var back = document.getElementById('back')
4
+ // back to where the reader came from, the start page when this page was opened directly
5
+ back.addEventListener('click', function (event) {
6
+ if (document.referrer.indexOf(location.origin) === 0 && history.length > 1) {
7
+ event.preventDefault()
8
+ history.back()
9
+ }
10
+ })
11
+
12
+ if (!window.Redoc) {
13
+ var error = document.getElementById('error')
14
+ error.textContent = 'Failed to load ReDoc'
15
+ error.hidden = false
16
+ return
17
+ }
18
+ window.Redoc.init('/api/v1/openapi.yaml', {}, document.getElementById('redoc'))
19
+ })()