archsight 0.1.4 → 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 (190) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +5 -4
  3. data/README.md +44 -59
  4. data/chart/archsight/Chart.yaml +6 -0
  5. data/chart/archsight/README.md +3 -0
  6. data/chart/archsight/templates/NOTES.txt +22 -0
  7. data/chart/archsight/templates/_helpers.tpl +62 -0
  8. data/chart/archsight/templates/deployment.yaml +114 -0
  9. data/chart/archsight/templates/ingress.yaml +56 -0
  10. data/chart/archsight/templates/resources-configmap.yaml +10 -0
  11. data/chart/archsight/templates/resources-pvc.yaml +23 -0
  12. data/chart/archsight/templates/service.yaml +15 -0
  13. data/chart/archsight/templates/serviceaccount.yaml +12 -0
  14. data/chart/archsight/values.yaml +162 -0
  15. data/docs/architecture.md +39 -0
  16. data/docs/docker.md +49 -0
  17. data/{lib/archsight/web/doc → docs}/import.md +10 -2
  18. data/{lib/archsight/web/doc → docs}/index.md.erb +3 -1
  19. data/docs/kubernetes.md +149 -0
  20. data/docs/licenses.md +307 -0
  21. data/lib/archsight/analysis/executor.rb +0 -10
  22. data/lib/archsight/annotations/annotation.rb +85 -36
  23. data/lib/archsight/annotations/architecture_annotations.rb +1 -34
  24. data/lib/archsight/annotations/computed.rb +1 -1
  25. data/lib/archsight/annotations/generated_annotations.rb +6 -3
  26. data/lib/archsight/annotations/git_annotations.rb +8 -4
  27. data/lib/archsight/annotations/interface_annotations.rb +35 -0
  28. data/lib/archsight/cli.rb +4 -2
  29. data/lib/archsight/editor/content_hasher.rb +37 -0
  30. data/lib/archsight/editor/file_writer.rb +79 -0
  31. data/lib/archsight/editor.rb +237 -0
  32. data/lib/archsight/graph.rb +1 -51
  33. data/lib/archsight/helpers.rb +0 -20
  34. data/lib/archsight/import/handlers/github.rb +16 -6
  35. data/lib/archsight/import/handlers/gitlab.rb +28 -10
  36. data/lib/archsight/import/handlers/repository.rb +56 -6
  37. data/lib/archsight/import/handlers/rest_api.rb +13 -1
  38. data/lib/archsight/import/license_analyzer.rb +650 -0
  39. data/lib/archsight/import/team_matcher.rb +111 -61
  40. data/lib/archsight/linter.rb +1 -1
  41. data/lib/archsight/mcp/base.rb +11 -0
  42. data/lib/archsight/mcp/execute_analysis_tool.rb +100 -0
  43. data/lib/archsight/mcp.rb +1 -0
  44. data/lib/archsight/renderer.rb +4 -4
  45. data/lib/archsight/resources/analysis.rb +1 -17
  46. data/lib/archsight/resources/application_interface.rb +1 -5
  47. data/lib/archsight/resources/base.rb +14 -14
  48. data/lib/archsight/resources/business_actor.rb +18 -3
  49. data/lib/archsight/resources/technology_artifact.rb +48 -0
  50. data/lib/archsight/resources/technology_interface.rb +1 -1
  51. data/lib/archsight/resources/technology_service.rb +5 -0
  52. data/lib/archsight/version.rb +1 -1
  53. data/lib/archsight/web/api/docs.rb +37 -2
  54. data/lib/archsight/web/api/json_helpers.rb +79 -13
  55. data/lib/archsight/web/api/openapi/spec.yaml +699 -0
  56. data/lib/archsight/web/api/routes.rb +23 -0
  57. data/lib/archsight/web/application.rb +48 -128
  58. data/lib/archsight/web/editor/form_builder.rb +100 -0
  59. data/lib/archsight/web/editor/helpers.rb +150 -0
  60. data/lib/archsight/web/editor/routes.rb +166 -0
  61. data/lib/archsight/web/public/vue/ApiDocsPage-B1RqTNqh.js +1 -0
  62. data/lib/archsight/web/public/vue/ApiDocsPage-DhNTOH4o.css +1 -0
  63. data/lib/archsight/web/public/vue/DocPage-DzwBgBd4.js +1 -0
  64. data/lib/archsight/web/public/vue/EditorPage-D_miHSv4.js +34 -0
  65. data/lib/archsight/web/public/vue/EditorPage-Dq0MuTnp.css +1 -0
  66. data/lib/archsight/web/public/vue/ErrorPage-CQQtPey3.js +2 -0
  67. data/lib/archsight/web/public/vue/ErrorPage-CwPT3JUr.css +1 -0
  68. data/lib/archsight/web/public/vue/GraphView-DRcIqAiR.css +1 -0
  69. data/lib/archsight/web/public/vue/GraphView-T9jFH_qg.js +1 -0
  70. data/lib/archsight/web/public/vue/InstanceRouter-1Sm-CRhf.js +2 -0
  71. data/lib/archsight/web/public/vue/InstanceRouter-BJkDRXZY.css +1 -0
  72. data/lib/archsight/web/public/vue/KindList-JA_L_-Cz.js +1 -0
  73. data/lib/archsight/web/public/vue/ResourceList-8iqavWdg.js +1 -0
  74. data/lib/archsight/web/public/vue/ResourceList-DP-z-j71.css +1 -0
  75. data/lib/archsight/web/public/vue/SearchResults-BGHbg48-.css +1 -0
  76. data/lib/archsight/web/public/vue/SearchResults-BdgFeHcm.js +1 -0
  77. data/lib/archsight/web/public/vue/_basePickBy-CVgieyx-.js +1 -0
  78. data/lib/archsight/web/public/vue/_baseUniq-BNfrOSaP.js +1 -0
  79. data/lib/archsight/web/public/vue/architectureDiagram-VXUJARFQ-CJXNpTr5.js +36 -0
  80. data/lib/archsight/web/public/vue/blockDiagram-VD42YOAC-B5488Hes.js +122 -0
  81. data/lib/archsight/web/public/vue/c4Diagram-YG6GDRKO-eYY3hprM.js +10 -0
  82. data/lib/archsight/web/public/vue/chunk-4BX2VUAB-ZoXeL4D1.js +1 -0
  83. data/lib/archsight/web/public/vue/chunk-55IACEB6-rNtQYnu_.js +1 -0
  84. data/lib/archsight/web/public/vue/chunk-B4BG7PRW-DolAeVV9.js +165 -0
  85. data/lib/archsight/web/public/vue/chunk-DI55MBZ5-DnN0f_hj.js +220 -0
  86. data/lib/archsight/web/public/vue/chunk-FMBD7UC4-BQWOCMuR.js +15 -0
  87. data/lib/archsight/web/public/vue/chunk-QN33PNHL-DId301Kb.js +1 -0
  88. data/lib/archsight/web/public/vue/chunk-QZHKN3VN-xbY0NLgv.js +1 -0
  89. data/lib/archsight/web/public/vue/chunk-TZMSLE5B-CgF9_37b.js +1 -0
  90. data/lib/archsight/web/public/vue/classDiagram-2ON5EDUG-jGlvI-Za.js +1 -0
  91. data/lib/archsight/web/public/vue/classDiagram-v2-WZHVMYZB-jGlvI-Za.js +1 -0
  92. data/lib/archsight/web/public/vue/clone-6iRPe1-W.js +1 -0
  93. data/lib/archsight/web/public/vue/cose-bilkent-S5V4N54A-CB9Zfu50.js +1 -0
  94. data/lib/archsight/web/public/vue/cytoscape.esm-5J0xJHOV.js +321 -0
  95. data/lib/archsight/web/public/vue/dagre-6UL2VRFP-BqkmE-LI.js +4 -0
  96. data/lib/archsight/web/public/vue/diagram-PSM6KHXK-CKBfqtw3.js +24 -0
  97. data/lib/archsight/web/public/vue/diagram-QEK2KX5R-B78rOlvK.js +43 -0
  98. data/lib/archsight/web/public/vue/diagram-S2PKOQOG-BlXC6Cia.js +24 -0
  99. data/lib/archsight/web/public/vue/erDiagram-Q2GNP2WA-BnliyziJ.js +60 -0
  100. data/lib/archsight/web/public/vue/flowDiagram-NV44I4VS-wuqPowTd.js +162 -0
  101. data/lib/archsight/web/public/vue/ganttDiagram-JELNMOA3-GSffAIH3.js +267 -0
  102. data/lib/archsight/web/public/vue/gitGraphDiagram-V2S2FVAM-OA7VyugW.js +65 -0
  103. data/lib/archsight/web/public/vue/graph-BXHAtA0S.js +1 -0
  104. data/lib/archsight/web/public/vue/graphviz-CJms5bxZ.js +13 -0
  105. data/lib/archsight/web/public/vue/index-DsEsN0_K.js +2 -0
  106. data/lib/archsight/web/public/vue/index-Tiu4C-Sb.css +1 -0
  107. data/lib/archsight/web/public/vue/infoDiagram-HS3SLOUP-nlVe2qgv.js +2 -0
  108. data/lib/archsight/web/public/vue/journeyDiagram-XKPGCS4Q-CtTIcKwf.js +139 -0
  109. data/lib/archsight/web/public/vue/kanban-definition-3W4ZIXB7-837KX0sW.js +89 -0
  110. data/lib/archsight/web/public/vue/katex-C-M49wc6.js +261 -0
  111. data/lib/archsight/web/public/vue/layout-DtE0QdL6.js +1 -0
  112. data/lib/archsight/web/public/vue/mermaid-DpPHPFQh.js +250 -0
  113. data/lib/archsight/web/public/vue/mindmap-definition-VGOIOE7T-9gLF2AoY.js +68 -0
  114. data/lib/archsight/web/public/vue/pieDiagram-ADFJNKIX-CyCNgw3u.js +30 -0
  115. data/lib/archsight/web/public/vue/quadrantDiagram-AYHSOK5B-CkPh8g02.js +7 -0
  116. data/lib/archsight/web/public/vue/requirementDiagram-UZGBJVZJ-Dkt6OSlY.js +64 -0
  117. data/lib/archsight/web/public/vue/sankeyDiagram-TZEHDZUN-BqprTk8x.js +10 -0
  118. data/lib/archsight/web/public/vue/sequenceDiagram-WL72ISMW-CTmTe1FQ.js +145 -0
  119. data/lib/archsight/web/public/vue/stateDiagram-FKZM4ZOC-CphqmkEU.js +1 -0
  120. data/lib/archsight/web/public/vue/stateDiagram-v2-4FDKWEC3-CxaDW5sW.js +1 -0
  121. data/lib/archsight/web/public/vue/timeline-definition-IT6M3QCI-CSQUZkyE.js +61 -0
  122. data/lib/archsight/web/public/vue/treemap-GDKQZRPO-DTojm7Yr.js +162 -0
  123. data/lib/archsight/web/public/{css/graph.css → vue/useGraphviz-A5s4h76R.js} +2 -1
  124. data/lib/archsight/web/public/vue/useHighlight-C6Kb5G3l.js +10 -0
  125. data/lib/archsight/web/public/vue/useMermaid-DqxTrLRB.js +1 -0
  126. data/lib/archsight/web/public/vue/usePanZoom-BybZ_rfh.js +11 -0
  127. data/lib/archsight/web/public/vue/xychartDiagram-PRI3JC2R-B1ZJZtDC.js +7 -0
  128. data/lib/archsight/web/public/vue.html +15 -0
  129. data/media/artifact.jpg +0 -0
  130. data/media/service.jpg +0 -0
  131. metadata +104 -77
  132. data/lib/archsight/web/public/css/artifact.css +0 -995
  133. data/lib/archsight/web/public/css/base.css +0 -201
  134. data/lib/archsight/web/public/css/highlight.min.css +0 -10
  135. data/lib/archsight/web/public/css/iconoir.css +0 -22
  136. data/lib/archsight/web/public/css/instance.css +0 -818
  137. data/lib/archsight/web/public/css/layout.css +0 -421
  138. data/lib/archsight/web/public/css/mermaid-layers.css +0 -188
  139. data/lib/archsight/web/public/css/pico.min.css +0 -4
  140. data/lib/archsight/web/public/img/archimate.png +0 -0
  141. data/lib/archsight/web/public/img/togaf-high-level.png +0 -0
  142. data/lib/archsight/web/public/js/graph-zoom.js +0 -18
  143. data/lib/archsight/web/public/js/highlight.min.js +0 -3899
  144. data/lib/archsight/web/public/js/htmx.min.js +0 -1
  145. data/lib/archsight/web/public/js/mermaid-init.js +0 -88
  146. data/lib/archsight/web/public/js/mermaid.min.js +0 -2811
  147. data/lib/archsight/web/public/js/sparkline.js +0 -42
  148. data/lib/archsight/web/public/js/svg-pan-zoom.min.js +0 -3
  149. data/lib/archsight/web/public/js/svg-zoom-controls.js +0 -93
  150. data/lib/archsight/web/views/api_docs.erb +0 -19
  151. data/lib/archsight/web/views/index.haml +0 -12
  152. data/lib/archsight/web/views/partials/artifact/_activity.haml +0 -55
  153. data/lib/archsight/web/views/partials/artifact/_agentic.haml +0 -25
  154. data/lib/archsight/web/views/partials/artifact/_deployment.haml +0 -29
  155. data/lib/archsight/web/views/partials/artifact/_git_info.haml +0 -16
  156. data/lib/archsight/web/views/partials/artifact/_language_stats.haml +0 -53
  157. data/lib/archsight/web/views/partials/artifact/_links.haml +0 -24
  158. data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +0 -32
  159. data/lib/archsight/web/views/partials/artifact/_repositories.haml +0 -55
  160. data/lib/archsight/web/views/partials/artifact/_team.haml +0 -83
  161. data/lib/archsight/web/views/partials/artifact/_workflow.haml +0 -69
  162. data/lib/archsight/web/views/partials/components/_activity.haml +0 -37
  163. data/lib/archsight/web/views/partials/components/_git.haml +0 -17
  164. data/lib/archsight/web/views/partials/components/_jira.haml +0 -18
  165. data/lib/archsight/web/views/partials/components/_languages.haml +0 -29
  166. data/lib/archsight/web/views/partials/components/_owner.haml +0 -15
  167. data/lib/archsight/web/views/partials/components/_repositories.haml +0 -37
  168. data/lib/archsight/web/views/partials/components/_status.haml +0 -23
  169. data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +0 -74
  170. data/lib/archsight/web/views/partials/instance/_analysis_result.haml +0 -64
  171. data/lib/archsight/web/views/partials/instance/_detail.haml +0 -103
  172. data/lib/archsight/web/views/partials/instance/_graph.haml +0 -6
  173. data/lib/archsight/web/views/partials/instance/_import_detail.haml +0 -87
  174. data/lib/archsight/web/views/partials/instance/_list.haml +0 -84
  175. data/lib/archsight/web/views/partials/instance/_relations.haml +0 -43
  176. data/lib/archsight/web/views/partials/instance/_requirements.haml +0 -41
  177. data/lib/archsight/web/views/partials/instance/_view_detail.haml +0 -57
  178. data/lib/archsight/web/views/partials/layout/_content.haml +0 -44
  179. data/lib/archsight/web/views/partials/layout/_error.haml +0 -22
  180. data/lib/archsight/web/views/partials/layout/_head.haml +0 -24
  181. data/lib/archsight/web/views/partials/layout/_navigation.haml +0 -21
  182. data/lib/archsight/web/views/partials/layout/_sidebar.haml +0 -27
  183. data/lib/archsight/web/views/search.haml +0 -53
  184. /data/{lib/archsight/web/doc → docs}/archimate.md +0 -0
  185. /data/{lib/archsight/web/doc → docs}/computed_annotations.md +0 -0
  186. /data/{lib/archsight/web/doc → docs}/icons.md +0 -0
  187. /data/{lib/archsight/web/doc → docs}/modeling.md +0 -0
  188. /data/{lib/archsight/web/doc → docs}/search.md +0 -0
  189. /data/{lib/archsight/web/doc → docs}/togaf.md +0 -0
  190. /data/{lib/archsight/web/doc → docs}/tool.md +0 -0
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Archsight
6
+ module Editor
7
+ # ContentHasher generates SHA256 hashes for optimistic locking
8
+ module ContentHasher
9
+ module_function
10
+
11
+ # Generate a hash of YAML content for comparison
12
+ # Normalizes line endings before hashing to ensure consistency across platforms
13
+ # @param content [String] YAML content
14
+ # @return [String] 16-character hex hash
15
+ def hash(content)
16
+ normalized = content.gsub("\r\n", "\n").gsub("\r", "\n")
17
+ Digest::SHA256.hexdigest(normalized)[0, 16]
18
+ end
19
+
20
+ # Validate that content hasn't changed since expected_hash was computed
21
+ # @param path [String] File path
22
+ # @param start_line [Integer] Line number where document starts
23
+ # @param expected_hash [String, nil] Expected content hash
24
+ # @return [Hash, nil] Error hash with :conflict and :error keys, or nil if valid
25
+ def validate(path:, start_line:, expected_hash:)
26
+ return nil unless expected_hash
27
+
28
+ current_content = FileWriter.read_document(path: path, start_line: start_line)
29
+ current_hash = hash(current_content)
30
+
31
+ return nil if current_hash == expected_hash
32
+
33
+ { conflict: true, error: "Conflict: The resource has been modified. Please reload the page and try again." }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Archsight
4
+ module Editor
5
+ # FileWriter handles reading and writing YAML documents in multi-document files
6
+ module FileWriter
7
+ class WriteError < StandardError; end
8
+
9
+ module_function
10
+
11
+ # Read a YAML document from a file starting at a given line
12
+ # @param path [String] File path
13
+ # @param start_line [Integer] Line number where document starts (1-indexed)
14
+ # @return [String] Document content
15
+ # @raise [WriteError] if file not found or line out of bounds
16
+ def read_document(path:, start_line:)
17
+ raise WriteError, "File not found: #{path}" unless File.exist?(path)
18
+
19
+ lines = File.readlines(path)
20
+ start_idx = start_line - 1 # Convert to 0-indexed
21
+
22
+ raise WriteError, "Line #{start_line} is beyond end of file" if start_idx >= lines.length
23
+
24
+ # Find the end of this document (next --- or EOF)
25
+ end_idx = find_document_end(lines, start_idx)
26
+
27
+ # Extract and join the document lines
28
+ lines[start_idx...end_idx].join
29
+ end
30
+
31
+ # Replace a YAML document in a file starting at a given line
32
+ # @param path [String] File path
33
+ # @param start_line [Integer] Line number where document starts (1-indexed)
34
+ # @param new_yaml [String] New YAML content (without leading ---)
35
+ # @raise [WriteError] if file cannot be written or document not found at expected line
36
+ def replace_document(path:, start_line:, new_yaml:)
37
+ raise WriteError, "File not found: #{path}" unless File.exist?(path)
38
+ raise WriteError, "File not writable: #{path}" unless File.writable?(path)
39
+
40
+ lines = File.readlines(path)
41
+ start_idx = start_line - 1 # Convert to 0-indexed
42
+
43
+ raise WriteError, "Line #{start_line} is beyond end of file" if start_idx >= lines.length
44
+
45
+ # Find the end of this document (next --- or EOF)
46
+ end_idx = find_document_end(lines, start_idx)
47
+
48
+ # Build the new content
49
+ # Ensure new_yaml ends with a newline
50
+ new_yaml = "#{new_yaml}\n" unless new_yaml.end_with?("\n")
51
+
52
+ # Replace the document
53
+ new_lines = lines[0...start_idx] + [new_yaml] + lines[end_idx..]
54
+
55
+ # Write atomically by writing to temp file then renaming
56
+ File.write(path, new_lines.join)
57
+ end
58
+
59
+ # Find the end index of a document (the line index of the next --- or EOF)
60
+ # @param lines [Array<String>] File lines
61
+ # @param start_idx [Integer] Starting line index (0-indexed)
62
+ # @return [Integer] End index (exclusive - the line after the document ends)
63
+ def find_document_end(lines, start_idx)
64
+ # Start searching from the line after start_idx
65
+ idx = start_idx + 1
66
+
67
+ while idx < lines.length
68
+ # Check if this line is a document separator
69
+ return idx if lines[idx].strip == "---"
70
+
71
+ idx += 1
72
+ end
73
+
74
+ # No separator found, document goes to EOF
75
+ lines.length
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "resources"
5
+ require_relative "editor/file_writer"
6
+ require_relative "editor/content_hasher"
7
+
8
+ module Archsight
9
+ # Editor handles building and validating resources for the web editor
10
+ module Editor
11
+ module_function
12
+
13
+ # Build a resource hash from form params
14
+ # @param kind [String] Resource kind (e.g., "TechnologyArtifact")
15
+ # @param name [String] Resource name
16
+ # @param annotations [Hash] Annotation key-value pairs
17
+ # @param relations [Array<Hash>] Array of {verb:, kind:, names:[]} hashes
18
+ # where kind is the target class name (e.g., "TechnologyArtifact")
19
+ # @return [Hash] Resource hash ready for YAML conversion
20
+ def build_resource(kind:, name:, annotations: {}, relations: [])
21
+ resource = {
22
+ "apiVersion" => "architecture/v1alpha1",
23
+ "kind" => kind,
24
+ "metadata" => {
25
+ "name" => name
26
+ }
27
+ }
28
+
29
+ # Add annotations if any non-empty values exist
30
+ filtered_annotations = filter_empty(annotations)
31
+ resource["metadata"]["annotations"] = filtered_annotations unless filtered_annotations.empty?
32
+
33
+ # Add spec with relations if any exist
34
+ spec = build_spec(kind, relations)
35
+ resource["spec"] = spec unless spec.empty?
36
+
37
+ resource
38
+ end
39
+
40
+ # Validate resource params using annotation definitions
41
+ # @param kind [String] Resource kind
42
+ # @param name [String] Resource name
43
+ # @param annotations [Hash] Annotation key-value pairs
44
+ # @return [Hash] { valid: Boolean, errors: { field => [messages] } }
45
+ def validate(kind, name:, annotations: {})
46
+ klass = Archsight::Resources[kind]
47
+ errors = {}
48
+
49
+ # Validate name
50
+ if name.nil? || name.strip.empty?
51
+ errors["name"] = ["Name is required"]
52
+ elsif name =~ /\s/
53
+ errors["name"] = ["Name cannot contain spaces"]
54
+ end
55
+
56
+ # Validate annotations against their definitions
57
+ klass.annotations.reject(&:pattern?).each do |ann|
58
+ value = annotations[ann.key]
59
+ next if value.nil? || value.to_s.strip.empty?
60
+
61
+ ann_errors = ann.validate(value)
62
+ errors[ann.key] = ann_errors if ann_errors.any?
63
+ end
64
+
65
+ { valid: errors.empty?, errors: errors }
66
+ end
67
+
68
+ # Generate YAML string from resource hash
69
+ # Uses custom YAML dump that formats multiline strings with literal block scalars
70
+ # @param resource_hash [Hash] Resource hash
71
+ # @return [String] YAML string
72
+ def to_yaml(resource_hash)
73
+ visitor = Psych::Visitors::YAMLTree.create
74
+ visitor << resource_hash
75
+
76
+ # Walk the AST and apply scalar style for multiline strings
77
+ ast = visitor.tree
78
+ apply_block_scalar_style(ast)
79
+
80
+ ast.yaml(nil, line_width: 80)
81
+ end
82
+
83
+ # Recursively apply literal block style for multiline strings in YAML AST
84
+ # @param node [Psych::Nodes::Node] YAML AST node
85
+ def apply_block_scalar_style(node)
86
+ case node
87
+ when Psych::Nodes::Scalar
88
+ if node.value.is_a?(String)
89
+ # Normalize Windows/old Mac line endings to Unix style
90
+ node.value = node.value.gsub("\r\n", "\n").gsub("\r", "\n") if node.value.include?("\r")
91
+ # Use literal block style for multiline strings
92
+ node.style = Psych::Nodes::Scalar::LITERAL if node.value.include?("\n")
93
+ end
94
+ when Psych::Nodes::Sequence, Psych::Nodes::Mapping, Psych::Nodes::Document, Psych::Nodes::Stream
95
+ node.children.each { |child| apply_block_scalar_style(child) }
96
+ end
97
+ end
98
+
99
+ # Get editable annotations for a resource kind
100
+ # Excludes pattern annotations, computed annotations, and annotations with editor: false
101
+ # @param kind [String] Resource kind
102
+ # @return [Array<Archsight::Annotations::Annotation>]
103
+ def editable_annotations(kind)
104
+ klass = Archsight::Resources[kind]
105
+ return [] unless klass
106
+
107
+ # Get all annotations except pattern annotations
108
+ annotations = klass.annotations.reject(&:pattern?)
109
+
110
+ # Filter out computed annotations
111
+ computed_keys = klass.computed_annotations.map(&:key)
112
+ annotations = annotations.reject { |a| computed_keys.include?(a.key) }
113
+
114
+ # Filter out non-editable annotations
115
+ annotations.reject { |a| a.editor == false }
116
+ end
117
+
118
+ # Get available relations for a resource kind
119
+ # @param kind [String] Resource kind
120
+ # @return [Array<Array>] Array of [verb, target_kind, target_class_name]
121
+ def available_relations(kind)
122
+ klass = Archsight::Resources[kind]
123
+ return [] unless klass
124
+
125
+ klass.relations
126
+ end
127
+
128
+ # Get unique verbs for a resource kind's relations
129
+ # @param kind [String] Resource kind
130
+ # @return [Array<String>]
131
+ def relation_verbs(kind)
132
+ available_relations(kind).map { |v, _, _| v.to_s }.uniq.sort
133
+ end
134
+
135
+ # Get valid target class names for a given verb (for UI display and instance lookup)
136
+ # @param kind [String] Resource kind
137
+ # @param verb [String] Relation verb
138
+ # @return [Array<String>] Target class names (e.g., "TechnologyArtifact")
139
+ def target_kinds_for_verb(kind, verb)
140
+ # Relations structure is [verb, relation_name, target_class_name]
141
+ available_relations(kind)
142
+ .select { |v, _, _| v.to_s == verb.to_s }
143
+ .map { |_, _, target_class| target_class.to_s }
144
+ .uniq
145
+ .sort
146
+ end
147
+
148
+ # Get relation name for a given verb and target class (for building spec)
149
+ # @param kind [String] Source resource kind
150
+ # @param verb [String] Relation verb
151
+ # @param target_class [String] Target class name
152
+ # @return [String, nil] Relation name (e.g., "technologyComponents")
153
+ def relation_name_for(kind, verb, target_class)
154
+ relation = available_relations(kind).find do |v, _, tc|
155
+ v.to_s == verb.to_s && tc.to_s == target_class.to_s
156
+ end
157
+ return nil unless relation
158
+
159
+ relation[1].to_s
160
+ end
161
+
162
+ # Get target class name for a given verb and relation name (reverse lookup)
163
+ # @param kind [String] Source resource kind
164
+ # @param verb [String] Relation verb
165
+ # @param relation_name [String] Relation name (e.g., "businessActors")
166
+ # @return [String, nil] Target class name (e.g., "BusinessActor")
167
+ def target_class_for_relation(kind, verb, relation_name)
168
+ relation = available_relations(kind).find do |v, rn, _|
169
+ v.to_s == verb.to_s && rn.to_s == relation_name.to_s
170
+ end
171
+ return nil unless relation
172
+
173
+ relation[2].to_s
174
+ end
175
+
176
+ # Filter out empty values from a hash and convert to plain Hash
177
+ # (avoids !ruby/hash:Sinatra::IndifferentHash in YAML output)
178
+ def filter_empty(hash)
179
+ return {} if hash.nil?
180
+
181
+ result = hash.reject { |_, v| v.nil? || v.to_s.strip.empty? }
182
+ # Convert to plain Hash to avoid Ruby-specific YAML tags
183
+ result.to_h
184
+ end
185
+
186
+ # Build spec hash from relations array
187
+ # @param source_kind [String] The source resource kind
188
+ # @param relations [Array<Hash>] Array of {verb:, kind:, names:[]} hashes
189
+ # where kind is the target class name (e.g., "TechnologyArtifact")
190
+ # @return [Hash] Spec hash with proper relation_name keys
191
+ def build_spec(source_kind, relations)
192
+ return {} if relations.nil? || relations.empty?
193
+
194
+ spec = {}
195
+ relations.each { |rel| add_relation_to_spec(spec, source_kind, rel) }
196
+ deduplicate_spec_values(spec)
197
+ end
198
+
199
+ def add_relation_to_spec(spec, source_kind, rel)
200
+ verb, target_class, names = extract_relation_parts(rel)
201
+ return if invalid_relation?(verb, target_class, names)
202
+
203
+ rel_name = relation_name_for(source_kind, verb, target_class)
204
+ return unless rel_name
205
+
206
+ spec[verb.to_s] ||= {}
207
+ spec[verb.to_s][rel_name] ||= []
208
+ spec[verb.to_s][rel_name].concat(names)
209
+ end
210
+
211
+ def extract_relation_parts(rel)
212
+ verb = rel[:verb] || rel["verb"]
213
+ target_class = rel[:kind] || rel["kind"]
214
+ names = normalize_names(rel[:names] || rel["names"] || [])
215
+ [verb, target_class, names]
216
+ end
217
+
218
+ def normalize_names(names)
219
+ names = [names] unless names.is_a?(Array)
220
+ names.map(&:to_s).reject(&:empty?)
221
+ end
222
+
223
+ def invalid_relation?(verb, target_class, names)
224
+ verb.nil? || verb.to_s.strip.empty? ||
225
+ target_class.nil? || target_class.to_s.strip.empty? ||
226
+ names.empty?
227
+ end
228
+
229
+ def deduplicate_spec_values(spec)
230
+ spec.transform_values { |kinds| kinds.transform_values(&:uniq) }
231
+ end
232
+
233
+ private_class_method :filter_empty, :build_spec, :add_relation_to_spec,
234
+ :extract_relation_parts, :normalize_names, :invalid_relation?,
235
+ :deduplicate_spec_values
236
+ end
237
+ end
@@ -3,33 +3,17 @@
3
3
  module Archsight
