archsight 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +5 -4
  3. data/README.md +17 -65
  4. data/chart/archsight/README.md +1 -158
  5. data/docs/architecture.md +39 -0
  6. data/docs/docker.md +49 -0
  7. data/{lib/archsight/web/doc → docs}/index.md.erb +3 -1
  8. data/docs/kubernetes.md +149 -0
  9. data/docs/licenses.md +307 -0
  10. data/lib/archsight/cli.rb +1 -1
  11. data/lib/archsight/graph.rb +1 -51
  12. data/lib/archsight/helpers.rb +0 -20
  13. data/lib/archsight/import/handlers/github.rb +3 -1
  14. data/lib/archsight/import/handlers/gitlab.rb +15 -5
  15. data/lib/archsight/import/handlers/repository.rb +53 -5
  16. data/lib/archsight/import/handlers/rest_api.rb +13 -1
  17. data/lib/archsight/import/license_analyzer.rb +650 -0
  18. data/lib/archsight/linter.rb +1 -1
  19. data/lib/archsight/mcp/base.rb +11 -0
  20. data/lib/archsight/renderer.rb +4 -4
  21. data/lib/archsight/resources/business_actor.rb +17 -2
  22. data/lib/archsight/resources/technology_artifact.rb +48 -0
  23. data/lib/archsight/version.rb +1 -1
  24. data/lib/archsight/web/api/docs.rb +37 -2
  25. data/lib/archsight/web/api/json_helpers.rb +79 -13
  26. data/lib/archsight/web/api/openapi/spec.yaml +699 -0
  27. data/lib/archsight/web/api/routes.rb +23 -0
  28. data/lib/archsight/web/application.rb +40 -128
  29. data/lib/archsight/web/editor/helpers.rb +150 -0
  30. data/lib/archsight/web/editor/routes.rb +85 -212
  31. data/lib/archsight/web/public/vue/ApiDocsPage-B1RqTNqh.js +1 -0
  32. data/lib/archsight/web/public/vue/ApiDocsPage-DhNTOH4o.css +1 -0
  33. data/lib/archsight/web/public/vue/DocPage-DzwBgBd4.js +1 -0
  34. data/lib/archsight/web/public/vue/EditorPage-D_miHSv4.js +34 -0
  35. data/lib/archsight/web/public/vue/EditorPage-Dq0MuTnp.css +1 -0
  36. data/lib/archsight/web/public/vue/ErrorPage-CQQtPey3.js +2 -0
  37. data/lib/archsight/web/public/vue/ErrorPage-CwPT3JUr.css +1 -0
  38. data/lib/archsight/web/public/vue/GraphView-DRcIqAiR.css +1 -0
  39. data/lib/archsight/web/public/vue/GraphView-T9jFH_qg.js +1 -0
  40. data/lib/archsight/web/public/vue/InstanceRouter-1Sm-CRhf.js +2 -0
  41. data/lib/archsight/web/public/vue/InstanceRouter-BJkDRXZY.css +1 -0
  42. data/lib/archsight/web/public/vue/KindList-JA_L_-Cz.js +1 -0
  43. data/lib/archsight/web/public/vue/ResourceList-8iqavWdg.js +1 -0
  44. data/lib/archsight/web/public/vue/ResourceList-DP-z-j71.css +1 -0
  45. data/lib/archsight/web/public/vue/SearchResults-BGHbg48-.css +1 -0
  46. data/lib/archsight/web/public/vue/SearchResults-BdgFeHcm.js +1 -0
  47. data/lib/archsight/web/public/vue/_basePickBy-CVgieyx-.js +1 -0
  48. data/lib/archsight/web/public/vue/_baseUniq-BNfrOSaP.js +1 -0
  49. data/lib/archsight/web/public/vue/architectureDiagram-VXUJARFQ-CJXNpTr5.js +36 -0
  50. data/lib/archsight/web/public/vue/blockDiagram-VD42YOAC-B5488Hes.js +122 -0
  51. data/lib/archsight/web/public/vue/c4Diagram-YG6GDRKO-eYY3hprM.js +10 -0
  52. data/lib/archsight/web/public/vue/chunk-4BX2VUAB-ZoXeL4D1.js +1 -0
  53. data/lib/archsight/web/public/vue/chunk-55IACEB6-rNtQYnu_.js +1 -0
  54. data/lib/archsight/web/public/vue/chunk-B4BG7PRW-DolAeVV9.js +165 -0
  55. data/lib/archsight/web/public/vue/chunk-DI55MBZ5-DnN0f_hj.js +220 -0
  56. data/lib/archsight/web/public/vue/chunk-FMBD7UC4-BQWOCMuR.js +15 -0
  57. data/lib/archsight/web/public/vue/chunk-QN33PNHL-DId301Kb.js +1 -0
  58. data/lib/archsight/web/public/vue/chunk-QZHKN3VN-xbY0NLgv.js +1 -0
  59. data/lib/archsight/web/public/vue/chunk-TZMSLE5B-CgF9_37b.js +1 -0
  60. data/lib/archsight/web/public/vue/classDiagram-2ON5EDUG-jGlvI-Za.js +1 -0
  61. data/lib/archsight/web/public/vue/classDiagram-v2-WZHVMYZB-jGlvI-Za.js +1 -0
  62. data/lib/archsight/web/public/vue/clone-6iRPe1-W.js +1 -0
  63. data/lib/archsight/web/public/vue/cose-bilkent-S5V4N54A-CB9Zfu50.js +1 -0
  64. data/lib/archsight/web/public/vue/cytoscape.esm-5J0xJHOV.js +321 -0
  65. data/lib/archsight/web/public/vue/dagre-6UL2VRFP-BqkmE-LI.js +4 -0
  66. data/lib/archsight/web/public/vue/diagram-PSM6KHXK-CKBfqtw3.js +24 -0
  67. data/lib/archsight/web/public/vue/diagram-QEK2KX5R-B78rOlvK.js +43 -0
  68. data/lib/archsight/web/public/vue/diagram-S2PKOQOG-BlXC6Cia.js +24 -0
  69. data/lib/archsight/web/public/vue/erDiagram-Q2GNP2WA-BnliyziJ.js +60 -0
  70. data/lib/archsight/web/public/vue/flowDiagram-NV44I4VS-wuqPowTd.js +162 -0
  71. data/lib/archsight/web/public/vue/ganttDiagram-JELNMOA3-GSffAIH3.js +267 -0
  72. data/lib/archsight/web/public/vue/gitGraphDiagram-V2S2FVAM-OA7VyugW.js +65 -0
  73. data/lib/archsight/web/public/vue/graph-BXHAtA0S.js +1 -0
  74. data/lib/archsight/web/public/vue/graphviz-CJms5bxZ.js +13 -0
  75. data/lib/archsight/web/public/vue/index-DsEsN0_K.js +2 -0
  76. data/lib/archsight/web/public/vue/index-Tiu4C-Sb.css +1 -0
  77. data/lib/archsight/web/public/vue/infoDiagram-HS3SLOUP-nlVe2qgv.js +2 -0
  78. data/lib/archsight/web/public/vue/journeyDiagram-XKPGCS4Q-CtTIcKwf.js +139 -0
  79. data/lib/archsight/web/public/vue/kanban-definition-3W4ZIXB7-837KX0sW.js +89 -0
  80. data/lib/archsight/web/public/vue/katex-C-M49wc6.js +261 -0
  81. data/lib/archsight/web/public/vue/layout-DtE0QdL6.js +1 -0
  82. data/lib/archsight/web/public/vue/mermaid-DpPHPFQh.js +250 -0
  83. data/lib/archsight/web/public/vue/mindmap-definition-VGOIOE7T-9gLF2AoY.js +68 -0
  84. data/lib/archsight/web/public/vue/pieDiagram-ADFJNKIX-CyCNgw3u.js +30 -0
  85. data/lib/archsight/web/public/vue/quadrantDiagram-AYHSOK5B-CkPh8g02.js +7 -0
  86. data/lib/archsight/web/public/vue/requirementDiagram-UZGBJVZJ-Dkt6OSlY.js +64 -0
  87. data/lib/archsight/web/public/vue/sankeyDiagram-TZEHDZUN-BqprTk8x.js +10 -0
  88. data/lib/archsight/web/public/vue/sequenceDiagram-WL72ISMW-CTmTe1FQ.js +145 -0
  89. data/lib/archsight/web/public/vue/stateDiagram-FKZM4ZOC-CphqmkEU.js +1 -0
  90. data/lib/archsight/web/public/vue/stateDiagram-v2-4FDKWEC3-CxaDW5sW.js +1 -0
  91. data/lib/archsight/web/public/vue/timeline-definition-IT6M3QCI-CSQUZkyE.js +61 -0
  92. data/lib/archsight/web/public/vue/treemap-GDKQZRPO-DTojm7Yr.js +162 -0
  93. data/lib/archsight/web/public/{css/graph.css → vue/useGraphviz-A5s4h76R.js} +2 -1
  94. data/lib/archsight/web/public/vue/useHighlight-C6Kb5G3l.js +10 -0
  95. data/lib/archsight/web/public/vue/useMermaid-DqxTrLRB.js +1 -0
  96. data/lib/archsight/web/public/vue/usePanZoom-BybZ_rfh.js +11 -0
  97. data/lib/archsight/web/public/vue/xychartDiagram-PRI3JC2R-B1ZJZtDC.js +7 -0
  98. data/lib/archsight/web/public/vue.html +15 -0
  99. data/media/artifact.jpg +0 -0
  100. data/media/service.jpg +0 -0
  101. metadata +86 -84
  102. data/lib/archsight/web/public/css/artifact.css +0 -995
  103. data/lib/archsight/web/public/css/base.css +0 -201
  104. data/lib/archsight/web/public/css/editor.css +0 -863
  105. data/lib/archsight/web/public/css/highlight.min.css +0 -10
  106. data/lib/archsight/web/public/css/iconoir.css +0 -22
  107. data/lib/archsight/web/public/css/instance.css +0 -824
  108. data/lib/archsight/web/public/css/layout.css +0 -421
  109. data/lib/archsight/web/public/css/mermaid-layers.css +0 -188
  110. data/lib/archsight/web/public/css/pico.min.css +0 -4
  111. data/lib/archsight/web/public/img/archimate.png +0 -0
  112. data/lib/archsight/web/public/img/togaf-high-level.png +0 -0
  113. data/lib/archsight/web/public/js/editor.js +0 -421
  114. data/lib/archsight/web/public/js/graph-zoom.js +0 -18
  115. data/lib/archsight/web/public/js/highlight.min.js +0 -3899
  116. data/lib/archsight/web/public/js/htmx.min.js +0 -1
  117. data/lib/archsight/web/public/js/lexical-editor.js +0 -308
  118. data/lib/archsight/web/public/js/mermaid-init.js +0 -88
  119. data/lib/archsight/web/public/js/mermaid.min.js +0 -2811
  120. data/lib/archsight/web/public/js/sparkline.js +0 -42
  121. data/lib/archsight/web/public/js/svg-pan-zoom.min.js +0 -3
  122. data/lib/archsight/web/public/js/svg-zoom-controls.js +0 -93
  123. data/lib/archsight/web/views/api_docs.erb +0 -19
  124. data/lib/archsight/web/views/index.haml +0 -12
  125. data/lib/archsight/web/views/partials/artifact/_activity.haml +0 -55
  126. data/lib/archsight/web/views/partials/artifact/_agentic.haml +0 -25
  127. data/lib/archsight/web/views/partials/artifact/_deployment.haml +0 -29
  128. data/lib/archsight/web/views/partials/artifact/_git_info.haml +0 -16
  129. data/lib/archsight/web/views/partials/artifact/_language_stats.haml +0 -53
  130. data/lib/archsight/web/views/partials/artifact/_links.haml +0 -24
  131. data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +0 -32
  132. data/lib/archsight/web/views/partials/artifact/_repositories.haml +0 -55
  133. data/lib/archsight/web/views/partials/artifact/_team.haml +0 -83
  134. data/lib/archsight/web/views/partials/artifact/_workflow.haml +0 -69
  135. data/lib/archsight/web/views/partials/components/_activity.haml +0 -37
  136. data/lib/archsight/web/views/partials/components/_git.haml +0 -17
  137. data/lib/archsight/web/views/partials/components/_jira.haml +0 -18
  138. data/lib/archsight/web/views/partials/components/_languages.haml +0 -29
  139. data/lib/archsight/web/views/partials/components/_owner.haml +0 -15
  140. data/lib/archsight/web/views/partials/components/_repositories.haml +0 -37
  141. data/lib/archsight/web/views/partials/components/_status.haml +0 -23
  142. data/lib/archsight/web/views/partials/editor/_field.haml +0 -80
  143. data/lib/archsight/web/views/partials/editor/_form.haml +0 -131
  144. data/lib/archsight/web/views/partials/editor/_relations.haml +0 -39
  145. data/lib/archsight/web/views/partials/editor/_yaml_output.haml +0 -33
  146. data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +0 -67
  147. data/lib/archsight/web/views/partials/instance/_analysis_result.haml +0 -64
  148. data/lib/archsight/web/views/partials/instance/_detail.haml +0 -107
  149. data/lib/archsight/web/views/partials/instance/_graph.haml +0 -6
  150. data/lib/archsight/web/views/partials/instance/_import_detail.haml +0 -87
  151. data/lib/archsight/web/views/partials/instance/_list.haml +0 -84
  152. data/lib/archsight/web/views/partials/instance/_relations.haml +0 -43
  153. data/lib/archsight/web/views/partials/instance/_requirements.haml +0 -41
  154. data/lib/archsight/web/views/partials/instance/_view_detail.haml +0 -57
  155. data/lib/archsight/web/views/partials/layout/_content.haml +0 -50
  156. data/lib/archsight/web/views/partials/layout/_error.haml +0 -22
  157. data/lib/archsight/web/views/partials/layout/_head.haml +0 -26
  158. data/lib/archsight/web/views/partials/layout/_navigation.haml +0 -21
  159. data/lib/archsight/web/views/partials/layout/_sidebar.haml +0 -27
  160. data/lib/archsight/web/views/search.haml +0 -53
  161. /data/{lib/archsight/web/doc → docs}/archimate.md +0 -0
  162. /data/{lib/archsight/web/doc → docs}/computed_annotations.md +0 -0
  163. /data/{lib/archsight/web/doc → docs}/icons.md +0 -0
  164. /data/{lib/archsight/web/doc → docs}/import.md +0 -0
  165. /data/{lib/archsight/web/doc → docs}/modeling.md +0 -0
  166. /data/{lib/archsight/web/doc → docs}/search.md +0 -0
  167. /data/{lib/archsight/web/doc → docs}/togaf.md +0 -0
  168. /data/{lib/archsight/web/doc → docs}/tool.md +0 -0
