archsight 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +1 -1
  3. data/README.md +7 -2
  4. data/docs/computed_annotations.md +7 -5
  5. data/docs/icons.md +3 -2
  6. data/docs/index.md.erb +5 -4
  7. data/docs/licenses.md +1 -2
  8. data/docs/modeling.md +127 -6
  9. data/docs/pages.md +72 -6
  10. data/docs/search.md +26 -2
  11. data/docs/togaf.md +4 -0
  12. data/lib/archsight/annotations/annotation.rb +5 -4
  13. data/lib/archsight/annotations/computed.rb +5 -1
  14. data/lib/archsight/annotations/relation_resolver.rb +109 -83
  15. data/lib/archsight/cli.rb +9 -2
  16. data/lib/archsight/database.rb +59 -4
  17. data/lib/archsight/diagram.rb +7 -0
  18. data/lib/archsight/documentation.rb +9 -5
  19. data/lib/archsight/editor.rb +2 -2
  20. data/lib/archsight/export/confluence/exporter.rb +11 -2
  21. data/lib/archsight/export/confluence/storage.rb +99 -8
  22. data/lib/archsight/export/confluence/tables.rb +78 -0
  23. data/lib/archsight/helpers/fenced_blocks.rb +61 -0
  24. data/lib/archsight/helpers/requirements_blocks.rb +90 -0
  25. data/lib/archsight/helpers/resource_resolver.rb +1 -1
  26. data/lib/archsight/helpers/view_blocks.rb +108 -0
  27. data/lib/archsight/helpers/wiki_links.rb +50 -5
  28. data/lib/archsight/helpers.rb +3 -0
  29. data/lib/archsight/import/handlers/go_grapher.rb +4 -1
  30. data/lib/archsight/import/handlers/go_module_parser.rb +4 -1
  31. data/lib/archsight/linter.rb +31 -3
  32. data/lib/archsight/mcp/analyze_resource_tool.rb +2 -2
  33. data/lib/archsight/mcp/base.rb +38 -0
  34. data/lib/archsight/mcp/resource_doc_tool.rb +2 -2
  35. data/lib/archsight/query/ast.rb +2 -1
  36. data/lib/archsight/query/evaluator.rb +2 -2
  37. data/lib/archsight/references.rb +129 -0
  38. data/lib/archsight/requirements.rb +70 -0
  39. data/lib/archsight/resources/analysis.rb +2 -1
  40. data/lib/archsight/resources/application_component.rb +8 -3
  41. data/lib/archsight/resources/application_interface.rb +5 -3
  42. data/lib/archsight/resources/application_service.rb +8 -7
  43. data/lib/archsight/resources/base.rb +67 -17
  44. data/lib/archsight/resources/business_actor.rb +5 -3
  45. data/lib/archsight/resources/business_control.rb +80 -0
  46. data/lib/archsight/resources/business_process.rb +3 -2
  47. data/lib/archsight/resources/business_product.rb +6 -5
  48. data/lib/archsight/resources/compliance_evidence.rb +40 -3
  49. data/lib/archsight/resources/data_object.rb +3 -2
  50. data/lib/archsight/resources/import.rb +5 -2
  51. data/lib/archsight/resources/{business_constraint.rb → motivation_constraint.rb} +4 -4
  52. data/lib/archsight/resources/motivation_goal.rb +1 -1
  53. data/lib/archsight/resources/{business_requirement.rb → motivation_requirement.rb} +19 -8
  54. data/lib/archsight/resources/motivation_stakeholder.rb +2 -2
  55. data/lib/archsight/resources/page.rb +4 -2
  56. data/lib/archsight/resources/strategy_capability.rb +2 -2
  57. data/lib/archsight/resources/technology_artifact.rb +4 -3
  58. data/lib/archsight/resources/technology_node.rb +2 -2
  59. data/lib/archsight/resources/technology_service.rb +4 -0
  60. data/lib/archsight/resources/technology_system_software.rb +4 -0
  61. data/lib/archsight/resources/view.rb +2 -1
  62. data/lib/archsight/resources.rb +34 -3
  63. data/lib/archsight/template.rb +2 -2
  64. data/lib/archsight/version.rb +1 -1
  65. data/lib/archsight/view_table.rb +102 -0
  66. data/lib/archsight/web/api/docs.rb +1 -1
  67. data/lib/archsight/web/api/json_helpers.rb +21 -15
  68. data/lib/archsight/web/api/openapi/spec.yaml +135 -2
  69. data/lib/archsight/web/api/page_helpers.rb +8 -7
  70. data/lib/archsight/web/api/requirements_helpers.rb +26 -0
  71. data/lib/archsight/web/api/routes.rb +19 -0
  72. data/lib/archsight/web/application.rb +12 -2
  73. data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +1 -0
  74. data/lib/archsight/web/public/vue/ApiDocsPage-DoOxKjG0.js +1 -0
  75. data/lib/archsight/web/public/vue/{DocPage-uaT8CdFm.js → DocPage-CfsC3CeQ.js} +1 -1
  76. data/lib/archsight/web/public/vue/EditorPage-CbG4mc9T.css +1 -0
  77. data/lib/archsight/web/public/vue/EditorPage-CsJA0q8n.js +35 -0
  78. data/lib/archsight/web/public/vue/ErrorPage-Ck9izEUS.css +1 -0
  79. data/lib/archsight/web/public/vue/ErrorPage-OJpJz9df.js +2 -0
  80. data/lib/archsight/web/public/vue/GraphView-BvWAbUAl.css +1 -0
  81. data/lib/archsight/web/public/vue/GraphView-CBq6oFRV.js +1 -0
  82. data/lib/archsight/web/public/vue/HomePage-BwgMLgVC.js +2 -0
  83. data/lib/archsight/web/public/vue/InstanceRouter-DQz-SsQq.js +1 -0
  84. data/lib/archsight/web/public/vue/InstanceRouter-jeavM6PW.css +1 -0
  85. data/lib/archsight/web/public/vue/KindList-4q0K9Cll.js +1 -0
  86. data/lib/archsight/web/public/vue/PageView-DzeGnvoS.js +1 -0
  87. data/lib/archsight/web/public/vue/QueryError-3EqjpbyV.css +1 -0
  88. data/lib/archsight/web/public/vue/QueryError-kg6Yf-pW.js +1 -0
  89. data/lib/archsight/web/public/vue/ResourceList-B0FLaFR3.css +1 -0
  90. data/lib/archsight/web/public/vue/ResourceList-Df_0e0qt.js +2 -0
  91. data/lib/archsight/web/public/vue/SearchResults-CKgY6_wH.js +1 -0
  92. data/lib/archsight/web/public/vue/SearchResults-CyPUZZSC.css +1 -0
  93. data/lib/archsight/web/public/vue/WikiPage-DoFJ0dbW.css +1 -0
  94. data/lib/archsight/web/public/vue/WikiPage-DrvCHwJ7.js +13 -0
  95. data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BfA1TPxQ.js +1 -0
  96. data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-BmgdUKVD.js +1 -0
  97. data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-DbXDvAWF.js +1 -0
  98. data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-BkVhrKS1.js +1 -0
  99. data/lib/archsight/web/public/vue/index-D0Q5GZRs.js +3 -0
  100. data/lib/archsight/web/public/vue/index-oF-iyZvk.css +1 -0
  101. data/lib/archsight/web/public/vue/info-A6RAGUB7-BIlCVuUY.js +1 -0
  102. data/lib/archsight/web/public/vue/mermaid-BjEi5URd.js +3334 -0
  103. data/lib/archsight/web/public/vue/packet-AYTQ26CC-eoQTjY3j.js +1 -0
  104. data/lib/archsight/web/public/vue/pie-WAS4IAKB-DEOotGhh.js +1 -0
  105. data/lib/archsight/web/public/vue/radar-RG4KPBEZ-C2yucuUN.js +1 -0
  106. data/lib/archsight/web/public/vue/railroad-74A4TZTK-CzONK5VY.js +1 -0
  107. data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-9zRPsDsx.js +1 -0
  108. data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-BJ4dkz9l.js +1 -0
  109. data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DFy_rqAj.js +1 -0
  110. data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-2cq5CyD2.js +1 -0
  111. data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DK-e9Ez4.js +1 -0
  112. data/lib/archsight/web/public/vue/{useGraphviz-C71SdG-N.js → useGraphviz-DweKV7Kg.js} +1 -1
  113. data/lib/archsight/web/public/vue/wardley-WFR3VGLG-BQwqUqfB.js +1 -0
  114. data/lib/archsight/web/public/vue.html +3 -3
  115. data/lib/archsight.rb +2 -0
  116. metadata +53 -42
  117. data/lib/archsight/web/public/vue/ApiDocsPage-C0y953v0.css +0 -1
  118. data/lib/archsight/web/public/vue/ApiDocsPage-C_4tAWis.js +0 -1
  119. data/lib/archsight/web/public/vue/EditorPage-C557BJC-.js +0 -35
  120. data/lib/archsight/web/public/vue/EditorPage-df5N-p21.css +0 -1
  121. data/lib/archsight/web/public/vue/ErrorPage-Vdebmife.js +0 -2
  122. data/lib/archsight/web/public/vue/ErrorPage-uMDnfY5_.css +0 -1
  123. data/lib/archsight/web/public/vue/GraphView-BduUql2N.js +0 -1
  124. data/lib/archsight/web/public/vue/GraphView-Cj2V2stN.css +0 -1
  125. data/lib/archsight/web/public/vue/HomePage-BHUTg8Ap.js +0 -2
  126. data/lib/archsight/web/public/vue/InstanceRouter-1lSigA58.js +0 -1
  127. data/lib/archsight/web/public/vue/InstanceRouter-Di7f3Rya.css +0 -1
  128. data/lib/archsight/web/public/vue/KindList-BDZs0j6d.js +0 -1
  129. data/lib/archsight/web/public/vue/PageView-BYzJDwof.js +0 -1
  130. data/lib/archsight/web/public/vue/ResourceList-CnbhU9wm.js +0 -1
  131. data/lib/archsight/web/public/vue/ResourceList-xyBwu7fh.css +0 -1
  132. data/lib/archsight/web/public/vue/SearchResults-CNhf6VOx.js +0 -1
  133. data/lib/archsight/web/public/vue/SearchResults-DOHzqAy3.css +0 -1
  134. data/lib/archsight/web/public/vue/WikiPage-DbmWkM7W.js +0 -13
  135. data/lib/archsight/web/public/vue/WikiPage-GV67QmNJ.css +0 -1
  136. data/lib/archsight/web/public/vue/architecture-TIHT7OUA-Bf-MWFmn.js +0 -1
  137. data/lib/archsight/web/public/vue/cynefin-VYW2F7L2-CkKt8qqs.js +0 -1
  138. data/lib/archsight/web/public/vue/eventmodeling-45OFAUF4-NwSTYPHj.js +0 -1
  139. data/lib/archsight/web/public/vue/gitGraph-TEB2WS4Q-DLRegrRG.js +0 -1
  140. data/lib/archsight/web/public/vue/index-CyVWObLU.js +0 -3
  141. data/lib/archsight/web/public/vue/index-DtKeHT3S.css +0 -1
  142. data/lib/archsight/web/public/vue/info-DKCQHKI2-EV39NzoN.js +0 -1
  143. data/lib/archsight/web/public/vue/mermaid-BMkGfnhm.js +0 -3279
  144. data/lib/archsight/web/public/vue/packet-7NZHBO7P-USljV3MZ.js +0 -1
  145. data/lib/archsight/web/public/vue/pie-RZYD4A2V-DChciBW2.js +0 -1
  146. data/lib/archsight/web/public/vue/radar-I7S5WNFK-CgGJdW1t.js +0 -1
  147. data/lib/archsight/web/public/vue/railroad-3IZDKUUU-Dy43vF5c.js +0 -1
  148. data/lib/archsight/web/public/vue/railroad-abnf-AHOZXSZD-BhsTH-sE.js +0 -1
  149. data/lib/archsight/web/public/vue/railroad-ebnf-EBAXGLYW-BBTd_u0u.js +0 -1
  150. data/lib/archsight/web/public/vue/railroad-peg-LSFZ7HO6-BiNTEzf0.js +0 -1
  151. data/lib/archsight/web/public/vue/treeView-QDETBFTQ-BfnCcf-q.js +0 -1
  152. data/lib/archsight/web/public/vue/treemap-6X3UGDF4-BsAORhvk.js +0 -1
  153. data/lib/archsight/web/public/vue/wardley-OPB4EBWU-DQvnEhRj.js +0 -1