4
4
  # Graphvis implements a graphviz abstraction
5
5
  class Graphvis
6
- def initialize(name, renderer = :dot, attrs = {})
6
+ def initialize(name, attrs = {})
7
7
  @name = name
8
- @renderer = renderer.to_s
9
8
  @attrs = { rankdir: :LR }.merge(attrs)
10
9
  end
11
10
 
12
- def draw_and_render_file(&)
13
- File.open("#{@name}.dot", "w") do |f|
14
- render(f, &)
15
- end
16
- system "#{@renderer} -Tpng #{@name}.dot -o #{@name}.png"
17
- end
18
-
19
11
  def draw_dot(&)
20
12
  f = StringIO.new
21
13
  render(f, &)
22
14
  f.string
23
15
  end
24
16
 
25
- def draw_svg(&)
26
- IO.popen([@renderer, "-Tsvg"], "r+") do |pipe|
27
- render(pipe, &)
28
- pipe.close_write
29
- pipe.read
30
- end
31
- end
32
-
33
17
  def render(file)
34
18
  @file = file
35
19
  file.puts "digraph G {"
@@ -76,38 +60,4 @@ module Archsight
76
60
  @file.puts " }"
77
61
  end
78
62
  end
79
-
80
- # GraphvisHelper for generating graphs using javascript and wasm
81
- module GraphvisHelper
82
- def graphviz_svg(dot, element_id)
83
- format(%{
84
- <script type="module">
85
- import { Graphviz } from "https://cdn.jsdelivr.net/npm/@hpcc-js/wasm/dist/index.js";
86
- if (Graphviz) {
87
- const re =/<svg width="([^"]+)pt" height="([^"]+)pt"/;
88
- const graphviz = await Graphviz.load();
89
- const dot = %s;
90
- let svg = graphviz.layout(dot, "svg", "dot");
91
- svg = svg.replace(re, "<svg");
92
- svg = svg.replace(/<polygon fill="white"[^>]*>/, "");
93
- svg = svg.replace(/fill="white"/g, 'fill="none"');
94
-
95
- const parser = new DOMParser();
96
- const svgDoc = parser.parseFromString(svg, "image/svg+xml");
97
- const svgElement = svgDoc.querySelector("svg");
98
-
99
- const cssResponse = await fetch("/css/graph.css?" + Date.now());
100
- const cssText = await cssResponse.text();
101
- const style = document.createElementNS("http://www.w3.org/2000/svg", "style");
102
- style.textContent = cssText;
103
- svgElement.insertBefore(style, svgElement.firstChild);
104
-
105
- document.getElementById(%s).innerHTML = svgElement.outerHTML;
106
- // Dispatch event to notify GraphViewer that SVG is ready
107
- document.dispatchEvent(new CustomEvent('graphviz:ready', { detail: { elementId: %s } }));
108
- }
109
- </script>
110
- }, dot.inspect, element_id.inspect, element_id.inspect)
111
- end
112
- end
113
63
  end