@@ -42,6 +42,15 @@ module Archsight::Web::API::Routes
42
42
  json_response(build_list_response(kind, pagination, resources))
43
43
  end
44
44
 
45
+ # GET /api/v1/kinds/:kind/filters - Get filterable annotations with values
46
+ get "/api/v1/kinds/:kind/filters" do
47
+ kind = params[:kind]
48
+ klass = Archsight::Resources[kind]
49
+ json_error("Kind '#{kind}' not found", status: 404, error_type: "NotFound") unless klass
50
+
51
+ json_response(build_filters_response(kind))
52
+ end
53
+
45
54
  # GET /api/v1/kinds/:kind/instances/:name - Get instance details with relations
46
55
  get "/api/v1/kinds/:kind/instances/:name" do
47
56
  kind = params[:kind]
@@ -79,6 +88,20 @@ module Archsight::Web::API::Routes
79
88
  end
80
89
  end
81
90
 
91
+ # POST /api/v1/kinds/Analysis/instances/:name/execute - Execute an analysis
92
+ post "/api/v1/kinds/Analysis/instances/:name/execute" do
93
+ require "archsight/analysis"
94
+
95
+ name = params[:name]
96
+ analysis = db.instance_by_kind("Analysis", name)
97
+ json_error("Analysis '#{name}' not found", status: 404, error_type: "NotFound") unless analysis
98
+
99
+ executor = Archsight::Analysis::Executor.new(db)
100
+ result = executor.execute(analysis)
101
+
102
+ json_response(build_analysis_result(result))
103
+ end
104
+
82
105
  # GET /api/v1/openapi.yaml - OpenAPI specification