@@ -18,7 +18,7 @@ module Archsight::Web::API::Docs
18
18
  # GET /api/v1/docs/resources/:filename — auto-generated resource docs
19
19
  get "/api/v1/docs/resources/:filename" do
20
20
  filename = params["filename"].gsub(/[^a-zA-Z0-9_-]/, "")
21
- kind_name = filename.split("_").map(&:capitalize).join
21
+ kind_name = Archsight::Resources.canonical(filename.split("_").map(&:capitalize).join) # an old help link shows the renamed kind
22
22
 
23
23
  begin
24
24
  content = Archsight::Documentation.generate(kind_name)
@@ -74,7 +74,7 @@ module Archsight::Web::API::JsonHelpers
74
74
 
75
75
  def build_list_response(kind, pagination, instances)
76
76
  {
77
- kind: kind,
77
+ kind: Archsight::Resources.canonical(kind),
78
78
  total: pagination[:total],
79
79
  limit: pagination[:limit],
80
80
  offset: pagination[:offset],
@@ -88,9 +88,10 @@ module Archsight::Web::API::JsonHelpers
88
88
 
89
89
  def build_instance_response(kind, instance)
90
90
  {
91
- kind: kind,
91
+ kind: Archsight::Resources.canonical(kind),
92
92
  name: instance.name,
93
- metadata: { annotations: render_annotations(instance.annotations, base: Archsight::Assets.base_dir_for(instance, resources_dir: Archsight.resources_dir)) },
93
+ metadata: { annotations: render_annotations(instance.annotations, base: Archsight::Assets.base_dir_for(instance, resources_dir: Archsight.resources_dir), kind: kind) },
94
+ annotation_formats: Archsight::Resources[kind]&.annotation_formats(instance.annotations.keys) || {},
94
95
  spec: serialize_spec(instance.spec),
95
96
  relations: extract_relations(instance),
96
97
  references: extract_references(instance),
@@ -106,10 +107,14 @@ module Archsight::Web::API::JsonHelpers
106
107
  Archsight::Helpers::DiagramBlocks.render_diagram(source, resolver: Archsight::Helpers::ResourceResolver.new(db))
107
108
  end
108
109
 
110
+ # The values of markdown annotations are rendered to HTML: architecture/description and every annotation the
111
+ # kind defines with `format: :markdown`.
109
112
  # @param base [String, nil] directory of the file the resource comes from, for the images of its markdown
110
- def render_annotations(annotations, base: nil)
113
+ # @param kind [String, nil] the kind of the resource, to find which of its annotations are markdown
114
+ def render_annotations(annotations, base: nil, kind: nil)
115
+ klass = kind && Archsight::Resources[kind]
111
116
  annotations.each_with_object({}) do |(key, value), result|
112
- result[key] = if MARKDOWN_ANNOTATION_KEYS.include?(key) && value.is_a?(String)
117
+ result[key] = if value.is_a?(String) && (MARKDOWN_ANNOTATION_KEYS.include?(key) || klass&.annotation_format(key) == :markdown)
113
118
  markdown(value, base: base)
114
119
  else
115
120
  value
@@ -130,13 +135,11 @@ module Archsight::Web::API::JsonHelpers
130
135
  end
131
136
 
132
137
  def build_count_response(query, results, query_time_ms)
133
- by_kind = results.group_by { |r| r.class.to_s.split("::").last }
134
- .transform_values(&:length)
135
138
  {
136
139
  query: query,
137
140
  total: results.length,
138
141
  query_time_ms: query_time_ms,
139
- by_kind: by_kind
142
+ by_kind: Archsight::MCP.count_by_kind(results)
140
143
  }
141
144
  end
142
145
 
@@ -144,10 +147,13 @@ module Archsight::Web::API::JsonHelpers
144
147
  limit, offset = parse_pagination_params
145
148
  output = parse_output_param
146
149
  sorted = results.sort_by(&:name)
147
- pagination = paginate(sorted, limit: limit, offset: offset)
150
+ # `kind` narrows the hits to one kind; by_kind always counts all hits so the facets stay complete
151
+ kind = params[:kind].to_s
152
+ visible = kind.empty? ? sorted : sorted.select { |r| Archsight::MCP.kind_of(r) == kind }
153
+ pagination = paginate(visible, limit: limit, offset: offset)
148
154
 
149
155
  instances = pagination[:items].map do |r|
150
- resource_summary(r, output: output, omit_kind: false)
156
+ resource_summary(r, output: output, omit_kind: false).merge(highlights: Archsight::MCP.highlights(r))
151
157
  end
152
158
 
153
159
  {
@@ -157,6 +163,7 @@ module Archsight::Web::API::JsonHelpers
157
163
  offset: pagination[:offset],
158
164
  count: instances.length,
159
165
  query_time_ms: query_time_ms,
166
+ by_kind: Archsight::MCP.count_by_kind(results),
160
167
  instances: instances
161
168
  }
162
169
  end
@@ -165,11 +172,10 @@ module Archsight::Web::API::JsonHelpers
165
172
  relations = {}
166
173
 
167
174
  instance.class.relations.each do |verb, spec_key, kind|
168
- rels = instance.relations(verb, spec_key).map(&:name)
169
- next if rels.empty?
170
-
171
- relations[verb] ||= {}
172
- relations[verb][kind] = rels
175
+ found = instance.relations(verb, spec_key)
176
+ # derived relations hold resources of any kind: grouped by the kind each one is
177
+ groups = Archsight::Resources::DERIVED_VERBS.include?(verb) ? found.group_by(&:klass) : { kind => found }
178
+ groups.each { |target_kind, rels| (relations[verb] ||= {})[target_kind] = rels.map(&:name) unless rels.empty? }
173
179
  end
174
180
 
175
181
  relations
@@ -650,7 +650,12 @@ paths:
650
650
  See the API description for full query language documentation.
651
651
 
652
652
  Response includes: kind, name, metadata, spec (no relations/references).
653
- Additional fields: query, query_time_ms, by_kind (count mode).
653
+ Additional fields: query, query_time_ms, by_kind.
654
+
655
+ Every hit carries `highlights`: the values of the kind's summary attributes (annotations marked
656
+ `summary: true` in the kind definition, at most 3 per kind, computed ones included) that are set.
657
+ `by_kind` counts all hits of the query per kind, also when `kind` narrows the list, so a client can show
658
+ facets and let the user pick one.
654
659
  operationId: search
655
660
  tags:
656
661
  - Search
@@ -674,6 +679,13 @@ paths:
674
679
  orphan:
675
680
  value: '<- none'
676
681
  summary: Find orphan resources
682
+ - name: kind
683
+ in: query
684
+ required: false
685
+ description: Only return hits of this kind (`total` and paging follow; `by_kind` still counts all hits)
686
+ schema:
687
+ type: string
688
+ example: TechnologyArtifact
677
689
  - $ref: '#/components/parameters/limit'
678
690
  - $ref: '#/components/parameters/offset'
679
691
  - $ref: '#/components/parameters/output'
@@ -691,6 +703,88 @@ paths:
691
703
  schema:
692
704
  $ref: '#/components/schemas/QueryError'
693
705
 
706
+ /api/v1/requirements:
707
+ get:
708
+ summary: Requirements of a selection of resources
709
+ description: |
710
+ 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.
714
+ operationId: getRequirements
715
+ tags:
716
+ - Search
717
+ parameters:
718
+ - name: of
719
+ in: query
720
+ required: true
721
+ description: Query selecting the resources whose requirements are listed
722
+ schema:
723
+ type: string
724
+ example: 'ApplicationService: name =~ "Backup"'
725
+ - name: priority
726
+ in: query
727
+ required: false
728
+ description: Comma separated list, only requirements with one of these priorities
729
+ schema:
730
+ type: string
731
+ example: must,should
732
+ - name: status
733
+ in: query
734
+ required: false
735
+ description: Comma separated list, only requirements with one of these statuses
736
+ schema:
737
+ type: string
738
+ example: implemented,partial
739
+ responses:
740
+ '200':
741
+ description: The requirements
742
+ content:
743
+ application/json:
744
+ schema:
745
+ type: object
746
+ properties:
747
+ query:
748
+ type: string
749
+ total:
750
+ type: integer
751
+ query_time_ms:
752
+ type: number
753
+ requirements:
754
+ type: array
755
+ items:
756
+ type: object
757
+ properties:
758
+ name:
759
+ type: string
760
+ status:
761
+ type: string
762
+ enum: [implemented, partial, planned]
763
+ priority:
764
+ type: string
765
+ nullable: true
766
+ story:
767
+ type: string
768
+ nullable: true
769
+ by:
770
+ type: array
771
+ items:
772
+ type: object
773
+ properties:
774
+ kind:
775
+ type: string
776
+ name:
777
+ type: string
778
+ status:
779
+ type: string
780
+ enum: [implemented, partial, planned]
781
+ '400':
782
+ description: Invalid query, or an unknown priority or status
783
+ content:
784
+ application/json:
785
+ schema:
786
+ $ref: '#/components/schemas/QueryError'
787
+
694
788
  /dot:
695
789
  get:
696
790
  summary: Get full architecture graph as DOT
@@ -1225,6 +1319,40 @@ components:
1225
1319
  items:
1226
1320
  $ref: '#/components/schemas/Kind'
1227
1321
 
1322
+ Highlight:
1323
+ type: object
1324
+ required:
1325
+ - key
1326
+ - title
1327
+ - value
1328
+ properties:
1329
+ key:
1330
+ type: string
1331
+ description: Annotation key, exactly as the query language takes it
1332
+ example: activity/status
1333
+ title:
1334
+ type: string
1335
+ description: Display title of the annotation
1336
+ example: Activity Status
1337
+ value:
1338
+ description: The value; an array for list annotations, a number for Integer/Float typed ones
1339
+ oneOf:
1340
+ - type: string
1341
+ - type: number
1342
+ - type: array
1343
+ items:
1344
+ type: string
1345
+ format:
1346
+ type: string
1347
+ nullable: true
1348
+ description: Rendering hint of the annotation (tag_word, tag_list, markdown, link, ...)
1349
+ example: tag_word
1350
+ type:
1351
+ type: string
1352
+ nullable: true
1353
+ description: Short name of the value type when the annotation declares one (Integer, Float, Time, Person, ...)
1354
+ example: Integer
1355
+
1228
1356
  Instance:
1229
1357
  type: object
1230
1358
  required:
@@ -1247,6 +1375,11 @@ components:
1247
1375
  type: object
1248
1376
  additionalProperties: true
1249
1377
  description: Instance specification
1378
+ highlights:
1379
+ type: array
1380
+ description: Values of the kind's summary attributes that are set (search results only)
1381
+ items:
1382
+ $ref: '#/components/schemas/Highlight'
1250
1383
  relations:
1251
1384
  type: object
1252
1385
  additionalProperties:
@@ -1307,7 +1440,7 @@ components:
1307
1440
  type: object
1308
1441
  additionalProperties:
1309
1442
  type: integer
1310
- description: Result counts by kind (count mode only)
1443
+ description: Result counts by kind (search and count mode; all hits of the query, not only the current page)
1311
1444
  instances:
1312
1445
  type: array
1313
1446
  items:
@@ -67,13 +67,14 @@ module Archsight::Web::API::PageHelpers
67
67
  rest.to_s.lstrip
68
68
  end
69
69
 
70
- # Other pages linking to this page with [[name]], [[title]] (optionally with |label)
70
+ # The other pages that link to this page with [[name]] or [[title]] (optionally with |label): the pages that mention it
71
+ # (see Archsight::References), so a link in code does not count and a page is not listed twice.
71
72
  def page_backlinks(page)
72
- targets = [page.name, page.title].map { |t| Regexp.escape(t.to_s) }.uniq.join("|")
73
- pattern = /\[\[\s*(?:#{targets})\s*(?:\||\]\])/i
74
- db.instances_by_kind("Page").values
75
- .select { |other| other != page && other.annotations["page/content"].to_s.match?(pattern) }
76
- .sort_by(&:name)
77
- .map { |other| { name: other.name, title: other.title } }
73
+ page.references
74
+ .select { |ref| ref[:verb] == :mentions && ref[:instance].klass == "Page" && !ref[:instance].equal?(page) }
75
+ .map { |ref| ref[:instance] }
76
+ .uniq
77
+ .sort_by(&:name)
78
+ .map { |other| { name: other.name, title: other.title } }
78
79
  end
79
80
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Archsight; end
4
+ module Archsight::Web; end
5
+ module Archsight::Web::API; end
6
+
7
+ # Parameters and JSON of `GET /api/v1/requirements` (see Archsight::Requirements)
8
+ module Archsight::Web::API::RequirementsHelpers
9
+ # @param name [Symbol] a comma separated list parameter, every value of which must be one of `allowed`
10
+ def csv_param(name, allowed)
11
+ values = params[name].to_s.split(",").map(&:strip).reject(&:empty?)
12
+ invalid = values - allowed
13
+ json_error("Parameter '#{name}' must be #{allowed.join(", ")}, not '#{invalid.first}'", status: 400, error_type: "BadRequest") unless invalid.empty?
14
+
15
+ values
16
+ end
17
+
18
+ def build_requirements_response(query, requirements, query_time_ms)
19
+ {
20
+ query: query,
21
+ total: requirements.length,
22
+ query_time_ms: query_time_ms,
23
+ requirements: requirements.map { |r| r.merge(story: r[:story] && markdown(r[:story])) }
24
+ }
25
+ end
26
+ end
@@ -4,6 +4,7 @@ require "sinatra/base"
4
4
  require "sinatra/extension"
5
5
  require_relative "json_helpers"
6
6
  require_relative "page_helpers"
7
+ require_relative "requirements_helpers"
7
8
  require_relative "../../assets"
8
9
 
9
10
  module Archsight; end
@@ -16,6 +17,7 @@ module Archsight::Web::API::Routes
16
17
 
17
18
  helpers Archsight::Web::API::JsonHelpers
18
19
  helpers Archsight::Web::API::PageHelpers
20
+ helpers Archsight::Web::API::RequirementsHelpers
19
21
 
20
22
  # Rendering is CPU work, previews are typed, not pasted
21
23
  MAX_DIAGRAM_SOURCE = 100_000
@@ -110,6 +112,23 @@ module Archsight::Web::API::Routes
110
112
  end
111
113
  end
112
114
 
115
+ # GET /api/v1/requirements - The requirements of the resources a query selects (see Archsight::Requirements)
116
+ get "/api/v1/requirements" do
117
+ query = params[:of]
118
+ json_error("Query parameter 'of' is required", status: 400, error_type: "BadRequest") unless query
119
+
120
+ priority = csv_param(:priority, Archsight::Requirements::PRIORITIES)
121
+ status = csv_param(:status, Archsight::Requirements::STATUSES.values)
122
+ start_time = Time.now
123
+
124
+ begin
125
+ requirements = Archsight::Requirements.collect(db, of: query, priority: priority, status: status)
126
+ json_response(build_requirements_response(query, requirements, ((Time.now - start_time) * 1000).round(2)))
127
+ rescue Archsight::Query::QueryError => e
128
+ json_error(e.message, status: 400, error_type: "QueryError", query: query)
129
+ end
130
+ end
131
+
113
132
  # GET /api/v1/assets/*path - Images and draw.io diagrams that markdown embeds. The path is relative to the
114
133
  # resources directory; only files of the allowed types inside it are ever served, never anything outside of
115
134
  # it and never resource definitions (see Archsight::Assets). Outside, missing and not-served-type all
@@ -87,9 +87,14 @@ class Archsight::Web::Application < Sinatra::Base
87
87
  mcp_server.register_tool(Archsight::MCP::ResourceDocTool)
88
88
  mcp_server.register_tool(Archsight::MCP::ExecuteAnalysisTool)
89
89
 
90
+ # The server is meant to be deployed (behind an ingress under its own hostname), and the web UI and API do not
91
+ # restrict who may reach them either. FastMcp's default would only accept the Origin / Host `localhost`
92
+ # (DNS-rebinding protection) and answer every other hostname with 403, so the check is switched off: an empty
93
+ # list of allowed origins disables it.
90
94
  use FastMcp::Transports::RackTransport, mcp_server,
91
95
  path_prefix: "/mcp",
92
- localhost_only: false
96
+ localhost_only: false,
97
+ allowed_origins: []
93
98
  end
94
99
 
95
100
  helpers Archsight::GraphvisRenderer, Archsight::Helpers
@@ -140,6 +145,9 @@ class Archsight::Web::Application < Sinatra::Base
140
145
  html = Archsight::Helpers::AssetImages.rewrite(html, base_dir: base, resources_dir: Archsight.resources_dir) if base
141
146
  # ```asd blocks become placeholders until the text passes below are done (see DiagramBlocks)
142
147
  html, diagrams = Archsight::Helpers::DiagramBlocks.extract(html, resolver: Archsight::Helpers::ResourceResolver.new(db))
148
+ # so are ```view and ```requirements blocks (see ViewBlocks, RequirementsBlocks)
149
+ html, views = Archsight::Helpers::ViewBlocks.extract(html)
150
+ html, requirements = Archsight::Helpers::RequirementsBlocks.extract(html)
143
151
 
144
152
  # Resolve relative URLs if we have a git URL (for repository READMEs)
145
153
  if git_url && (base_url = github_raw_base_url(git_url))
@@ -157,7 +165,9 @@ class Archsight::Web::Application < Sinatra::Base
157
165
  # Convert [[Target]] / [[Target|label]] wiki-style links to page and resource links
158
166
  html = Archsight::Helpers::Embeds.new(db).render(html)
159
167
  html = Archsight::Helpers::WikiLinks.new(db).render(Archsight::Helpers::Macros.render(html, context: Archsight::Helpers::Macros::Context.new(db, page)))
160
- Archsight::Helpers::DiagramBlocks.restore(html, diagrams)
168
+ html = Archsight::Helpers::DiagramBlocks.restore(html, diagrams)
169
+ html = Archsight::Helpers::ViewBlocks.restore(html, views)
170
+ Archsight::Helpers::RequirementsBlocks.restore(html, requirements)
161
171
  end
162
172
 
163
173
  # Generate asset path with cache-busting query string based on file mtime
@@ -0,0 +1 @@
1
+ .back-link[data-v-4823b89b]{z-index:100;font-size:var(--fs-sm);background:#fff;border:1px solid #ddd;border-radius:4px;padding:6px 14px;text-decoration:none;position:fixed;top:12px;right:12px;box-shadow:0 1px 4px #0000001a}
@@ -0,0 +1 @@
1
+ import{$ as e,B as t,H as n,M as r,T as i,X as a,Y as o,j as s,k as c,t as l,x as u}from"./index-D0Q5GZRs.js";var d={class:`api-docs-page`},f={key:0,class:`pico-color-red-500`},p=l({__name:`ApiDocsPage`,setup(l){let p=u(),m=o(null),h=o(null);function g(e){return new Promise((t,n)=>{if(window.Redoc)return t();let r=document.createElement(`script`);r.src=e,r.onload=t,r.onerror=()=>n(Error(`Failed to load ReDoc`)),document.head.appendChild(r)})}return t(async()=>{try{await g(`https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js`),window.Redoc.init(`/api/v1/openapi.yaml`,{},m.value)}catch(e){h.value=e.message}}),(t,o)=>(n(),r(`div`,d,[c(`a`,{href:`#`,class:`back-link`,onClick:o[0]||=i(e=>a(p).back(),[`prevent`])},`← Back`),h.value?(n(),r(`div`,f,e(h.value),1)):s(``,!0),c(`div`,{ref_key:`container`,ref:m},null,512)]))}},[[`__scopeId`,`data-v-4823b89b`]]);export{p as default};
@@ -1 +1 @@
1
- import{G as e,J as t,O as n,V as r,j as i,k as a,s as o}from"./index-CyVWObLU.js";import{t as s}from"./WikiPage-DbmWkM7W.js";var c={key:1},l={__name:`DocPage`,props:{filename:String},setup(l){let u=l,d=t(null);function f(e,t){let n=new DOMParser().parseFromString(e,`text/html`).body,r=n.querySelector(`:scope > article`)||n,i=r.querySelector(`h1`),a=i?.textContent.trim()||t.split(`/`).pop().replace(/[-_]/g,` `);i?.remove();let o=[...r.querySelectorAll(`h2[id], h3[id], h4[id]`)].map(e=>({level:Number(e.tagName[1]),id:e.id,text:e.textContent.trim()})),s=[{title:`Documentation`}];return t.startsWith(`resources/`)&&s.push({title:`Resource types`}),{title:a,html:r.innerHTML,toc:o.length>=2?o:[],breadcrumb:s}}async function p(){let e=await o(u.filename);d.value=e?f(e,u.filename):{title:`Not found`,html:`<p>Documentation not found.</p>`,toc:[],breadcrumb:[{title:`Documentation`}]},document.title=`${d.value.title} - Archsight`}return e(()=>u.filename,p,{immediate:!0}),(e,t)=>d.value?(r(),a(s,{key:0,title:d.value.title,html:d.value.html,breadcrumb:d.value.breadcrumb,toc:d.value.toc},null,8,[`title`,`html`,`breadcrumb`,`toc`])):(r(),i(`article`,c,[...t[0]||=[n(`p`,null,`Loading...`,-1)]]))}};export{l as default};
1
+ import{A as e,H as t,K as n,M as r,Y as i,k as a,s as o}from"./index-D0Q5GZRs.js";import{t as s}from"./WikiPage-DrvCHwJ7.js";var c={key:1},l={__name:`DocPage`,props:{filename:String},setup(l){let u=l,d=i(null);function f(e,t){let n=new DOMParser().parseFromString(e,`text/html`).body,r=n.querySelector(`:scope > article`)||n,i=r.querySelector(`h1`),a=i?.textContent.trim()||t.split(`/`).pop().replace(/[-_]/g,` `);i?.remove();let o=[...r.querySelectorAll(`h2[id], h3[id], h4[id]`)].map(e=>({level:Number(e.tagName[1]),id:e.id,text:e.textContent.trim()})),s=[{title:`Documentation`}];return t.startsWith(`resources/`)&&s.push({title:`Resource types`}),{title:a,html:r.innerHTML,toc:o.length>=2?o:[],breadcrumb:s}}async function p(){let e=await o(u.filename);d.value=e?f(e,u.filename):{title:`Not found`,html:`<p>Documentation not found.</p>`,toc:[],breadcrumb:[{title:`Documentation`}]},document.title=`${d.value.title} - Archsight`}return n(()=>u.filename,p,{immediate:!0}),(n,i)=>d.value?(t(),e(s,{key:0,title:d.value.title,html:d.value.html,breadcrumb:d.value.breadcrumb,toc:d.value.toc},null,8,[`title`,`html`,`breadcrumb`,`toc`])):(t(),r(`article`,c,[...i[0]||=[a(`p`,null,`Loading...`,-1)]]))}};export{l as default};
@@ -0,0 +1 @@
1
+ .diagram-preview[data-v-11568657]{flex-direction:column;gap:.5rem;min-height:0;display:flex}.preview-head[data-v-11568657]{color:var(--pico-muted-color);font-size:var(--fs-sm);text-transform:uppercase;letter-spacing:.04em;justify-content:space-between;align-items:center;display:flex}.preview-tools[data-v-11568657]{align-items:center;gap:.75rem;display:inline-flex}.preview-state[data-v-11568657]{text-transform:none;letter-spacing:0}.preview-layout[data-v-11568657]{gap:.15rem;display:inline-flex}.preview-layout button[data-v-11568657]{width:1.4rem;height:1.4rem;font-size:var(--fs-xs);color:var(--pico-muted-color);border-radius:var(--pico-border-radius);cursor:pointer;background:0 0;border:1px solid #0000;justify-content:center;align-items:center;margin:0;padding:0;display:inline-flex}.preview-layout button[data-v-11568657]:hover,.preview-layout button[data-v-11568657]:focus-visible{color:var(--pico-primary);border-color:var(--pico-muted-border-color)}.preview-layout button.active[data-v-11568657]{color:var(--pico-primary);border-color:var(--pico-primary)}.preview-error[data-v-11568657]{border-left:4px solid var(--pico-del-color);background:var(--pico-mark-background-color);font-size:var(--fs-sm);overflow-wrap:anywhere;padding:.35rem .6rem}.preview-body[data-v-11568657]{min-height:0;overflow:auto}.preview-body.stale[data-v-11568657]{opacity:.45}.preview-body[data-v-11568657] .asd-diagram{margin:0}.preview-empty[data-v-11568657]{color:var(--pico-muted-color);font-size:var(--fs-xs);margin:0}.field-group[data-v-fe1221de]{margin-bottom:0}.field-group label[data-v-fe1221de]{margin-bottom:.25rem}.field-group label .required[data-v-fe1221de]{color:var(--pico-del-color);margin-left:.15rem}.field-group input[data-v-fe1221de],.field-group select[data-v-fe1221de],.field-group textarea[data-v-fe1221de]{margin-bottom:0}.field-description[data-v-fe1221de]{font-size:var(--fs-xs);color:var(--pico-muted-color);margin-top:.25rem}.field-error[data-v-fe1221de]{color:var(--pico-del-color);font-size:var(--fs-xs);margin-top:.25rem}.field-full-width[data-v-fe1221de]{grid-column:1/-1}.markdown-field textarea[data-v-fe1221de]{font-family:var(--font-mono);font-size:var(--fs-sm);resize:vertical;tab-size:2;min-height:24rem;line-height:1.5}.markdown-field.page-body textarea[data-v-fe1221de]{min-height:65vh}.code-field textarea[data-v-fe1221de]{font-family:var(--font-mono);font-size:var(--fs-sm);resize:vertical;tab-size:2;white-space:pre;overflow-wrap:normal;min-height:600px;line-height:1.5;overflow-x:auto}.field-diagram-preview[data-v-fe1221de]{margin-top:.75rem}.code-field.with-preview.layout-right[data-v-fe1221de]{grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:start;gap:1.5rem;display:grid}.code-field.with-preview.layout-right .field-diagram-preview[data-v-fe1221de]{max-height:calc(100vh - 2rem);margin-top:0;position:sticky;top:1rem}.code-field.with-preview.layout-full .code-source[data-v-fe1221de]{display:none}.code-field.with-preview.layout-full .field-diagram-preview[data-v-fe1221de]{margin-top:0}.future-hint[data-v-fe1221de]{color:var(--pico-muted-color);font-size:var(--fs-xs);align-items:center;gap:.35rem;margin-top:.5rem;display:flex}.label-row[data-v-fe1221de]{justify-content:space-between;align-items:center;margin-bottom:.25rem;display:flex}.label-row label[data-v-fe1221de]{margin-bottom:0}.btn-edit-markdown[data-v-fe1221de]{font-size:var(--fs-xs);color:var(--pico-muted-color);border:1px solid var(--pico-muted-border-color);border-radius:var(--pico-border-radius);cursor:pointer;background:0 0;align-items:center;gap:.35rem;padding:.25rem .5rem;transition:all .15s;display:inline-flex}.btn-edit-markdown[data-v-fe1221de]:hover{color:var(--pico-primary);border-color:var(--pico-primary);background-color:#0000}.btn-edit-markdown i[data-v-fe1221de]{font-size:var(--fs-sm)}@media (prefers-color-scheme:dark){.btn-edit-markdown[data-v-fe1221de]{color:#8b949e;border-color:#ffffff26}.btn-edit-markdown[data-v-fe1221de]:hover{color:#58a6ff;border-color:#58a6ff}}.relations-editor[data-v-a987a6fe]{flex-direction:column;gap:.75rem;display:flex}#relations-list[data-v-a987a6fe]{background-color:var(--pico-background-color);border:1px solid var(--pico-muted-border-color);border-radius:4px;flex-direction:column;display:flex}#relations-list[data-v-a987a6fe]:empty{display:none}.relation-item[data-v-a987a6fe]{font-size:var(--fs-sm);border-bottom:1px solid var(--pico-muted-border-color);justify-content:space-between;align-items:center;gap:.5rem;padding:.35rem .5rem;display:flex}.relation-item[data-v-a987a6fe]:last-child{border-bottom:none}.relation-text[data-v-a987a6fe]{white-space:nowrap;text-overflow:ellipsis;flex:1;overflow:hidden}.relation-text strong[data-v-a987a6fe]{font-weight:600}.relation-text em[data-v-a987a6fe]{color:var(--pico-muted-color);font-style:normal}.btn-remove[data-v-a987a6fe]{width:1.5rem;height:1.5rem;color:var(--pico-muted-color);cursor:pointer;background:0 0;border:none;border-radius:3px;flex-shrink:0;justify-content:center;align-items:center;margin:0;padding:0;display:inline-flex}.btn-remove[data-v-a987a6fe]:hover{background-color:var(--pico-del-color);color:#fff}.btn-remove i[data-v-a987a6fe]{font-size:var(--fs-xs)}.add-relation-row[data-v-a987a6fe]{align-items:center;gap:.5rem;display:flex}.add-relation-row select[data-v-a987a6fe]{flex:1;margin:0}.add-relation-row select[data-v-a987a6fe]:first-child{flex:1.2}.btn-add[data-v-a987a6fe]{white-space:nowrap;flex-shrink:0;margin:0}.no-relations[data-v-a987a6fe]{color:var(--pico-muted-color);margin:0;font-style:italic}@media (width<=768px){.add-relation-row[data-v-a987a6fe]{flex-direction:column}.add-relation-row select[data-v-a987a6fe],.add-relation-row button[data-v-a987a6fe]{width:100%}}.yaml-output-container[data-v-b5ea938f]{width:100%}.yaml-success[data-v-b5ea938f]{color:var(--pico-ins-color);align-items:center;gap:.5rem;margin:0 0 1rem;font-weight:500;display:flex}#yaml-content[data-v-b5ea938f]{margin:0 calc(var(--pico-block-spacing-horizontal) * -1);padding:var(--pico-block-spacing-horizontal);background-color:var(--pico-code-background-color);border-radius:0;overflow-x:auto}#yaml-content code[data-v-b5ea938f]{font-size:var(--fs-sm);white-space:pre;padding:0;line-height:1.5}footer[data-v-b5ea938f]{justify-content:space-between;align-items:center;gap:1rem;display:flex}.yaml-file-info[data-v-b5ea938f],.yaml-instructions[data-v-b5ea938f]{font-size:var(--fs-sm);color:var(--pico-muted-color);flex:1;gap:.5rem;min-width:0;margin:0;line-height:1.5;display:flex}.yaml-file-info[data-v-b5ea938f]{align-items:center}.yaml-instructions[data-v-b5ea938f]{align-items:flex-start}.yaml-file-info>span[data-v-b5ea938f],.yaml-instructions>span[data-v-b5ea938f]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.yaml-file-info>i[data-v-b5ea938f],.yaml-instructions>i[data-v-b5ea938f]{flex-shrink:0}.yaml-actions[data-v-b5ea938f]{flex-shrink:0;gap:.5rem;display:flex}.yaml-actions button[data-v-b5ea938f],.yaml-actions [role=button][data-v-b5ea938f]{width:auto;margin:0}.conflict-error[data-v-b5ea938f]{background-color:var(--pico-mark-background-color);color:var(--pico-del-color);border-radius:var(--pico-border-radius);align-items:center;gap:.5rem;margin-bottom:1rem;padding:1rem;display:flex}.conflict-error i[data-v-b5ea938f]{flex-shrink:0}@media (width<=768px){footer[data-v-b5ea938f]{flex-direction:column;align-items:stretch}.yaml-actions[data-v-b5ea938f]{flex-direction:column}.yaml-actions button[data-v-b5ea938f],.yaml-actions [role=button][data-v-b5ea938f]{width:100%}}#markdown-editor-overlay[data-v-e8d0cc10]{z-index:1000;position:fixed;inset:0}.markdown-editor-backdrop[data-v-e8d0cc10]{background-color:#00000080;position:absolute;inset:0}.markdown-editor-panel[data-v-e8d0cc10]{background-color:var(--pico-card-background-color,#fff);border-radius:12px;flex-direction:column;display:flex;position:absolute;inset:2rem;overflow:hidden;box-shadow:0 8px 32px #0000004d,0 2px 8px #0003}.lexical-toolbar[data-v-e8d0cc10]{background-color:var(--pico-card-background-color,#fff);z-index:1;flex-wrap:wrap;align-items:center;gap:.25rem;padding:.5rem 1rem;display:flex;box-shadow:0 2px 4px #0000001a}.toolbar-btn[data-v-e8d0cc10]{border-radius:var(--pico-border-radius);min-width:2rem;height:2rem;color:var(--pico-color);font-size:var(--fs-xs);cursor:pointer;background:0 0;border:1px solid #0000;justify-content:center;align-items:center;margin:0;padding:0 .5rem;font-weight:600;transition:all .15s;display:inline-flex}.toolbar-btn[data-v-e8d0cc10]:hover{background-color:var(--pico-muted-border-color);border-color:var(--pico-muted-border-color)}.toolbar-btn[data-v-e8d0cc10]:active{background-color:var(--pico-primary);border-color:var(--pico-primary);color:var(--pico-primary-inverse)}.toolbar-btn i[data-v-e8d0cc10]{font-size:var(--fs-md)}.toolbar-divider[data-v-e8d0cc10]{background-color:var(--pico-muted-border-color);width:1px;height:1.5rem;margin:0 .5rem}.toolbar-spacer[data-v-e8d0cc10]{flex:1}.toolbar-action[data-v-e8d0cc10]{font-size:var(--fs-xs);margin:0;padding:.4rem .75rem}.toolbar-select[data-v-e8d0cc10]{width:auto;min-width:11rem;max-width:15rem;height:2rem;font-size:var(--fs-xs);border:1px solid var(--pico-muted-border-color,#e4e4e7);border-radius:var(--pico-border-radius,.375rem);color:var(--pico-color,inherit);cursor:pointer;background-color:#0000;flex-shrink:0;margin:0;padding:0 .75rem}.toolbar-select[data-v-e8d0cc10]:hover{border-color:var(--pico-primary,#1e88e5)}.toolbar-select[data-v-e8d0cc10]:focus{border-color:var(--pico-primary,#1e88e5);outline:none}.editor-split[data-v-e8d0cc10]{flex-direction:column;flex:1;min-height:0;display:flex}.editor-diagram-preview[data-v-e8d0cc10]{border-top:1px solid var(--pico-muted-border-color,#e4e4e7);flex:0 45%;padding:.5rem 1.5rem 1rem;overflow:hidden}.editor-split.layout-right[data-v-e8d0cc10]{flex-direction:row}.editor-split.layout-right .lexical-editor-container[data-v-e8d0cc10]{flex:50%;min-width:0}.editor-split.layout-right .editor-diagram-preview[data-v-e8d0cc10]{border-top:none;border-left:1px solid var(--pico-muted-border-color,#e4e4e7);flex:50%;min-width:0;padding:1rem 1.5rem;overflow-y:auto}.editor-split.layout-full .lexical-editor-container[data-v-e8d0cc10]{display:none}.editor-split.layout-full .editor-diagram-preview[data-v-e8d0cc10]{border-top:none;flex:auto;overflow-y:auto}.editor-split.layout-hidden .editor-diagram-preview[data-v-e8d0cc10]{flex:none;padding-bottom:.5rem}.lexical-editor-container[data-v-e8d0cc10]{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}#lexical-editor-root[data-v-e8d0cc10]{min-height:0;font-family:var(--pico-font-family);font-size:var(--fs-sm);flex:1;padding:1rem 1.5rem;line-height:1.55;position:relative;overflow-y:auto}#lexical-editor-root[data-v-e8d0cc10]:focus{outline:none}#lexical-editor-root[data-v-e8d0cc10] p{margin:0 0 .75rem}#lexical-editor-root[data-v-e8d0cc10] p:last-child{margin-bottom:0}#lexical-editor-root[data-v-e8d0cc10] h1,#lexical-editor-root[data-v-e8d0cc10] h2,#lexical-editor-root[data-v-e8d0cc10] h3,#lexical-editor-root[data-v-e8d0cc10] h4{margin:1rem 0 .5rem;font-weight:600}#lexical-editor-root[data-v-e8d0cc10] h1{font-size:var(--fs-xl)}#lexical-editor-root[data-v-e8d0cc10] h2{font-size:var(--fs-lg)}#lexical-editor-root[data-v-e8d0cc10] h3{font-size:var(--fs-md)}#lexical-editor-root[data-v-e8d0cc10] h4{font-size:1em}#lexical-editor-root[data-v-e8d0cc10] h1:first-child,#lexical-editor-root[data-v-e8d0cc10] h2:first-child,#lexical-editor-root[data-v-e8d0cc10] h3:first-child{margin-top:0}#lexical-editor-root[data-v-e8d0cc10] ul,#lexical-editor-root[data-v-e8d0cc10] ol{margin:.5rem 0;padding-left:1.5rem}#lexical-editor-root[data-v-e8d0cc10] li{margin:.25rem 0}#lexical-editor-root[data-v-e8d0cc10] blockquote{border-left:3px solid var(--pico-muted-border-color,#e4e4e7);color:var(--pico-muted-color,#71717a);margin:.75rem 0;padding:.5rem 1rem;font-style:italic}#lexical-editor-root[data-v-e8d0cc10] code{background-color:var(--pico-code-background-color,#f4f4f5);border:1px solid var(--pico-muted-border-color,#e4e4e7);font-family:var(--pico-font-family-monospace,monospace);font-size:var(--fs-sm);border-radius:3px;padding:.15rem .35rem}#lexical-editor-root[data-v-e8d0cc10] pre{background-color:var(--pico-code-background-color);border-radius:var(--pico-border-radius);margin:.75rem 0;padding:1rem;position:relative;overflow-x:auto}#lexical-editor-root[data-v-e8d0cc10] pre code{background:0 0;padding:0}#lexical-editor-root[data-v-e8d0cc10] .lexical-code{background-color:var(--pico-code-background-color,#f4f4f5);border-top:1px solid var(--pico-muted-border-color,#e4e4e7);border-bottom:1px solid var(--pico-muted-border-color,#e4e4e7);min-width:calc(100% + 3rem);font-family:var(--pico-font-family-monospace,monospace);font-size:var(--fs-sm);white-space:pre;tab-size:2;box-sizing:border-box;border-radius:0;margin:.75rem -1.5rem;padding:1rem 1.5rem;line-height:1.5;display:block;position:relative;overflow-x:auto}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenComment{color:#6a737d}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenPunctuation{color:#24292e}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenProperty{color:#005cc5}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenSelector{color:#032f62}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenOperator{color:#005cc5}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenAttr{color:#d73a49}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenVariable{color:#e36209}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenFunction{color:#6f42c1}#lexical-editor-root[data-v-e8d0cc10] a{color:var(--pico-primary);text-decoration:underline}#lexical-editor-root[data-v-e8d0cc10] .lexical-link{color:var(--pico-primary,#1e88e5);cursor:pointer;text-decoration:underline}#lexical-editor-root[data-v-e8d0cc10] .lexical-link:hover{text-decoration:none}#lexical-editor-root[data-v-e8d0cc10] strong,#lexical-editor-root[data-v-e8d0cc10] .lexical-bold{font-weight:700}#lexical-editor-root[data-v-e8d0cc10] em,#lexical-editor-root[data-v-e8d0cc10] .lexical-italic{font-style:italic}#lexical-editor-root[data-v-e8d0cc10] u,#lexical-editor-root[data-v-e8d0cc10] .lexical-underline{text-decoration:underline}#lexical-editor-root[data-v-e8d0cc10] s,#lexical-editor-root[data-v-e8d0cc10] .lexical-strikethrough{text-decoration:line-through}@media (prefers-color-scheme:dark){.markdown-editor-panel[data-v-e8d0cc10]{background-color:var(--pico-card-background-color,#1e1e1e);box-shadow:0 8px 32px #00000080,0 2px 8px #0000004d}.markdown-editor-backdrop[data-v-e8d0cc10]{background-color:#000000b3}.lexical-toolbar[data-v-e8d0cc10]{background-color:var(--pico-card-background-color,#1e1e1e);box-shadow:0 2px 4px #0000004d}.toolbar-btn[data-v-e8d0cc10]{color:var(--pico-color,#c9d1d9)}.toolbar-btn[data-v-e8d0cc10]:hover{background-color:#ffffff1a;border-color:#ffffff1a}.toolbar-btn[data-v-e8d0cc10]:active{background-color:var(--pico-primary);border-color:var(--pico-primary)}.toolbar-divider[data-v-e8d0cc10]{background-color:#ffffff26}.toolbar-select[data-v-e8d0cc10]{color:var(--pico-color,#c9d1d9);background-color:#0000;border-color:#fff3}.toolbar-select[data-v-e8d0cc10]:hover,.toolbar-select[data-v-e8d0cc10]:focus{border-color:var(--pico-primary,#58a6ff)}#lexical-editor-root[data-v-e8d0cc10]{color:var(--pico-color,#c9d1d9);background-color:var(--pico-card-background-color,#1e1e1e)}#lexical-editor-root[data-v-e8d0cc10] blockquote{color:#8b949e;border-left-color:#fff3}#lexical-editor-root[data-v-e8d0cc10] code{color:#c9d1d9;background-color:#6e768166;border-color:#ffffff1a}#lexical-editor-root[data-v-e8d0cc10] .lexical-code{color:#c9d1d9;background-color:#0d1117;border-color:#ffffff1a}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenComment{color:#8b949e}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenPunctuation{color:#c9d1d9}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenProperty{color:#79c0ff}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenSelector{color:#a5d6ff}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenOperator{color:#79c0ff}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenAttr{color:#ff7b72}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenVariable{color:#ffa657}#lexical-editor-root[data-v-e8d0cc10] .lexical-code .tokenFunction{color:#d2a8ff}#lexical-editor-root[data-v-e8d0cc10] .lexical-link,#lexical-editor-root[data-v-e8d0cc10] a{color:#58a6ff}}.editor-form[data-v-822cad5e]{max-width:100%}.annotations-grid[data-v-822cad5e]{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem;display:grid}.annotations-grid .field-full-width[data-v-822cad5e]{grid-column:1/-1}.form-actions[data-v-822cad5e]{gap:.75rem;margin-top:1rem;display:flex}.form-actions button[data-v-822cad5e]{width:auto;margin:0;padding:.5rem 1.25rem}.btn-back[data-v-822cad5e]{align-items:center;gap:.25rem;text-decoration:none;display:inline-flex}.relations-editor .relation-item[data-v-822cad5e]{align-items:center;gap:.5rem;padding:.25rem 0;display:flex}.relations-editor .relation-text[data-v-822cad5e]{flex:1}.relations-editor .btn-remove[data-v-822cad5e]{cursor:pointer;color:var(--pico-del-color,#c62828);background:0 0;border:none;width:auto;margin:0;padding:.25rem}.relations-editor .add-relation-row[data-v-822cad5e]{align-items:flex-start;gap:.5rem;margin-top:.5rem;display:flex}.relations-editor .add-relation-row select[data-v-822cad5e]{flex:1;margin-bottom:0}.relations-editor .btn-add[data-v-822cad5e]{white-space:nowrap;margin-bottom:0}.no-relations[data-v-822cad5e]{color:var(--pico-muted-color);font-style:italic}.field-group[data-v-822cad5e]{margin-bottom:.5rem}.field-group .label-row[data-v-822cad5e]{align-items:center;gap:.5rem;margin-bottom:.25rem;display:flex}.field-group .required[data-v-822cad5e]{color:var(--pico-del-color,#c62828)}.field-group .field-description[data-v-822cad5e]{color:var(--pico-muted-color);margin-top:.125rem;display:block}.field-group .field-error[data-v-822cad5e]{color:var(--pico-del-color,#c62828);font-size:var(--fs-sm);margin-top:.125rem}.field-group .btn-edit-markdown[data-v-822cad5e]{border:1px solid var(--pico-primary);color:var(--pico-primary);cursor:pointer;font-size:var(--fs-sm);background:0 0;border-radius:4px;width:auto;margin:0;padding:.125rem .5rem}.field-group .future-hint[data-v-822cad5e]{color:var(--pico-muted-color);margin-top:.125rem;display:block}.field-group textarea[data-v-822cad5e]{min-height:8rem;font-family:inherit}.field-group .code-field textarea[data-v-822cad5e]{font-family:var(--font-mono)}.yaml-output-container .yaml-success[data-v-822cad5e]{color:var(--pico-ins-color,#2e7d32)}.yaml-output-container pre[data-v-822cad5e]{max-height:60vh;overflow:auto}.yaml-output-container .yaml-actions[data-v-822cad5e]{gap:.5rem;margin-top:.5rem;display:flex}.yaml-output-container .yaml-file-info[data-v-822cad5e],.yaml-output-container .yaml-instructions[data-v-822cad5e]{color:var(--pico-muted-color)}.yaml-output-container .copied[data-v-822cad5e],.yaml-output-container .saved[data-v-822cad5e]{opacity:.8}.conflict-error[data-v-822cad5e]{background:var(--pico-del-color,#c62828);color:#fff;border-radius:4px;align-items:center;gap:.5rem;margin-bottom:1rem;padding:.75rem 1rem;display:flex}.editor-container[data-v-822cad5e]{width:100%}.editor-header[data-v-822cad5e]{border-bottom:1px solid var(--pico-muted-border-color);justify-content:space-between;align-items:center;margin-bottom:1.5rem;padding-bottom:1rem;display:flex}.editor-header h2[data-v-822cad5e]{align-items:center;gap:.75rem;margin:0;display:flex}.header-actions[data-v-822cad5e]{align-items:center;gap:.5rem;display:flex}.btn-header[data-v-822cad5e]{font-size:var(--fs-sm);color:var(--pico-muted-color);border-radius:var(--pico-border-radius);cursor:pointer;background:0 0;border:1px solid #0000;align-items:center;gap:.35rem;padding:.4rem .75rem;text-decoration:none;transition:all .15s;display:inline-flex}.btn-header[data-v-822cad5e]:hover{color:var(--pico-primary);border-color:var(--pico-primary)}.yaml-output>header[data-v-822cad5e]{justify-content:space-between;align-items:center;display:flex}.yaml-output>header h3[data-v-822cad5e]{align-items:center;gap:.5rem;margin:0;display:flex}@media (width<=768px){.editor-header[data-v-822cad5e]{flex-direction:column;align-items:flex-start;gap:1rem}.annotations-grid[data-v-822cad5e]{grid-template-columns:1fr}}