@@ -64,26 +64,6 @@ module Archsight
64
64
  end
65
65
  end
66
66
 
67
- # Generate htmx attributes for a custom search query
68
- def search_link_attrs(query)
69
- search_url = "/search?#{URI.encode_www_form(q: query)}"
70
- {
71
- "href" => search_url,
72
- "hx-post" => "/search",
73
- "hx-vals" => { q: query }.to_json,
74
- "hx-swap" => "innerHTML",
75
- "hx-target" => ".content",
76
- "hx-push-url" => search_url
77
- }
78
- end
79
-
80
- # Generate htmx attributes for filtering by annotation (convenience wrapper)
81
- def filter_link_attrs(tag, value, method = "==", kind = nil)
82
- query = "#{tag} #{method} \"#{value}\""
83
- query = "#{kind}: #{query}" if kind
84
- search_link_attrs(query)
85
- end
86
-
87
67
  # Get icon class for a URL based on domain patterns
88
68
  def icon_for_url(url)
89
69
  case url
@@ -14,6 +14,10 @@ require_relative "../registry"
14
14
  # import/config/org - GitHub organization name
15
15
  # import/config/repoOutputPath - Output path for repository handler results (e.g., "generated/repositories.yaml")
16
16
  # import/config/childCacheTime - Cache time for generated child imports (e.g., "1h", "30m")