83
106
  get "/api/v1/openapi.yaml" do
84
107
  content_type "text/yaml"
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "sinatra/base"
4
4
  require "kramdown"
5
- require "haml"
6
5
  require "erb"
7
6
  require "fast_mcp"
8
7
 
@@ -55,9 +54,7 @@ class Archsight::Web::Application < Sinatra::Base
55
54
  end
56
55
 
57
56
  configure do
58
- set :views, File.join(__dir__, "views")
59
57
  set :public_folder, File.join(__dir__, "public")
60
- set :haml, format: :html5
61
58
  set :server, :puma
62
59
  set :reload_enabled, true
63
60
  set :inline_edit_enabled, false
@@ -83,7 +80,7 @@ class Archsight::Web::Application < Sinatra::Base
83
80
  localhost_only: false
84
81
  end
85
82
 
86
- helpers Archsight::GraphvisHelper, Archsight::GraphvisRenderer, Archsight::Helpers
83
+ helpers Archsight::GraphvisRenderer, Archsight::Helpers
87
84
 
88
85
  # Register API modules
89
86
  register Archsight::Web::API::Routes
@@ -156,168 +153,83 @@ class Archsight::Web::Application < Sinatra::Base
156
153
  def render_analysis_section(section)
157
154
  Archsight::Helpers.render_analysis_section(section, markdown_renderer: method(:markdown))
