archsight 0.1.4 → 0.1.5
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/README.md +33 -0
- data/chart/archsight/Chart.yaml +6 -0
- data/chart/archsight/README.md +160 -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/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 +3 -1
- 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/import/handlers/github.rb +14 -6
- data/lib/archsight/import/handlers/gitlab.rb +14 -6
- data/lib/archsight/import/handlers/repository.rb +3 -1
- data/lib/archsight/import/team_matcher.rb +111 -61
- data/lib/archsight/mcp/execute_analysis_tool.rb +100 -0
- data/lib/archsight/mcp.rb +1 -0
- 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 +1 -1
- 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/application.rb +8 -0
- data/lib/archsight/web/doc/import.md +10 -2
- data/lib/archsight/web/editor/form_builder.rb +100 -0
- data/lib/archsight/web/editor/routes.rb +293 -0
- data/lib/archsight/web/public/css/editor.css +863 -0
- data/lib/archsight/web/public/css/instance.css +6 -0
- data/lib/archsight/web/public/js/editor.js +421 -0
- data/lib/archsight/web/public/js/lexical-editor.js +308 -0
- data/lib/archsight/web/views/partials/editor/_field.haml +80 -0
- data/lib/archsight/web/views/partials/editor/_form.haml +131 -0
- data/lib/archsight/web/views/partials/editor/_relations.haml +39 -0
- data/lib/archsight/web/views/partials/editor/_yaml_output.haml +33 -0
- data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +4 -11
- data/lib/archsight/web/views/partials/instance/_detail.haml +4 -0
- data/lib/archsight/web/views/partials/layout/_content.haml +8 -2
- data/lib/archsight/web/views/partials/layout/_head.haml +2 -0
- metadata +26 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
replicaCount: 1
|
|
2
|
+
|
|
3
|
+
image:
|
|
4
|
+
repository: ghcr.io/ionos-cloud/archsight
|
|
5
|
+
pullPolicy: IfNotPresent
|
|
6
|
+
# Overrides the image tag whose default is the chart appVersion.
|
|
7
|
+
tag: ""
|
|
8
|
+
|
|
9
|
+
imagePullSecrets: []
|
|
10
|
+
nameOverride: ""
|
|
11
|
+
fullnameOverride: ""
|
|
12
|
+
|
|
13
|
+
serviceAccount:
|
|
14
|
+
# Specifies whether a service account should be created
|
|
15
|
+
create: true
|
|
16
|
+
# Annotations to add to the service account
|
|
17
|
+
annotations: {}
|
|
18
|
+
# The name of the service account to use.
|
|
19
|
+
# If not set and create is true, a name is generated using the fullname template
|
|
20
|
+
name: ""
|
|
21
|
+
|
|
22
|
+
podAnnotations: {}
|
|
23
|
+
|
|
24
|
+
podSecurityContext: {}
|
|
25
|
+
# fsGroup: 2000
|
|
26
|
+
|
|
27
|
+
securityContext: {}
|
|
28
|
+
# capabilities:
|
|
29
|
+
# drop:
|
|
30
|
+
# - ALL
|
|
31
|
+
# readOnlyRootFilesystem: true
|
|
32
|
+
# runAsNonRoot: true
|
|
33
|
+
# runAsUser: 1000
|
|
34
|
+
|
|
35
|
+
service:
|
|
36
|
+
type: ClusterIP
|
|
37
|
+
port: 80
|
|
38
|
+
targetPort: 4567
|
|
39
|
+
|
|
40
|
+
ingress:
|
|
41
|
+
enabled: false
|
|
42
|
+
className: ""
|
|
43
|
+
annotations: {}
|
|
44
|
+
# kubernetes.io/ingress.class: nginx
|
|
45
|
+
# kubernetes.io/tls-acme: "true"
|
|
46
|
+
hosts:
|
|
47
|
+
- host: chart-example.local
|
|
48
|
+
paths:
|
|
49
|
+
- path: /
|
|
50
|
+
pathType: ImplementationSpecific
|
|
51
|
+
tls: []
|
|
52
|
+
# - secretName: chart-example-tls
|
|
53
|
+
# hosts:
|
|
54
|
+
# - chart-example.local
|
|
55
|
+
|
|
56
|
+
resources: {}
|
|
57
|
+
# limits:
|
|
58
|
+
# cpu: 100m
|
|
59
|
+
# memory: 128Mi
|
|
60
|
+
# requests:
|
|
61
|
+
# cpu: 100m
|
|
62
|
+
# memory: 128Mi
|
|
63
|
+
|
|
64
|
+
autoscaling:
|
|
65
|
+
enabled: false
|
|
66
|
+
minReplicas: 1
|
|
67
|
+
maxReplicas: 100
|
|
68
|
+
targetCPUUtilizationPercentage: 80
|
|
69
|
+
# targetMemoryUtilizationPercentage: 80
|
|
70
|
+
|
|
71
|
+
nodeSelector: {}
|
|
72
|
+
|
|
73
|
+
tolerations: []
|
|
74
|
+
|
|
75
|
+
affinity: {}
|
|
76
|
+
|
|
77
|
+
# Application specific configuration
|
|
78
|
+
app:
|
|
79
|
+
resourcesDir: /resources
|
|
80
|
+
env: production
|
|
81
|
+
|
|
82
|
+
# Strategy for populating the /resources directory
|
|
83
|
+
# Options are "emptyDir", "configMap", "persistence" (PVC), or usage of "extraVolumes"
|
|
84
|
+
content:
|
|
85
|
+
# type: "emptyDir" | "configMap" | "persistence" | "custom"
|
|
86
|
+
# - emptyDir: Simple, ephemeral. Good if using initContainers to fetch data.
|
|
87
|
+
# - configMap: Mounts a ConfigMap. Good for small, static datasets.
|
|
88
|
+
# - persistence: Mounts a PersistentVolumeClaim. Good for large datasets.
|
|
89
|
+
# - custom: Do not create the main volume automatically; rely on extraVolumes/extraVolumeMounts.
|
|
90
|
+
type: emptyDir
|
|
91
|
+
|
|
92
|
+
configMap:
|
|
93
|
+
# If true, creating a ConfigMap from the `data` below.
|
|
94
|
+
create: false
|
|
95
|
+
# Name of the ConfigMap to mount (if create: false) or create (if create: true)
|
|
96
|
+
# If create: true and name is empty, fullname + "-resources" is used.
|
|
97
|
+
name: ""
|
|
98
|
+
# Data to inject into the ConfigMap (filename: content)
|
|
99
|
+
data:
|
|
100
|
+
# sample.yaml: |
|
|
101
|
+
# apiVersion: architecture/v1alpha1
|
|
102
|
+
# kind: Analysis
|
|
103
|
+
# ...
|
|
104
|
+
{}
|
|
105
|
+
|
|
106
|
+
persistence:
|
|
107
|
+
enabled: false
|
|
108
|
+
# existingClaim: my-claim
|
|
109
|
+
storageClass: ""
|
|
110
|
+
accessModes:
|
|
111
|
+
- ReadWriteOnce
|
|
112
|
+
size: 1Gi
|
|
113
|
+
annotations: {}
|
|
114
|
+
|
|
115
|
+
# Add sidecars or initContainers (e.g., for git-sync)
|
|
116
|
+
initContainers: []
|
|
117
|
+
# Example: Fetching from a private repository using a specific Secret
|
|
118
|
+
# - name: git-sync
|
|
119
|
+
# image: alpine/git
|
|
120
|
+
# env:
|
|
121
|
+
# - name: GIT_TOKEN
|
|
122
|
+
# valueFrom:
|
|
123
|
+
# secretKeyRef:
|
|
124
|
+
# name: my-git-credentials
|
|
125
|
+
# key: token
|
|
126
|
+
# command: ["/bin/sh", "-c"]
|
|
127
|
+
# args:
|
|
128
|
+
# - "git clone https://oauth2:$GIT_TOKEN@github.com/my-org/my-private-repo.git /resources"
|
|
129
|
+
# volumeMounts:
|
|
130
|
+
# - name: resources
|
|
131
|
+
# mountPath: /resources
|
|
132
|
+
|
|
133
|
+
sidecars: []
|
|
134
|
+
# Example: Periodically update resources from a private repository
|
|
135
|
+
# - name: git-sync-private
|
|
136
|
+
# image: alpine/git
|
|
137
|
+
# env:
|
|
138
|
+
# - name: GIT_TOKEN
|
|
139
|
+
# valueFrom:
|
|
140
|
+
# secretKeyRef:
|
|
141
|
+
# name: my-git-credentials
|
|
142
|
+
# key: token
|
|
143
|
+
# command: ["/bin/sh", "-c"]
|
|
144
|
+
# args:
|
|
145
|
+
# - |
|
|
146
|
+
# cd /resources
|
|
147
|
+
# # Ensure the remote uses the current token
|
|
148
|
+
# git remote set-url origin "https://oauth2:${GIT_TOKEN}@github.com/my-org/my-private-repo.git"
|
|
149
|
+
# while true; do
|
|
150
|
+
# git pull
|
|
151
|
+
# sleep 60
|
|
152
|
+
# done
|
|
153
|
+
# volumeMounts:
|
|
154
|
+
# - name: resources
|
|
155
|
+
# mountPath: /resources
|
|
156
|
+
|
|
157
|
+
# Custom volumes and mounts if you need more control
|
|
158
|
+
extraVolumes: []
|
|
159
|
+
extraVolumeMounts: []
|
|
160
|
+
|
|
161
|
+
# Command arguments (passed to ENTRYPOINT ["archsight"])
|
|
162
|
+
args: ["web", "--port", "4567", "-H", "0.0.0.0", "--production"]
|
|
@@ -79,9 +79,6 @@ module Archsight
|
|
|
79
79
|
# Filter by name pattern if provided
|
|
80
80
|
analyses = analyses.select { |a| filter.match?(a.name) } if filter
|
|
81
81
|
|
|
82
|
-
# Filter to enabled analyses
|
|
83
|
-
analyses = analyses.select { |a| analysis_enabled?(a) }
|
|
84
|
-
|
|
85
82
|
analyses.map { |analysis| execute(analysis) }
|
|
86
83
|
end
|
|
87
84
|
|
|
@@ -100,13 +97,6 @@ module Archsight
|
|
|
100
97
|
|
|
101
98
|
DEFAULT_TIMEOUT
|
|
102
99
|
end
|
|
103
|
-
|
|
104
|
-
# Check if analysis is enabled
|
|
105
|
-
# @param analysis [Object] Analysis instance
|
|
106
|
-
# @return [Boolean] true if enabled
|
|
107
|
-
def analysis_enabled?(analysis)
|
|
108
|
-
analysis.annotations["analysis/enabled"] != "false"
|
|
109
|
-
end
|
|
110
100
|
end
|
|
111
101
|
end
|
|
112
102
|
end
|
|
@@ -4,7 +4,7 @@ require_relative "email_recipient"
|
|
|
4
4
|
|
|
5
5
|
# Annotation represents a single annotation definition with its schema and behavior
|
|
6
6
|
class Archsight::Annotations::Annotation
|
|
7
|
-
attr_reader :key, :description, :filter, :format, :enum, :sidebar, :type, :list
|
|
7
|
+
attr_reader :key, :description, :filter, :format, :enum, :sidebar, :type, :list, :editor
|
|
8
8
|
|
|
9
9
|
def initialize(key, options = {})
|
|
10
10
|
@key = key
|
|
@@ -14,6 +14,7 @@ class Archsight::Annotations::Annotation
|
|
|
14
14
|
@enum = options[:enum]
|
|
15
15
|
@sidebar = options.fetch(:sidebar, true)
|
|
16
16
|
@list = options.fetch(:list, false)
|
|
17
|
+
@editor = options.fetch(:editor, true)
|
|
17
18
|
@type = options[:type]
|
|
18
19
|
|
|
19
20
|
# Auto-add filter if enum present
|
|
@@ -80,43 +81,12 @@ class Archsight::Annotations::Annotation
|
|
|
80
81
|
|
|
81
82
|
# Validate a value and return array of error messages (empty if valid)
|
|
82
83
|
def validate(value)
|
|
83
|
-
errors = []
|
|
84
|
+
errors = [] #: Array[String]
|
|
84
85
|
return errors if value.nil?
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
if
|
|
88
|
-
|
|
89
|
-
invalid_values = values.reject { |v| @enum.include?(v) }
|
|
90
|
-
invalid_values.each do |v|
|
|
91
|
-
errors << "invalid value '#{v}'. Expected one of: #{@enum.join(", ")}"
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Check type constraint
|
|
96
|
-
if @type.is_a?(Class) && errors.empty?
|
|
97
|
-
values_to_check = list? ? value.to_s.split(/,|\n/).map(&:strip).reject(&:empty?) : [value.to_s]
|
|
98
|
-
|
|
99
|
-
values_to_check.each do |string_value|
|
|
100
|
-
valid = case @type.to_s
|
|
101
|
-
when "Integer"
|
|
102
|
-
string_value.match?(/\A-?\d+\z/)
|
|
103
|
-
when "Float"
|
|
104
|
-
string_value.match?(/\A-?\d+(\.\d+)?\z/)
|
|
105
|
-
when "URI"
|
|
106
|
-
begin
|
|
107
|
-
URI.parse(string_value)
|
|
108
|
-
string_value.match?(%r{\Ahttps?://})
|
|
109
|
-
rescue URI::InvalidURIError
|
|
110
|
-
false
|
|
111
|
-
end
|
|
112
|
-
when "Archsight::Annotations::EmailRecipient"
|
|
113
|
-
Archsight::Annotations::EmailRecipient.valid?(string_value)
|
|
114
|
-
else
|
|
115
|
-
true
|
|
116
|
-
end
|
|
117
|
-
errors << "invalid value '#{string_value}'. #{type_error_message}" unless valid
|
|
118
|
-
end
|
|
119
|
-
end
|
|
87
|
+
validate_enum(value, errors)
|
|
88
|
+
validate_type(value, errors) if errors.empty?
|
|
89
|
+
validate_code(value, errors) if errors.empty?
|
|
120
90
|
|
|
121
91
|
errors
|
|
122
92
|
end
|
|
@@ -130,6 +100,18 @@ class Archsight::Annotations::Annotation
|
|
|
130
100
|
@format == :markdown
|
|
131
101
|
end
|
|
132
102
|
|
|
103
|
+
def code?
|
|
104
|
+
@format == :ruby
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def multiline?
|
|
108
|
+
@format == :multiline
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def code_language
|
|
112
|
+
@format if code?
|
|
113
|
+
end
|
|
114
|
+
|
|
133
115
|
# Example value for templates
|
|
134
116
|
def example_value
|
|
135
117
|
if @enum
|
|
@@ -155,6 +137,49 @@ class Archsight::Annotations::Annotation
|
|
|
155
137
|
end
|
|
156
138
|
end
|
|
157
139
|
|
|
140
|
+
def validate_enum(value, errors)
|
|
141
|
+
return unless @enum
|
|
142
|
+
|
|
143
|
+
values = list? ? value.to_s.split(",").map(&:strip) : [value.to_s]
|
|
144
|
+
invalid_values = values.reject { |v| @enum.include?(v) }
|
|
145
|
+
invalid_values.each do |v|
|
|
146
|
+
errors << "invalid value '#{v}'. Expected one of: #{@enum.join(", ")}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def validate_type(value, errors)
|
|
151
|
+
return unless @type.is_a?(Class)
|
|
152
|
+
|
|
153
|
+
values_to_check = list? ? value.to_s.split(/,|\n/).map(&:strip).reject(&:empty?) : [value.to_s]
|
|
154
|
+
values_to_check.each do |string_value|
|
|
155
|
+
errors << "invalid value '#{string_value}'. #{type_error_message}" unless valid_type_value?(string_value)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def valid_type_value?(string_value)
|
|
160
|
+
case @type.to_s
|
|
161
|
+
when "Integer" then string_value.match?(/\A-?\d+\z/)
|
|
162
|
+
when "Float" then string_value.match?(/\A-?\d+(\.\d+)?\z/)
|
|
163
|
+
when "URI" then valid_uri?(string_value)
|
|
164
|
+
when "Archsight::Annotations::EmailRecipient" then Archsight::Annotations::EmailRecipient.valid?(string_value)
|
|
165
|
+
else true
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def valid_uri?(string_value)
|
|
170
|
+
URI.parse(string_value)
|
|
171
|
+
string_value.match?(%r{\Ahttps?://})
|
|
172
|
+
rescue URI::InvalidURIError
|
|
173
|
+
false
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def validate_code(value, errors)
|
|
177
|
+
return unless code? && !value.to_s.strip.empty?
|
|
178
|
+
|
|
179
|
+
syntax_error = validate_code_syntax(value.to_s)
|
|
180
|
+
errors << syntax_error if syntax_error
|
|
181
|
+
end
|
|
182
|
+
|
|
158
183
|
def derive_format
|
|
159
184
|
case @filter
|
|
160
185
|
when :word then :tag_word
|
|
@@ -165,4 +190,28 @@ class Archsight::Annotations::Annotation
|
|
|
165
190
|
def build_regex
|
|
166
191
|
Regexp.new("^#{Regexp.escape(key).gsub('\*', ".+")}$")
|
|
167
192
|
end
|
|
193
|
+
|
|
194
|
+
# Validate code syntax based on format
|
|
195
|
+
# @param code [String] The code to validate
|
|
196
|
+
# @return [String, nil] Error message or nil if valid
|
|
197
|
+
def validate_code_syntax(code)
|
|
198
|
+
case @format
|
|
199
|
+
when :ruby
|
|
200
|
+
validate_ruby_syntax(code)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Validate Ruby syntax using RubyVM
|
|
205
|
+
# @param code [String] Ruby code to validate
|
|
206
|
+
# @return [String, nil] Error message or nil if valid
|
|
207
|
+
def validate_ruby_syntax(code)
|
|
208
|
+
# steep:ignore:start
|
|
209
|
+
RubyVM::InstructionSequence.compile(code)
|
|
210
|
+
# steep:ignore:end
|
|
211
|
+
nil
|
|
212
|
+
rescue SyntaxError => e
|
|
213
|
+
# Extract just the error message without the full backtrace
|
|
214
|
+
message = e.message.lines.first&.strip || "Syntax error"
|
|
215
|
+
"Ruby syntax error: #{message}"
|
|
216
|
+
end
|
|
168
217
|
end
|
|
@@ -9,12 +9,8 @@ module Archsight::Annotations::Architecture
|
|
|
9
9
|
annotation "architecture/abbr",
|
|
10
10
|
description: "Abbreviation or short name",
|
|
11
11
|
title: "Abbreviation"
|
|
12
|
-
annotation "architecture/evidence",
|
|
13
|
-
description: "Supporting evidence or notes",
|
|
14
|
-
title: "Evidence",
|
|
15
|
-
format: :markdown
|
|
16
12
|
annotation "architecture/description",
|
|
17
|
-
description: "Textual description of the
|
|
13
|
+
description: "Textual description of the resource",
|
|
18
14
|
title: "Description",
|
|
19
15
|
format: :markdown
|
|
20
16
|
annotation "architecture/documentation",
|
|
@@ -25,35 +21,6 @@ module Archsight::Annotations::Architecture
|
|
|
25
21
|
description: "Comma-separated tags",
|
|
26
22
|
filter: :list,
|
|
27
23
|
title: "Tags"
|
|
28
|
-
annotation "architecture/encoding",
|
|
29
|
-
description: "Data encoding format",
|
|
30
|
-
filter: :list,
|
|
31
|
-
title: "Encoding"
|
|
32
|
-
annotation "architecture/title",
|
|
33
|
-
description: "Interface title",
|
|
34
|
-
title: "Title"
|
|
35
|
-
annotation "architecture/openapi",
|
|
36
|
-
description: "OpenAPI specification version",
|
|
37
|
-
filter: :word,
|
|
38
|
-
title: "OpenAPI"
|
|
39
|
-
annotation "architecture/version",
|
|
40
|
-
description: "API or interface version",
|
|
41
|
-
filter: :word,
|
|
42
|
-
title: "Version",
|
|
43
|
-
sidebar: false
|
|
44
|
-
annotation "architecture/status",
|
|
45
|
-
description: "Lifecycle status (General-Availability, Early-Access, Development)",
|
|
46
|
-
filter: :word,
|
|
47
|
-
title: "Status"
|
|
48
|
-
annotation "architecture/visibility",
|
|
49
|
-
description: "API visibility (public, private, internal)",
|
|
50
|
-
filter: :word,
|
|
51
|
-
enum: %w[public private internal],
|
|
52
|
-
title: "Visibility"
|
|
53
|
-
annotation "architecture/applicationSets",
|
|
54
|
-
description: "Related ArgoCD ApplicationSets",
|
|
55
|
-
title: "ApplicationSets",
|
|
56
|
-
format: :markdown
|
|
57
24
|
end
|
|
58
25
|
end
|
|
59
26
|
end
|
|
@@ -212,7 +212,7 @@ class Archsight::Annotations::ComputedManager
|
|
|
212
212
|
@computing.add(cache_key)
|
|
213
213
|
begin
|
|
214
214
|
evaluator = Archsight::Annotations::ComputedEvaluator.new(instance, @database, self)
|
|
215
|
-
value = evaluator.instance_eval(&definition.block)
|
|
215
|
+
value = evaluator.instance_eval(&definition.block) # steep:ignore BlockTypeMismatch
|
|
216
216
|
|
|
217
217
|
# Apply type coercion if specified
|
|
218
218
|
value = coerce_value(value, definition.type) if definition.type
|
|
@@ -7,15 +7,18 @@ module Archsight::Annotations::Generated
|
|
|
7
7
|
annotation "generated/script",
|
|
8
8
|
description: "Name of the script that generated this resource",
|
|
9
9
|
title: "Generated By",
|
|
10
|
-
sidebar: false
|
|
10
|
+
sidebar: false,
|
|
11
|
+
editor: false
|
|
11
12
|
annotation "generated/at",
|
|
12
13
|
description: "Timestamp when this resource was generated (ISO8601)",
|
|
13
14
|
title: "Generated At",
|
|
14
|
-
sidebar: false
|
|
15
|
+
sidebar: false,
|
|
16
|
+
editor: false
|
|
15
17
|
annotation "generated/configHash",
|
|
16
18
|
description: "Hash of configuration used to generate this resource (for change detection)",
|
|
17
19
|
title: "Config Hash",
|
|
18
|
-
sidebar: false
|
|
20
|
+
sidebar: false,
|
|
21
|
+
editor: false
|
|
19
22
|
end
|
|
20
23
|
end
|
|
21
24
|
end
|
|
@@ -6,16 +6,20 @@ module Archsight::Annotations::Git
|
|
|
6
6
|
base.class_eval do
|
|
7
7
|
annotation "git/updatedAt",
|
|
8
8
|
description: "Date when the resource was last updated",
|
|
9
|
-
title: "Updated At"
|
|
9
|
+
title: "Updated At",
|
|
10
|
+
editor: false
|
|
10
11
|
annotation "git/updatedBy",
|
|
11
12
|
description: "Email of person who last updated the resource",
|
|
12
|
-
title: "Updated By"
|
|
13
|
+
title: "Updated By",
|
|
14
|
+
editor: false
|
|
13
15
|
annotation "git/reviewedAt",
|
|
14
16
|
description: "Date when the resource was last reviewed",
|
|
15
|
-
title: "Reviewed At"
|
|
17
|
+
title: "Reviewed At",
|
|
18
|
+
editor: false
|
|
16
19
|
annotation "git/reviewedBy",
|
|
17
20
|
description: "Email of person who last reviewed the resource",
|
|
18
|
-
title: "Reviewed By"
|
|
21
|
+
title: "Reviewed By",
|
|
22
|
+
editor: false
|
|
19
23
|
end
|
|
20
24
|
end
|
|
21
25
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Interface module adds interface-specific annotations to resource classes
|
|
4
|
+
module Archsight::Annotations::Interface
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.class_eval do
|
|
7
|
+
annotation "architecture/encoding",
|
|
8
|
+
description: "Data encoding format",
|
|
9
|
+
filter: :list,
|
|
10
|
+
title: "Encoding"
|
|
11
|
+
annotation "architecture/title",
|
|
12
|
+
description: "Interface title",
|
|
13
|
+
title: "Title"
|
|
14
|
+
annotation "architecture/openapi",
|
|
15
|
+
description: "OpenAPI specification version",
|
|
16
|
+
filter: :word,
|
|
17
|
+
title: "OpenAPI"
|
|
18
|
+
annotation "architecture/version",
|
|
19
|
+
description: "API or interface version",
|
|
20
|
+
filter: :word,
|
|
21
|
+
title: "Version",
|
|
22
|
+
sidebar: false
|
|
23
|
+
annotation "architecture/status",
|
|
24
|
+
description: "Lifecycle status",
|
|
25
|
+
filter: :word,
|
|
26
|
+
enum: %w[General-Availability Early-Access Development],
|
|
27
|
+
title: "Status"
|
|
28
|
+
annotation "architecture/visibility",
|
|
29
|
+
description: "API visibility (public, private, internal)",
|
|
30
|
+
filter: :word,
|
|
31
|
+
enum: %w[public private internal],
|
|
32
|
+
title: "Visibility"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/archsight/cli.rb
CHANGED
|
@@ -19,6 +19,7 @@ module Archsight
|
|
|
19
19
|
option :production, type: :boolean, default: false, desc: "Run in production mode"
|
|
20
20
|
option :disable_reload, type: :boolean, default: false, desc: "Disable the reload button in the UI"
|
|
21
21
|
option :enable_logging, type: :boolean, default: nil, desc: "Enable request logging (default: false in dev, true in prod)"
|
|
22
|
+
option :inline_edit, type: :boolean, default: false, desc: "Enable inline editing (save directly to source files)"
|
|
22
23
|
def web
|
|
23
24
|
configure_resources
|
|
24
25
|
require "archsight/web/application"
|
|
@@ -26,6 +27,7 @@ module Archsight
|
|
|
26
27
|
env = options[:production] ? :production : :development
|
|
27
28
|
Archsight::Web::Application.configure_environment!(env, logging: options[:enable_logging])
|
|
28
29
|
Archsight::Web::Application.set :reload_enabled, !options[:disable_reload]
|
|
30
|
+
Archsight::Web::Application.set :inline_edit_enabled, options[:inline_edit]
|
|
29
31
|
Archsight::Web::Application.setup_mcp!
|
|
30
32
|
Archsight::Web::Application.run!(port: options[:port], bind: options[:host])
|
|
31
33
|
rescue Archsight::ResourceError => e
|
|
@@ -191,7 +193,7 @@ module Archsight
|
|
|
191
193
|
def filter_analyses(db)
|
|
192
194
|
analyses = db.instances_by_kind("Analysis").values
|
|
193
195
|
analyses = analyses.select { |a| Regexp.new(options[:filter], Regexp::IGNORECASE).match?(a.name) } if options[:filter]
|
|
194
|
-
analyses
|
|
196
|
+
analyses
|
|
195
197
|
end
|
|
196
198
|
|
|
197
199
|
def print_analysis_dry_run(analyses)
|
|
@@ -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
|