17
+ # import/config/fallbackTeam - Default team when no contributor match found (propagated to child imports)
18
+ # import/config/botTeam - Team for bot-only repositories (propagated to child imports)
19
+ # import/config/corporateAffixes - Comma-separated corporate username affixes for team matching (propagated to child imports)
20
+ # import/config/defaultVisibility - Default visibility for non-public repos (default: "internal")
17
21
  #
18
22
  # Environment:
19
23
  # GITHUB_TOKEN - GitHub Personal Access Token (required)
@@ -35,6 +39,7 @@ class Archsight::Import::Handlers::Github < Archsight::Import::Handler
35
39
 
36
40
  @repo_output_path = config("repoOutputPath")
37
41
  @child_cache_time = config("childCacheTime")
42
+ @default_visibility = config("defaultVisibility", default: "internal")
38
43
  @target_dir = File.join(Dir.home, ".cache", "archsight", "git", "github", @org)
39
44
 
40
45
  # Fetch all repositories with pagination
@@ -136,15 +141,20 @@ class Archsight::Import::Handlers::Github < Archsight::Import::Handler
136
141
  child_annotations["import/outputPath"] = @repo_output_path if @repo_output_path
137
142
  child_annotations["import/cacheTime"] = @child_cache_time if @child_cache_time