158
155
  end
156
+
157
+ # Serve the Vue SPA shell (built by Vite)
158
+ def serve_vue
159
+ vue_path = File.join(settings.public_folder, "vue.html")
160
+ content_type :html
161
+ if File.exist?(vue_path)
162
+ File.read(vue_path)
163
+ else
164
+ "<!DOCTYPE html><html><body><p>Vue frontend not built. Run: cd frontend &amp;&amp; npm run build</p></body></html>"
165
+ end
166
+ end
159
167
  end
160
168
 
161
169
  get "/" do
162
- haml :index
170
+ serve_vue
163
171
  end
164
172
 
165
173
  get "/reload" do
166
174
  halt 404, "Reload is disabled" unless settings.reload_enabled
167
175
 
168
176
  Archsight::Web::Application.reload!
169
- if params["redirect"]&.start_with?("/")
177
+ if request.env["HTTP_ACCEPT"]&.include?("application/json") || request.xhr?
178
+ content_type :json
179
+ JSON.generate({ ok: true })
180
+ elsif params["redirect"]&.start_with?("/")
170
181
  redirect params["redirect"]
171
182
  else
172
183
  redirect "/"
173
184
  end
174
185
  rescue Archsight::ResourceError => e
175
- @error = e
176
- haml :index
177
- end
178
-
179
- get "/doc/resources/:filename" do
180
- filename = params["filename"].gsub(/[^a-zA-Z0-9_-]/, "") # sanitize
181
- # Convert snake_case to PascalCase for resource kind
182
- kind_name = filename.split("_").map(&:capitalize).join
183
-
184
- begin
185
- content = Archsight::Documentation.generate(kind_name)
186
- @doc_content = markdown(content)
187
- rescue StandardError
188
- halt 404, "Documentation not found"
189
- end
190
-
191
- if request.env["HTTP_HX_REQUEST"]
192
- "<article>#{@doc_content}</article>"
186
+ if request.env["HTTP_ACCEPT"]&.include?("application/json") || request.xhr?
187
+ content_type :json
188
+ status 422
189
+ JSON.generate({
190
+ error: e.message,
191
+ path: relative_error_path(e.ref.path),
192
+ line_no: e.ref.line_no,
193
+ context: error_context_lines(e.ref.path, e.ref.line_no)
194
+ })
193
195
  else
