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.
- checksums.yaml +4 -4
- data/Dockerfile +5 -4
- data/README.md +44 -59
- data/chart/archsight/Chart.yaml +6 -0
- data/chart/archsight/README.md +3 -0
- data/chart/archsight/templates/NOTES.txt +22 -0
- data/chart/archsight/templates/_helpers.tpl +62 -0
- data/chart/archsight/templates/deployment.yaml +114 -0
- data/chart/archsight/templates/ingress.yaml +56 -0
- data/chart/archsight/templates/resources-configmap.yaml +10 -0
- data/chart/archsight/templates/resources-pvc.yaml +23 -0
- data/chart/archsight/templates/service.yaml +15 -0
- data/chart/archsight/templates/serviceaccount.yaml +12 -0
- data/chart/archsight/values.yaml +162 -0
- data/docs/architecture.md +39 -0
- data/docs/docker.md +49 -0
- data/{lib/archsight/web/doc → docs}/import.md +10 -2
- data/{lib/archsight/web/doc → docs}/index.md.erb +3 -1
- data/docs/kubernetes.md +149 -0
- data/docs/licenses.md +307 -0
- data/lib/archsight/analysis/executor.rb +0 -10
- data/lib/archsight/annotations/annotation.rb +85 -36
- data/lib/archsight/annotations/architecture_annotations.rb +1 -34
- data/lib/archsight/annotations/computed.rb +1 -1
- data/lib/archsight/annotations/generated_annotations.rb +6 -3
- data/lib/archsight/annotations/git_annotations.rb +8 -4
- data/lib/archsight/annotations/interface_annotations.rb +35 -0
- data/lib/archsight/cli.rb +4 -2
- data/lib/archsight/editor/content_hasher.rb +37 -0
- data/lib/archsight/editor/file_writer.rb +79 -0
- data/lib/archsight/editor.rb +237 -0
- data/lib/archsight/graph.rb +1 -51
- data/lib/archsight/helpers.rb +0 -20
- data/lib/archsight/import/handlers/github.rb +16 -6
- data/lib/archsight/import/handlers/gitlab.rb +28 -10
- data/lib/archsight/import/handlers/repository.rb +56 -6
- data/lib/archsight/import/handlers/rest_api.rb +13 -1
- data/lib/archsight/import/license_analyzer.rb +650 -0
- data/lib/archsight/import/team_matcher.rb +111 -61
- data/lib/archsight/linter.rb +1 -1
- data/lib/archsight/mcp/base.rb +11 -0
- data/lib/archsight/mcp/execute_analysis_tool.rb +100 -0
- data/lib/archsight/mcp.rb +1 -0
- data/lib/archsight/renderer.rb +4 -4
- data/lib/archsight/resources/analysis.rb +1 -17
- data/lib/archsight/resources/application_interface.rb +1 -5
- data/lib/archsight/resources/base.rb +14 -14
- data/lib/archsight/resources/business_actor.rb +18 -3
- data/lib/archsight/resources/technology_artifact.rb +48 -0
- data/lib/archsight/resources/technology_interface.rb +1 -1
- data/lib/archsight/resources/technology_service.rb +5 -0
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/docs.rb +37 -2
- data/lib/archsight/web/api/json_helpers.rb +79 -13
- data/lib/archsight/web/api/openapi/spec.yaml +699 -0
- data/lib/archsight/web/api/routes.rb +23 -0
- data/lib/archsight/web/application.rb +48 -128
- data/lib/archsight/web/editor/form_builder.rb +100 -0
- data/lib/archsight/web/editor/helpers.rb +150 -0
- data/lib/archsight/web/editor/routes.rb +166 -0
- data/lib/archsight/web/public/vue/ApiDocsPage-B1RqTNqh.js +1 -0
- data/lib/archsight/web/public/vue/ApiDocsPage-DhNTOH4o.css +1 -0
- data/lib/archsight/web/public/vue/DocPage-DzwBgBd4.js +1 -0
- data/lib/archsight/web/public/vue/EditorPage-D_miHSv4.js +34 -0
- data/lib/archsight/web/public/vue/EditorPage-Dq0MuTnp.css +1 -0
- data/lib/archsight/web/public/vue/ErrorPage-CQQtPey3.js +2 -0
- data/lib/archsight/web/public/vue/ErrorPage-CwPT3JUr.css +1 -0
- data/lib/archsight/web/public/vue/GraphView-DRcIqAiR.css +1 -0
- data/lib/archsight/web/public/vue/GraphView-T9jFH_qg.js +1 -0
- data/lib/archsight/web/public/vue/InstanceRouter-1Sm-CRhf.js +2 -0
- data/lib/archsight/web/public/vue/InstanceRouter-BJkDRXZY.css +1 -0
- data/lib/archsight/web/public/vue/KindList-JA_L_-Cz.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-8iqavWdg.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-DP-z-j71.css +1 -0
- data/lib/archsight/web/public/vue/SearchResults-BGHbg48-.css +1 -0
- data/lib/archsight/web/public/vue/SearchResults-BdgFeHcm.js +1 -0
- data/lib/archsight/web/public/vue/_basePickBy-CVgieyx-.js +1 -0
- data/lib/archsight/web/public/vue/_baseUniq-BNfrOSaP.js +1 -0
- data/lib/archsight/web/public/vue/architectureDiagram-VXUJARFQ-CJXNpTr5.js +36 -0
- data/lib/archsight/web/public/vue/blockDiagram-VD42YOAC-B5488Hes.js +122 -0
- data/lib/archsight/web/public/vue/c4Diagram-YG6GDRKO-eYY3hprM.js +10 -0
- data/lib/archsight/web/public/vue/chunk-4BX2VUAB-ZoXeL4D1.js +1 -0
- data/lib/archsight/web/public/vue/chunk-55IACEB6-rNtQYnu_.js +1 -0
- data/lib/archsight/web/public/vue/chunk-B4BG7PRW-DolAeVV9.js +165 -0
- data/lib/archsight/web/public/vue/chunk-DI55MBZ5-DnN0f_hj.js +220 -0
- data/lib/archsight/web/public/vue/chunk-FMBD7UC4-BQWOCMuR.js +15 -0
- data/lib/archsight/web/public/vue/chunk-QN33PNHL-DId301Kb.js +1 -0
- data/lib/archsight/web/public/vue/chunk-QZHKN3VN-xbY0NLgv.js +1 -0
- data/lib/archsight/web/public/vue/chunk-TZMSLE5B-CgF9_37b.js +1 -0
- data/lib/archsight/web/public/vue/classDiagram-2ON5EDUG-jGlvI-Za.js +1 -0
- data/lib/archsight/web/public/vue/classDiagram-v2-WZHVMYZB-jGlvI-Za.js +1 -0
- data/lib/archsight/web/public/vue/clone-6iRPe1-W.js +1 -0
- data/lib/archsight/web/public/vue/cose-bilkent-S5V4N54A-CB9Zfu50.js +1 -0
- data/lib/archsight/web/public/vue/cytoscape.esm-5J0xJHOV.js +321 -0
- data/lib/archsight/web/public/vue/dagre-6UL2VRFP-BqkmE-LI.js +4 -0
- data/lib/archsight/web/public/vue/diagram-PSM6KHXK-CKBfqtw3.js +24 -0
- data/lib/archsight/web/public/vue/diagram-QEK2KX5R-B78rOlvK.js +43 -0
- data/lib/archsight/web/public/vue/diagram-S2PKOQOG-BlXC6Cia.js +24 -0
- data/lib/archsight/web/public/vue/erDiagram-Q2GNP2WA-BnliyziJ.js +60 -0
- data/lib/archsight/web/public/vue/flowDiagram-NV44I4VS-wuqPowTd.js +162 -0
- data/lib/archsight/web/public/vue/ganttDiagram-JELNMOA3-GSffAIH3.js +267 -0
- data/lib/archsight/web/public/vue/gitGraphDiagram-V2S2FVAM-OA7VyugW.js +65 -0
- data/lib/archsight/web/public/vue/graph-BXHAtA0S.js +1 -0
- data/lib/archsight/web/public/vue/graphviz-CJms5bxZ.js +13 -0
- data/lib/archsight/web/public/vue/index-DsEsN0_K.js +2 -0
- data/lib/archsight/web/public/vue/index-Tiu4C-Sb.css +1 -0
- data/lib/archsight/web/public/vue/infoDiagram-HS3SLOUP-nlVe2qgv.js +2 -0
- data/lib/archsight/web/public/vue/journeyDiagram-XKPGCS4Q-CtTIcKwf.js +139 -0
- data/lib/archsight/web/public/vue/kanban-definition-3W4ZIXB7-837KX0sW.js +89 -0
- data/lib/archsight/web/public/vue/katex-C-M49wc6.js +261 -0
- data/lib/archsight/web/public/vue/layout-DtE0QdL6.js +1 -0
- data/lib/archsight/web/public/vue/mermaid-DpPHPFQh.js +250 -0
- data/lib/archsight/web/public/vue/mindmap-definition-VGOIOE7T-9gLF2AoY.js +68 -0
- data/lib/archsight/web/public/vue/pieDiagram-ADFJNKIX-CyCNgw3u.js +30 -0
- data/lib/archsight/web/public/vue/quadrantDiagram-AYHSOK5B-CkPh8g02.js +7 -0
- data/lib/archsight/web/public/vue/requirementDiagram-UZGBJVZJ-Dkt6OSlY.js +64 -0
- data/lib/archsight/web/public/vue/sankeyDiagram-TZEHDZUN-BqprTk8x.js +10 -0
- data/lib/archsight/web/public/vue/sequenceDiagram-WL72ISMW-CTmTe1FQ.js +145 -0
- data/lib/archsight/web/public/vue/stateDiagram-FKZM4ZOC-CphqmkEU.js +1 -0
- data/lib/archsight/web/public/vue/stateDiagram-v2-4FDKWEC3-CxaDW5sW.js +1 -0
- data/lib/archsight/web/public/vue/timeline-definition-IT6M3QCI-CSQUZkyE.js +61 -0
- data/lib/archsight/web/public/vue/treemap-GDKQZRPO-DTojm7Yr.js +162 -0
- data/lib/archsight/web/public/{css/graph.css → vue/useGraphviz-A5s4h76R.js} +2 -1
- data/lib/archsight/web/public/vue/useHighlight-C6Kb5G3l.js +10 -0
- data/lib/archsight/web/public/vue/useMermaid-DqxTrLRB.js +1 -0
- data/lib/archsight/web/public/vue/usePanZoom-BybZ_rfh.js +11 -0
- data/lib/archsight/web/public/vue/xychartDiagram-PRI3JC2R-B1ZJZtDC.js +7 -0
- data/lib/archsight/web/public/vue.html +15 -0
- data/media/artifact.jpg +0 -0
- data/media/service.jpg +0 -0
- metadata +104 -77
- data/lib/archsight/web/public/css/artifact.css +0 -995
- data/lib/archsight/web/public/css/base.css +0 -201
- data/lib/archsight/web/public/css/highlight.min.css +0 -10
- data/lib/archsight/web/public/css/iconoir.css +0 -22
- data/lib/archsight/web/public/css/instance.css +0 -818
- data/lib/archsight/web/public/css/layout.css +0 -421
- data/lib/archsight/web/public/css/mermaid-layers.css +0 -188
- data/lib/archsight/web/public/css/pico.min.css +0 -4
- data/lib/archsight/web/public/img/archimate.png +0 -0
- data/lib/archsight/web/public/img/togaf-high-level.png +0 -0
- data/lib/archsight/web/public/js/graph-zoom.js +0 -18
- data/lib/archsight/web/public/js/highlight.min.js +0 -3899
- data/lib/archsight/web/public/js/htmx.min.js +0 -1
- data/lib/archsight/web/public/js/mermaid-init.js +0 -88
- data/lib/archsight/web/public/js/mermaid.min.js +0 -2811
- data/lib/archsight/web/public/js/sparkline.js +0 -42
- data/lib/archsight/web/public/js/svg-pan-zoom.min.js +0 -3
- data/lib/archsight/web/public/js/svg-zoom-controls.js +0 -93
- data/lib/archsight/web/views/api_docs.erb +0 -19
- data/lib/archsight/web/views/index.haml +0 -12
- data/lib/archsight/web/views/partials/artifact/_activity.haml +0 -55
- data/lib/archsight/web/views/partials/artifact/_agentic.haml +0 -25
- data/lib/archsight/web/views/partials/artifact/_deployment.haml +0 -29
- data/lib/archsight/web/views/partials/artifact/_git_info.haml +0 -16
- data/lib/archsight/web/views/partials/artifact/_language_stats.haml +0 -53
- data/lib/archsight/web/views/partials/artifact/_links.haml +0 -24
- data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +0 -32
- data/lib/archsight/web/views/partials/artifact/_repositories.haml +0 -55
- data/lib/archsight/web/views/partials/artifact/_team.haml +0 -83
- data/lib/archsight/web/views/partials/artifact/_workflow.haml +0 -69
- data/lib/archsight/web/views/partials/components/_activity.haml +0 -37
- data/lib/archsight/web/views/partials/components/_git.haml +0 -17
- data/lib/archsight/web/views/partials/components/_jira.haml +0 -18
- data/lib/archsight/web/views/partials/components/_languages.haml +0 -29
- data/lib/archsight/web/views/partials/components/_owner.haml +0 -15
- data/lib/archsight/web/views/partials/components/_repositories.haml +0 -37
- data/lib/archsight/web/views/partials/components/_status.haml +0 -23
- data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +0 -74
- data/lib/archsight/web/views/partials/instance/_analysis_result.haml +0 -64
- data/lib/archsight/web/views/partials/instance/_detail.haml +0 -103
- data/lib/archsight/web/views/partials/instance/_graph.haml +0 -6
- data/lib/archsight/web/views/partials/instance/_import_detail.haml +0 -87
- data/lib/archsight/web/views/partials/instance/_list.haml +0 -84
- data/lib/archsight/web/views/partials/instance/_relations.haml +0 -43
- data/lib/archsight/web/views/partials/instance/_requirements.haml +0 -41
- data/lib/archsight/web/views/partials/instance/_view_detail.haml +0 -57
- data/lib/archsight/web/views/partials/layout/_content.haml +0 -44
- data/lib/archsight/web/views/partials/layout/_error.haml +0 -22
- data/lib/archsight/web/views/partials/layout/_head.haml +0 -24
- data/lib/archsight/web/views/partials/layout/_navigation.haml +0 -21
- data/lib/archsight/web/views/partials/layout/_sidebar.haml +0 -27
- data/lib/archsight/web/views/search.haml +0 -53
- /data/{lib/archsight/web/doc → docs}/archimate.md +0 -0
- /data/{lib/archsight/web/doc → docs}/computed_annotations.md +0 -0
- /data/{lib/archsight/web/doc → docs}/icons.md +0 -0
- /data/{lib/archsight/web/doc → docs}/modeling.md +0 -0
- /data/{lib/archsight/web/doc → docs}/search.md +0 -0
- /data/{lib/archsight/web/doc → docs}/togaf.md +0 -0
- /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
|
data/lib/archsight/graph.rb
CHANGED
|
@@ -3,33 +3,17 @@
|
|
|
3
3
|
module Archsight
|
|
4
4
|
# Graphvis implements a graphviz abstraction
|
|
5
5
|
class Graphvis
|
|
6
|
-
def initialize(name,
|
|
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
|
data/lib/archsight/helpers.rb
CHANGED
|
@@ -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 -
|
|
78
|
-
http.verify_mode = OpenSSL::SSL::
|
|
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
|
-
|
|
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
|
-
|
|
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
|