138
143
 
144
+ child_config = {
145
+ "path" => repo_path,
146
+ "gitUrl" => git_url,
147
+ "archived" => repo["isArchived"].to_s,
148
+ "visibility" => visibility == "public" ? "open-source" : @default_visibility
149
+ }
150
+ child_config["fallbackTeam"] = config("fallbackTeam") if config("fallbackTeam")
151
+ child_config["botTeam"] = config("botTeam") if config("botTeam")
152
+ child_config["corporateAffixes"] = config("corporateAffixes") if config("corporateAffixes")
153
+
139
154
  import_yaml(
140
155
  name: "Import:Repo:github:#{@org}:#{repo_name}",
141
156
  handler: "repository",
142
- config: {
143
- "path" => repo_path,
144
- "gitUrl" => git_url,
145
- "archived" => repo["isArchived"].to_s,
146
- "visibility" => visibility == "public" ? "open-source" : "internal"
147
- },
157
+ config: child_config,
148
158
  annotations: child_annotations
149
159
  )
150
160
  end
@@ -19,6 +19,10 @@ require_relative "../registry"
19
19
  # import/config/sslFingerprint - SSL certificate fingerprint for pinning (SHA256, colon-separated hex)
20
20
  # import/config/repoOutputPath - Output path for repository handler results (e.g., "generated/repositories.yaml")