194
- haml :index
196
+ content_type :html
197
+ path = ERB::Util.html_escape(relative_error_path(e.ref.path))
198
+ msg = ERB::Util.html_escape(e.message)
199
+ "<!DOCTYPE html><html><body><h3>Error: #{msg}</h3><p>#{path} line #{e.ref.line_no}</p><a href='/'>Back</a></body></html>"
195
200
  end
196
201
  end
197
202
 
198
- get "/doc/:filename" do
199
- filename = params["filename"].gsub(/[^a-zA-Z0-9_-]/, "") # sanitize
200
-
201
- # Check for ERB template first, then plain markdown
202
- erb_path = File.join(settings.views, "..", "doc", "#{filename}.md.erb")
203
- md_path = File.join(settings.views, "..", "doc", "#{filename}.md")
204
-
205
- content = if File.exist?(erb_path)
206
- template = ERB.new(File.read(erb_path))
207
- template.result(binding)
208
- elsif File.exist?(md_path)
209
- File.read(md_path)
210
- else
211
- halt 404, "Documentation not found"
212
- end
213
-
214
- @doc_content = markdown(content)
215
-
216
- if request.env["HTTP_HX_REQUEST"]
217
- "<article>#{@doc_content}</article>"
218
- else
219
- haml :index
220
- end
203
+ get "/doc/resources/:filename" do
204
+ serve_vue
221
205
  end