21
21
  # import/config/childCacheTime - Cache time for generated child imports (e.g., "1h", "30m")
22
+ # import/config/fallbackTeam - Default team when no contributor match found (propagated to child imports)
23
+ # import/config/botTeam - Team for bot-only repositories (propagated to child imports)
24
+ # import/config/corporateAffixes - Comma-separated corporate username affixes for team matching (propagated to child imports)
25
+ # import/config/defaultVisibility - Default visibility when API returns none (default: "internal")
22
26
  #
23
27
  # Environment:
24
28
  # GITLAB_TOKEN - GitLab personal access token (required)
@@ -36,6 +40,7 @@ class Archsight::Import::Handlers::Gitlab < Archsight::Import::Handler
36
40
 
37
41
  @repo_output_path = config("repoOutputPath")
38
42
  @child_cache_time = config("childCacheTime")
43
+ @default_visibility = config("defaultVisibility", default: "internal")
39
44
 
40
45
  @target_dir = File.join(Dir.home, ".cache", "archsight", "git", "gitlab")
41
46
  @explore_groups = config("exploreGroups") == "true"
@@ -74,8 +79,9 @@ class Archsight::Import::Handlers::Gitlab < Archsight::Import::Handler
74
79
 
75
80
  # Configure SSL verification
76
81
  if @ssl_fingerprint
77
- # Use certificate pinning - disable default verification, we verify fingerprint manually
78
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
82
+ # Use certificate pinning - verify fingerprint in callback, return false to reject
83
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
84
+ fingerprint_mismatch = nil
79
85
  http.verify_callback = lambda do |_preverify_ok, cert_store|
80
86
  # Get the peer certificate from the chain
81
87
  cert = cert_store.chain&.first
@@ -85,7 +91,10 @@ class Archsight::Import::Handlers::Gitlab < Archsight::Import::Handler
85
91
  fingerprint = OpenSSL::Digest::SHA256.new(cert.to_der).to_s.upcase.scan(/../).join(":")
86
92
  expected = @ssl_fingerprint.upcase
87
93
 
88
- raise OpenSSL::SSL::SSLError, "Certificate fingerprint mismatch! Expected: #{expected}, Got: #{fingerprint}" if fingerprint != expected
94
+ if fingerprint != expected
95
+ fingerprint_mismatch = "Certificate fingerprint mismatch! Expected: #{expected}, Got: #{fingerprint}"
96
+ return false
97
+ end
89
98
 
90
99
  true
91
100
  end
@@ -96,7 +105,11 @@ class Archsight::Import::Handlers::Gitlab < Archsight::Import::Handler
96
105
  request = Net::HTTP::Get.new(uri)