222
206
 
223
- # Shared search logic for both GET and POST
224
- def perform_search
225
- start_time = Time.now
226
- if (@q = params["q"])
227
- @instances = db.query(@q)
228
- elsif (@tag = params["tag"]) && (@value = params["value"])
229
- @method = params["method"] || "=="
230
- # Build query string - quote value for string operators, leave unquoted for numeric
231
- quoted_value = if %w[> < >= <=].include?(@method)
232
- @value # Numeric comparison, no quotes
233
- else
234
- "\"#{@value.gsub('"', '\\"')}\"" # String comparison, quote it
235
- end
236
- @q = "#{@tag} #{@method} #{quoted_value}"
237
- @instances = db.query(@q)
238
- else
239
- @instances = []
240
- end
241
- if (@kind = params["kind"])
242
- @instances = @instances.select { |i| i.kind == @kind } if @kind
243
- end
244
- @search_time_ms = ((Time.now - start_time) * 1000).round(2)
245
- rescue Archsight::Query::QueryError => e
246
- @query_error = e
247
- @search_time_ms = ((Time.now - start_time) * 1000).round(2)
248
- @q = params["q"] || "#{params["tag"]} #{params["method"] || "=="} \"#{params["value"]}\""
207
+ get "/doc/:filename" do
208
+ serve_vue
249
209
  end
250
210
 
251
- # GET /search - for direct URL access, bookmarks, and browser history
211
+ # GET /search - for direct URL access, Vue SPA handles rendering
252
212
  get "/search" do
253
- perform_search
254
- haml :index
255
- end
256
-
257
- # POST /search - for HTMX requests
258
- post "/search" do
259
- perform_search
260
- haml :search
261
- end
262
-
263
- get "/svg" do
264
- content_type :svg
265
- create_graph_all(db, :draw_svg)
213
+ serve_vue
266
214
  end
267
215
 
268
216
  get "/dot" do
269
217
  content_type "text/plain"
270
- create_graph_all(db, :draw_dot)
218
+ create_graph_all(db)
271
219
  end
272
220
 
273
221
  get "/kinds/:kind" do
274
- @kind = params["kind"]
275
- haml :index
222
+ serve_vue
276
223
  end
277
224
 
278
225
  get "/kinds/:kind/instances/:instance" do
279
- @kind = params["kind"]
280
- @instance = params["instance"]
281
- haml :index
282
- end
283
-
284
- get "/kinds/:kind/instances/:instance/svg" do
285
- @kind = params["kind"]
286
- @instance = params["instance"]
287
- content_type :svg
288
- create_graph_one(db, @kind, @instance, :draw_svg)
226
+ serve_vue
289
227
  end
290
228
 
291
229
  get "/kinds/:kind/instances/:instance/dot" do
292
230
  @kind = params["kind"]
293
231
  @instance = params["instance"]
294
232
  content_type "text/plain"