97
106
  request["PRIVATE-TOKEN"] = @token
98
107
 
99
- response = http.request(request)
108
+ begin
109
+ response = http.request(request)
110
+ rescue OpenSSL::SSL::SSLError => e
111
+ raise OpenSSL::SSL::SSLError, fingerprint_mismatch || e.message
112
+ end
100
113
 
101
114
  raise "GitLab API error: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
102
115
 
@@ -177,15 +190,20 @@ class Archsight::Import::Handlers::Gitlab < Archsight::Import::Handler
177
190
  child_annotations["import/outputPath"] = @repo_output_path if @repo_output_path
178
191
  child_annotations["import/cacheTime"] = @child_cache_time if @child_cache_time
179
192
 
193
+ child_config = {
194
+ "path" => repo_path,
195
+ "gitUrl" => git_url,
196
+ "archived" => project["archived"].to_s,
197
+ "visibility" => project["visibility"] || @default_visibility
198
+ }
199
+ child_config["fallbackTeam"] = config("fallbackTeam") if config("fallbackTeam")
200
+ child_config["botTeam"] = config("botTeam") if config("botTeam")
201
+ child_config["corporateAffixes"] = config("corporateAffixes") if config("corporateAffixes")
202
+
180
203
  import_yaml(
181
204
  name: "Import:Repo:gitlab:#{dir_name}",
182
205
  handler: "repository",
183
- config: {
184
- "path" => repo_path,
185
- "gitUrl" => git_url,
186
- "archived" => project["archived"].to_s,
187
- "visibility" => project["visibility"] || "internal"
188
- },
206
+ config: child_config,
189
207
  annotations: child_annotations
190
208
  )
191
209
  end