295
- create_graph_one(db, @kind, @instance, :draw_dot)
296
- end
297
-
298
- # Execute an Analysis and return HTML results
299
- post "/kinds/Analysis/instances/:instance/execute" do
300
- require "archsight/analysis"
301
-
302
- @instance = params["instance"]
303
- analysis = db.instance_by_kind("Analysis", @instance)
304
-
305
- unless analysis
306
- return haml_inline('.analysis-error
307
- %i.iconoir-warning-triangle
308
- Analysis not found: #{@instance}')
309
- end
310
-
311
- executor = Archsight::Analysis::Executor.new(db)
312
- result = executor.execute(analysis)
313
-
314
- haml :"partials/instance/_analysis_result", locals: { result: result }
315
- end
316
-
317
- private
318
-
319
- # Helper for inline HAML rendering
320
- def haml_inline(template)
321
- haml template
233
+ create_graph_one(db, @kind, @instance)
322
234
  end
323
235
  end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../editor"
4
+ require_relative "form_builder"
5
+
6
+ module Archsight
7
+ module Web
8
+ module Editor
9
+ # Shared helpers for editor routes
10
+ module Helpers
11
+ # Get form fields for a resource kind
12
+ def editor_fields(kind)
13
+ FormBuilder.fields_for(kind)
14
+ end
15
+
16
+ # Get available relations for a resource kind
17
+ def available_relations(kind)
18
+ Archsight::Editor.available_relations(kind)
19
+ end
20
+
21
+ # Extract annotations from JSON body or form params
22
+ def extract_annotations(params)
23
+ annotations = params["annotations"] || {}
24
+ annotations.transform_values { |v| v.is_a?(String) ? v.strip : v }
25
+ end
26
+
27
+ # Parse relations from JSON array format
28
+ # Accepts: [{ verb, kind, names: [...] }] or [{ verb, kind, name }]
29
+ def parse_json_relations(relations_data)
30
+ return [] unless relations_data.is_a?(Array)
31
+
32
+ relations_data.each_with_object([]) do |rel, result|
33
+ next unless rel.is_a?(Hash)
34
+
35
+ parsed = parse_single_relation(rel)
36
+ next unless parsed
37
+
38
+ merge_relation(result, parsed)
39
+ end
40
+ end
41
+
42
+ # Validate content hash for optimistic locking
43
+ def validate_content_hash(instance, expected_hash)
44
+ Archsight::Editor::ContentHasher.validate(
45
+ path: instance.path_ref.path,
46
+ start_line: instance.path_ref.line_no,
47
+ expected_hash: expected_hash
48
+ )
49
+ end
50
+
51
+ # Build form metadata response for a kind
52
+ def build_form_metadata(kind, klass)
53
+ {
54
+ kind: kind,
55
+ icon: klass.icon,
56
+ layer: klass.layer,
57
+ fields: serialize_fields(kind),
58
+ relation_options: build_relation_options(kind),
59
+ instances_by_kind: build_instances_by_kind(kind),
60
+ inline_edit_enabled: settings.inline_edit_enabled
61
+ }
62
+ end
63
+
64
+ # Extract relations from an existing instance into form format
65
+ def extract_instance_relations(instance)
66
+ relations = []
67
+
68
+ instance.spec.each do |verb, relation_groups|
69
+ next unless relation_groups.is_a?(Hash)
70
+
71
+ relation_groups.each do |relation_name, targets|
72
+ next unless targets.is_a?(Array)
73
+
74
+ target_class = Archsight::Editor.target_class_for_relation(instance.kind, verb, relation_name)
75
+ next unless target_class
76
+
77
+ targets.each do |target|
78
+ target_name = target.respond_to?(:name) ? target.name : target.to_s
79
+ relations << { verb: verb, kind: target_class, name: target_name }
80
+ end
81
+ end
82
+ end
83
+
84
+ relations
85
+ end
86
+
87
+ private
88
+
89
+ def parse_single_relation(rel)
90
+ verb = (rel["verb"] || rel[:verb])&.to_s&.strip
91
+ kind = (rel["kind"] || rel[:kind])&.to_s&.strip
92
+ return if verb.nil? || verb.empty? || kind.nil? || kind.empty?
93
+
94
+ names = extract_relation_names(rel)
95
+ return if names.empty?
96
+
97
+ { verb: verb, kind: kind, names: names }
98
+ end
99
+
100
+ def extract_relation_names(rel)
101
+ names = rel["names"] || rel[:names]
102
+ name = rel["name"] || rel[:name]
103
+
104
+ if names.is_a?(Array)
105
+ names.map(&:to_s).reject(&:empty?)
106
+ elsif name
107
+ [name.to_s.strip].reject(&:empty?)
108
+ else
109
+ []
110
+ end
111
+ end
112
+
113
+ def merge_relation(relations, parsed)
114
+ existing = relations.find { |r| r[:verb] == parsed[:verb] && r[:kind] == parsed[:kind] }
115
+ if existing
116
+ parsed[:names].each { |n| existing[:names] << n unless existing[:names].include?(n) }
117
+ else
118
+ relations << parsed
119
+ end
120
+ end
121
+
122
+ def serialize_fields(kind)
123
+ editor_fields(kind).map do |f|
124
+ {
125
+ key: f.key, title: f.title, description: f.description,
126
+ input_type: f.input_type.to_s, options: f.options, step: f.step,
127
+ required: f.required, code_language: f.code_language&.to_s
128
+ }
129
+ end
130
+ end
131
+
132
+ def build_relation_options(kind)
133
+ available_relations(kind)
134
+ .map { |v, _, k| { combo: "#{v}:#{k}", verb: v.to_s, target_kind: k.to_s } }
135
+ .sort_by { |r| r[:combo] }
136
+ .uniq { |r| r[:combo] }
137
+ end
138
+
139
+ def build_instances_by_kind(kind)
140
+ target_kinds = build_relation_options(kind).map { |r| r[:target_kind] }.uniq
141
+ target_kinds.each_with_object({}) do |k, h|
142
+ h[k] = db.instances_by_kind(k).keys.sort
143
+ rescue StandardError
144
+ h[k] = []
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end