archsight 0.3.3 → 0.4.1
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 +7 -1
- data/docs/archimate.md +21 -1
- data/docs/configuration.md +1 -0
- data/docs/diagram.md +15 -3
- data/docs/docker.md +4 -1
- data/docs/icons.md +8 -2
- data/docs/import.md +178 -55
- data/docs/index.md.erb +24 -18
- data/docs/modeling.md +645 -34
- data/docs/pages.md +46 -15
- data/docs/search.md +21 -1
- data/docs/togaf.md +2 -1
- data/lib/archsight/analysis/result.rb +8 -2
- data/lib/archsight/analysis/sandbox.rb +8 -0
- data/lib/archsight/analysis/typst_renderer.rb +245 -0
- data/lib/archsight/analysis.rb +1 -0
- data/lib/archsight/annotations/annotation.rb +30 -3
- data/lib/archsight/annotations/architecture_annotations.rb +47 -0
- data/lib/archsight/annotations/link_annotations.rb +15 -0
- data/lib/archsight/annotations/resource_reference.rb +12 -0
- data/lib/archsight/cli.rb +124 -23
- data/lib/archsight/database.rb +40 -0
- data/lib/archsight/diagram/cli.rb +50 -2
- data/lib/archsight/diagram/formatter.rb +112 -0
- data/lib/archsight/diagram/routing/edge_router/orthogonal_path.rb +38 -10
- data/lib/archsight/diagram/routing/edge_router.rb +29 -4
- data/lib/archsight/diagram/routing/edge_routing.rb +61 -5
- data/lib/archsight/diagram/style/theme.rb +7 -2
- data/lib/archsight/documentation.rb +4 -4
- data/lib/archsight/editor/page_source.rb +13 -1
- data/lib/archsight/export/confluence/analysis_output.rb +100 -0
- data/lib/archsight/export/confluence/client.rb +13 -0
- data/lib/archsight/export/confluence/diagram_links.rb +1 -1
- data/lib/archsight/export/confluence/exporter.rb +43 -14
- data/lib/archsight/export/confluence/labels.rb +25 -0
- data/lib/archsight/export/confluence/page_properties.rb +6 -9
- data/lib/archsight/export/confluence/storage.rb +63 -14
- data/lib/archsight/export/confluence/tables.rb +2 -3
- data/lib/archsight/git_source.rb +81 -0
- data/lib/archsight/helpers/asset_images.rb +3 -4
- data/lib/archsight/helpers/embeds.rb +18 -7
- data/lib/archsight/helpers/resource_resolver.rb +25 -0
- data/lib/archsight/import/executor.rb +140 -16
- data/lib/archsight/import/handler.rb +55 -6
- data/lib/archsight/import/handler_loader.rb +64 -0
- data/lib/archsight/import/handlers/cache_pruner.rb +30 -7
- data/lib/archsight/import/handlers/command.rb +120 -0
- data/lib/archsight/import/handlers/github.rb +4 -1
- data/lib/archsight/import/handlers/gitlab.rb +4 -1
- data/lib/archsight/import/handlers/go_dep_resolver.rb +10 -2
- data/lib/archsight/import/handlers/go_grapher.rb +102 -5
- data/lib/archsight/import/handlers/go_module_parser.rb +88 -0
- data/lib/archsight/import/handlers/grapher.rb +40 -1
- data/lib/archsight/import/handlers/repository.rb +3 -0
- data/lib/archsight/import/handlers/rest_api_index.rb +3 -0
- data/lib/archsight/import/registry.rb +22 -1
- data/lib/archsight/import.rb +89 -1
- data/lib/archsight/linter/structure.rb +239 -0
- data/lib/archsight/linter.rb +123 -16
- data/lib/archsight/markdown.rb +48 -0
- data/lib/archsight/openapi.rb +169 -0
- data/lib/archsight/page_loader.rb +12 -1
- data/lib/archsight/page_tree.rb +2 -3
- data/lib/archsight/query/evaluator.rb +13 -6
- data/lib/archsight/query/parser.rb +73 -6
- data/lib/archsight/query.rb +1 -1
- data/lib/archsight/references.rb +3 -3
- data/lib/archsight/renderer.rb +80 -23
- data/lib/archsight/resources/analysis.rb +3 -2
- data/lib/archsight/resources/application_component.rb +75 -7
- data/lib/archsight/resources/application_interface.rb +12 -0
- data/lib/archsight/resources/application_service.rb +12 -1
- data/lib/archsight/resources/base.rb +58 -5
- data/lib/archsight/resources/business_actor.rb +5 -0
- data/lib/archsight/resources/business_contract.rb +72 -0
- data/lib/archsight/resources/business_product.rb +12 -0
- data/lib/archsight/resources/business_role.rb +1 -0
- data/lib/archsight/resources/implementation_deliverable.rb +1 -0
- data/lib/archsight/resources/implementation_plateau.rb +8 -2
- data/lib/archsight/resources/implementation_work_package.rb +2 -1
- data/lib/archsight/resources/import.rb +37 -11
- data/lib/archsight/resources/location.rb +56 -0
- data/lib/archsight/resources/motivation_requirement.rb +6 -0
- data/lib/archsight/resources/motivation_stakeholder.rb +14 -0
- data/lib/archsight/resources/page.rb +4 -7
- data/lib/archsight/resources/strategy_capability.rb +9 -0
- data/lib/archsight/resources/technology_artifact.rb +11 -4
- data/lib/archsight/resources/technology_communication_network.rb +91 -0
- data/lib/archsight/resources/technology_device.rb +88 -0
- data/lib/archsight/resources/technology_equipment.rb +75 -0
- data/lib/archsight/resources/technology_facility.rb +70 -0
- data/lib/archsight/resources/technology_node.rb +50 -0
- data/lib/archsight/resources/technology_service.rb +33 -0
- data/lib/archsight/resources/technology_system_software.rb +28 -0
- data/lib/archsight/resources.rb +12 -0
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/analysis_helpers.rb +55 -0
- data/lib/archsight/web/api/docs.rb +2 -1
- data/lib/archsight/web/api/json_helpers.rb +3 -37
- data/lib/archsight/web/api/openapi/spec.yaml +146 -35
- data/lib/archsight/web/api/page_helpers.rb +1 -8
- data/lib/archsight/web/api/routes.rb +9 -4
- data/lib/archsight/web/application.rb +40 -4
- data/lib/archsight/web/editor/routes.rb +2 -1
- data/lib/archsight/web/public/vendor/redoc/LICENSE +22 -0
- data/lib/archsight/web/public/vendor/redoc/VERSION +1 -0
- data/lib/archsight/web/public/vendor/redoc/api.html +27 -0
- data/lib/archsight/web/public/vendor/redoc/api.js +19 -0
- data/lib/archsight/web/public/vendor/redoc/redoc.standalone.js +1851 -0
- data/lib/archsight/web/public/vendor/redoc/redoc.standalone.js.LICENSE.txt +102 -0
- data/lib/archsight/web/public/vue/DocPage-C2ubTXH7.js +1 -0
- data/lib/archsight/web/public/vue/EditorPage-Dso6PLMn.js +35 -0
- data/lib/archsight/web/public/vue/ErrorPage-DnZ6YJLi.js +2 -0
- data/lib/archsight/web/public/vue/GraphView-CTl03Yre.js +1 -0
- data/lib/archsight/web/public/vue/HomePage-C339nihQ.js +2 -0
- data/lib/archsight/web/public/vue/InstanceRouter-DFW7tOrD.js +1 -0
- data/lib/archsight/web/public/vue/InstanceRouter-ZaCcjlIX.css +1 -0
- data/lib/archsight/web/public/vue/KindList-C0oaM3CF.js +1 -0
- data/lib/archsight/web/public/vue/KindList-yYcRT2BV.css +1 -0
- data/lib/archsight/web/public/vue/PageView-B93woeN3.css +1 -0
- data/lib/archsight/web/public/vue/PageView-OZB6G9Wu.js +1 -0
- data/lib/archsight/web/public/vue/QueryError-C2xLkCEm.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-D75O87zG.css +1 -0
- data/lib/archsight/web/public/vue/ResourceList-DooWFwQP.js +2 -0
- data/lib/archsight/web/public/vue/SearchResults-BK-HjFnv.js +1 -0
- data/lib/archsight/web/public/vue/{SearchResults-BewvsfOc.css → SearchResults-BV6UHriJ.css} +1 -1
- data/lib/archsight/web/public/vue/WikiPage-B2FYmeHL.js +3 -0
- data/lib/archsight/web/public/vue/WikiPage-TpmM1VAF.css +1 -0
- data/lib/archsight/web/public/vue/architecture-WOLXFQ4H-BGN-TV5r.js +1 -0
- data/lib/archsight/web/public/vue/cynefin-EF2NZ3EQ-CZd6g_tq.js +1 -0
- data/lib/archsight/web/public/vue/eventmodeling-K75KTNOO-CYnbEx8O.js +1 -0
- data/lib/archsight/web/public/vue/gitGraph-VSP46ZUC-DialfQSk.js +1 -0
- data/lib/archsight/web/public/vue/graphviz-1lWBhpEX.js +3 -0
- data/lib/archsight/web/public/vue/index-BLSE5DSv.css +1 -0
- data/lib/archsight/web/public/vue/index-CLuLTE5I.js +3 -0
- data/lib/archsight/web/public/vue/info-OHQRW6UA-BuhOW7ss.js +1 -0
- data/lib/archsight/web/public/vue/katex-B_JcIwqj.js +257 -0
- data/lib/archsight/web/public/vue/mermaid-C3bOamfn.js +3999 -0
- data/lib/archsight/web/public/vue/packet-JDAUHWVQ-CUeRz7w4.js +1 -0
- data/lib/archsight/web/public/vue/pie-XZMESJXO-DnuewJI6.js +1 -0
- data/lib/archsight/web/public/vue/radar-ABXABTNO-Bi9l8bcv.js +1 -0
- data/lib/archsight/web/public/vue/railroad-I3PHUGI6-DSlxFbxk.js +1 -0
- data/lib/archsight/web/public/vue/railroad-abnf-LNEFI6M7-D6Kqu8wu.js +1 -0
- data/lib/archsight/web/public/vue/railroad-ebnf-DWYD2UWJ-MR7uCYT0.js +1 -0
- data/lib/archsight/web/public/vue/railroad-peg-JR7BVNR7-BL7aKIc-.js +1 -0
- data/lib/archsight/web/public/vue/rolldown-runtime-COiEfhsA.js +1 -0
- data/lib/archsight/web/public/vue/treeView-D4JQ5SDB-CamtB04D.js +1 -0
- data/lib/archsight/web/public/vue/treemap-SAJKECNS-DeQxu_HM.js +1 -0
- data/lib/archsight/web/public/vue/{useGraphviz-DlSnFeCL.js → useGraphviz-Dnm2pkon.js} +2 -2
- data/lib/archsight/web/public/vue/useHighlight-BH32C7yt.js +10 -0
- data/lib/archsight/web/public/vue/usePanZoom-HxgEsqq2.js +12 -0
- data/lib/archsight/web/public/vue/wardley-7MLQ67FV-BvKIB6Tv.js +1 -0
- data/lib/archsight/web/public/vue.html +5 -5
- metadata +68 -41
- data/lib/archsight/web/public/vue/ApiDocsPage-BgqnQgwa.js +0 -1
- data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +0 -1
- data/lib/archsight/web/public/vue/DocPage-CNO71nBH.js +0 -1
- data/lib/archsight/web/public/vue/EditorPage-DR0FCNTz.js +0 -35
- data/lib/archsight/web/public/vue/ErrorPage-BfYArv6s.js +0 -2
- data/lib/archsight/web/public/vue/GraphView-BVTW30Ak.js +0 -1
- data/lib/archsight/web/public/vue/HomePage-Wk9P4Pma.js +0 -2
- data/lib/archsight/web/public/vue/InstanceRouter-BV8ycydz.js +0 -1
- data/lib/archsight/web/public/vue/InstanceRouter-jeavM6PW.css +0 -1
- data/lib/archsight/web/public/vue/KindList-C8yMsN5J.js +0 -1
- data/lib/archsight/web/public/vue/PageView-BaN6TyJB.js +0 -1
- data/lib/archsight/web/public/vue/QueryError-D790wH-a.js +0 -1
- data/lib/archsight/web/public/vue/ResourceList-B0FLaFR3.css +0 -1
- data/lib/archsight/web/public/vue/ResourceList-D-66nas2.js +0 -2
- data/lib/archsight/web/public/vue/SearchResults-CHyqIerQ.js +0 -1
- data/lib/archsight/web/public/vue/WikiPage-D2svpk6B.js +0 -13
- data/lib/archsight/web/public/vue/WikiPage-DoFJ0dbW.css +0 -1
- data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BfA1TPxQ.js +0 -1
- data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-BmgdUKVD.js +0 -1
- data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-DbXDvAWF.js +0 -1
- data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-BkVhrKS1.js +0 -1
- data/lib/archsight/web/public/vue/graphviz-SXWTgZH5.js +0 -3
- data/lib/archsight/web/public/vue/index-BXXkUK1V.js +0 -3
- data/lib/archsight/web/public/vue/index-Cov1SnzY.css +0 -1
- data/lib/archsight/web/public/vue/info-A6RAGUB7-BIlCVuUY.js +0 -1
- data/lib/archsight/web/public/vue/katex-CXMH3UgJ.js +0 -257
- data/lib/archsight/web/public/vue/mermaid-BjEi5URd.js +0 -3334
- data/lib/archsight/web/public/vue/packet-AYTQ26CC-eoQTjY3j.js +0 -1
- data/lib/archsight/web/public/vue/pie-WAS4IAKB-DEOotGhh.js +0 -1
- data/lib/archsight/web/public/vue/radar-RG4KPBEZ-C2yucuUN.js +0 -1
- data/lib/archsight/web/public/vue/railroad-74A4TZTK-CzONK5VY.js +0 -1
- data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-9zRPsDsx.js +0 -1
- data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-BJ4dkz9l.js +0 -1
- data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DFy_rqAj.js +0 -1
- data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-2cq5CyD2.js +0 -1
- data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DK-e9Ez4.js +0 -1
- data/lib/archsight/web/public/vue/useHighlight-D5Gf8DLl.js +0 -10
- data/lib/archsight/web/public/vue/wardley-WFR3VGLG-BQwqUqfB.js +0 -1
|
@@ -35,6 +35,11 @@ class Archsight::Import::Handlers::Grapher < Archsight::Import::Handler
|
|
|
35
35
|
[]
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Namespaces of the machine tags (namespace:value) that importers write into architecture/tags.
|
|
39
|
+
# `entrypoint` is no longer written (see component/entrypoints) but stays listed so a re-import
|
|
40
|
+
# removes the tags of earlier runs.
|
|
41
|
+
MACHINE_TAG_NAMESPACES = %w[ecosystem packaging entrypoint linkage].freeze
|
|
42
|
+
|
|
38
43
|
PALETTE = [
|
|
39
44
|
{ fill: "#ddeeff", edge: "#2266cc" },
|
|
40
45
|
{ fill: "#ddffd8", edge: "#2a8a1e" },
|
|
@@ -51,7 +56,13 @@ class Archsight::Import::Handlers::Grapher < Archsight::Import::Handler
|
|
|
51
56
|
def execute
|
|
52
57
|
@path = config_path("path")
|
|
53
58
|
raise "Missing required config: path" unless @path
|
|
54
|
-
|
|
59
|
+
|
|
60
|
+
unless File.directory?(@path)
|
|
61
|
+
# The clone is the repository import's job (it reports its own failure);
|
|
62
|
+
# no marker is written, so this runs again once the clone exists.
|
|
63
|
+
progress.warn("Skipping, repository not cloned: #{@path}")
|
|
64
|
+
return
|
|
65
|
+
end
|
|
55
66
|
|
|
56
67
|
# Consolidate all grapher output into a single shared file unless an
|
|
57
68
|
# explicit output path is already set (e.g. via grapherOutputPath on the
|
|
@@ -123,6 +134,34 @@ class Archsight::Import::Handlers::Grapher < Archsight::Import::Handler
|
|
|
123
134
|
raise NotImplementedError, "#{self.class}#collect_packages must be implemented"
|
|
124
135
|
end
|
|
125
136
|
|
|
137
|
+
# Hook: subclasses that emit ApplicationComponents describe each module here.
|
|
138
|
+
# @param _repo_root [String] Absolute path to the repository root
|
|
139
|
+
# @param _rel_dir [String] Directory of the module relative to the root ("." for the root)
|
|
140
|
+
# @param _mod_name [String] Module name from discover_modules
|
|
141
|
+
# @return [Hash] { type: "executable|library|module|...", tags: ["ecosystem:go", ...],
|
|
142
|
+
# entrypoints: ["cmd/api", ...] }, empty when unknown
|
|
143
|
+
def component_facts(_repo_root, _rel_dir, _mod_name)
|
|
144
|
+
{}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# component/type, component/entrypoints and the machine tags (namespace:value) in architecture/tags for the
|
|
148
|
+
# facts of component_facts.
|
|
149
|
+
# The tags a person set stay; machine tags of the importer's namespaces from an earlier run are replaced.
|
|
150
|
+
def component_annotations(facts, existing_tags: nil)
|
|
151
|
+
annotations = {}
|
|
152
|
+
annotations["component/type"] = facts[:type] if facts[:type]
|
|
153
|
+
annotations["component/entrypoints"] = facts[:entrypoints].join(",") if facts[:entrypoints]&.any?
|
|
154
|
+
annotations["architecture/tags"] = merge_machine_tags(existing_tags, facts[:tags]).join(",") if facts[:tags]&.any?
|
|
155
|
+
annotations
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def merge_machine_tags(existing_tags, machine_tags)
|
|
159
|
+
kept = existing_tags.to_s.split(",").map(&:strip).reject(&:empty?).reject do |tag|
|
|
160
|
+
MACHINE_TAG_NAMESPACES.include?(tag.split(":", 2).first) && tag.include?(":")
|
|
161
|
+
end
|
|
162
|
+
(kept + Array(machine_tags)).uniq
|
|
163
|
+
end
|
|
164
|
+
|
|
126
165
|
# ── Module coloring ───────────────────────────────────────────────────────
|
|
127
166
|
|
|
128
167
|
def build_module_colors(modules)
|
|
@@ -22,6 +22,9 @@ require_relative "../team_matcher"
|
|
|
22
22
|
# import/config/corporateAffixes - Optional comma-separated corporate username affixes for team matching (e.g., "ionos,1and1")
|
|
23
23
|
# import/config/grapherOutputPath - Optional output path for language-grapher child imports
|
|
24
24
|
class Archsight::Import::Handlers::Repository < Archsight::Import::Handler
|
|
25
|
+
# generates child imports (see Handler.generates_imports?)
|
|
26
|
+
def self.generates_imports? = true
|
|
27
|
+
|
|
25
28
|
def execute
|
|
26
29
|
@path = config_path("path")
|
|
27
30
|
@git_url = config("gitUrl")
|
|
@@ -43,6 +43,9 @@ require_relative "../registry"
|
|
|
43
43
|
# ]
|
|
44
44
|
# }
|
|
45
45
|
class Archsight::Import::Handlers::RestApiIndex < Archsight::Import::Handler
|
|
46
|
+
# generates child imports (see Handler.generates_imports?)
|
|
47
|
+
def self.generates_imports? = true
|
|
48
|
+
|
|
46
49
|
VALID_GATES = %w[General-Availability Early-Access Development GA EA DEV].freeze
|
|
47
50
|
GATE_TOKEN_RE = /\b(ga|ea|dev)\b/i
|
|
48
51
|
|
|
@@ -8,13 +8,25 @@ require "archsight/import"
|
|
|
8
8
|
# The executor looks up handlers by name to instantiate and execute them.
|
|
9
9
|
module Archsight::Import::Registry
|
|
10
10
|
@handlers = {}
|
|
11
|
+
@builtin = {}
|
|
11
12
|
|
|
12
13
|
class << self
|
|
13
14
|
# Register a handler class with a name
|
|
14
15
|
# @param name [String, Symbol] Handler name (matches import/handler annotation)
|
|
15
16
|
# @param handler_class [Class] Handler class that extends Handler
|
|
17
|
+
# @raise [DuplicateHandlerError] if another class already registered the name
|
|
16
18
|
def register(name, handler_class)
|
|
17
|
-
|
|
19
|
+
name = name.to_s
|
|
20
|
+
existing = @handlers[name] || @builtin[name]
|
|
21
|
+
raise Archsight::Import::DuplicateHandlerError, "Import handler '#{name}' is registered twice (#{existing} and #{handler_class})" if existing && existing != handler_class
|
|
22
|
+
|
|
23
|
+
@handlers[name] = handler_class
|
|
24
|
+
@builtin[name] = handler_class if builtin_caller?(caller_locations(1, 1)&.first)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Register the built-in handlers again after `clear!` (they are only required once per process)
|
|
28
|
+
def restore_builtin!
|
|
29
|
+
@handlers = @builtin.merge(@handlers)
|
|
18
30
|
end
|
|
19
31
|
|
|
20
32
|
# Look up a handler class by name
|
|
@@ -70,8 +82,17 @@ module Archsight::Import::Registry
|
|
|
70
82
|
def clear!
|
|
71
83
|
@handlers = {}
|
|
72
84
|
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def builtin_caller?(location)
|
|
89
|
+
location && File.expand_path(location.path).start_with?("#{File.expand_path("handlers", __dir__)}/")
|
|
90
|
+
end
|
|
73
91
|
end
|
|
74
92
|
end
|
|
75
93
|
|
|
76
94
|
# Error raised when an unknown handler is requested
|
|
77
95
|
class Archsight::Import::UnknownHandlerError < StandardError; end
|
|
96
|
+
|
|
97
|
+
# Error raised when two handler classes register the same name
|
|
98
|
+
class Archsight::Import::DuplicateHandlerError < StandardError; end
|
data/lib/archsight/import.rb
CHANGED
|
@@ -1,14 +1,102 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
3
5
|
# Define import module namespaces
|
|
4
6
|
module Archsight
|
|
5
7
|
module Import
|
|
6
8
|
module Handlers
|
|
7
9
|
end
|
|
10
|
+
|
|
11
|
+
# Where the git-based handlers clone repositories: the handler's own `cacheRoot` setting, else `import.cache_root`
|
|
12
|
+
# of the user configuration (or ARCHSIGHT_IMPORT_CACHE_ROOT), else ~/.cache/archsight/git
|
|
13
|
+
# @param override [String, nil] a handler's own setting
|
|
14
|
+
# @return [String] the directory as configured (a `~` stays, so generated child imports keep the portable form;
|
|
15
|
+
# expand it before use)
|
|
16
|
+
def self.cache_root(override = nil)
|
|
17
|
+
require "archsight/user_config"
|
|
18
|
+
override || Archsight::UserConfig.setting("import", "cache_root")&.to_s || File.join(cache_dir, "git")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The cache directory of Archsight, ~/.cache/archsight
|
|
22
|
+
def self.cache_dir = File.join(Dir.home, ".cache", "archsight")
|
|
23
|
+
|
|
24
|
+
# Tells Spotlight (macOS) not to index this
|
|
25
|
+
NEVER_INDEX = ".metadata_never_index"
|
|
26
|
+
|
|
27
|
+
# Makes sure the clones are not indexed by Spotlight: the marker file in ~/.cache/archsight, or in the cache root
|
|
28
|
+
# itself when it is somewhere else. Run before every import; a directory that cannot be written is left alone,
|
|
29
|
+
# the import does not depend on it.
|
|
30
|
+
# @param root [String] the cache root (Handler#cache_root)
|
|
31
|
+
# @return [String, nil] the marker file, nil if it could not be written
|
|
32
|
+
def self.never_index(root)
|
|
33
|
+
dir = File.expand_path(root)
|
|
34
|
+
dir = cache_dir if dir.start_with?("#{cache_dir}/")
|
|
35
|
+
marker = File.join(dir, NEVER_INDEX)
|
|
36
|
+
return marker if File.exist?(marker)
|
|
37
|
+
|
|
38
|
+
FileUtils.mkdir_p(dir)
|
|
39
|
+
FileUtils.touch(marker)
|
|
40
|
+
marker
|
|
41
|
+
rescue SystemCallError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The entries of `import/dependsOn`: import names and glob patterns
|
|
46
|
+
# @return [Array<String>]
|
|
47
|
+
def self.declared_dependencies(import)
|
|
48
|
+
import.annotations["import/dependsOn"].to_s.split(",").map(&:strip).reject(&:empty?)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Absolute path of a file below the resources directory, nil when the path leaves it
|
|
52
|
+
def self.resources_path(resources_dir, rel_path)
|
|
53
|
+
root = File.expand_path(resources_dir)
|
|
54
|
+
path = File.expand_path(rel_path.to_s, root)
|
|
55
|
+
path.start_with?("#{root}/") ? path : nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The file an import writes its resources to, relative to the resources directory: `import/outputPath`, else
|
|
59
|
+
# generated/<name>.yaml (see Handler#write_yaml)
|
|
60
|
+
def self.output_path(import)
|
|
61
|
+
import.annotations["import/outputPath"] || File.join("generated", "#{import.name.gsub(/[^a-zA-Z0-9_-]/, "_")}.yaml")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The files an import wrote, relative to the resources directory: its output path and the `generated/outputs`
|
|
65
|
+
# of its last run. Tells which import a resource of a generated file comes from.
|
|
66
|
+
def self.output_files(import)
|
|
67
|
+
[output_path(import), *import.annotations["generated/outputs"].to_s.split(",").map(&:strip)].reject(&:empty?).uniq
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Hash of the files `import/config/inputs` matches (glob patterns relative to the resources directory, comma
|
|
71
|
+
# separated): their paths, sizes and modification times. nil when the import declares no inputs.
|
|
72
|
+
# @return [String, nil]
|
|
73
|
+
def self.inputs_hash(import, resources_dir)
|
|
74
|
+
patterns = import.annotations["import/config/inputs"].to_s.split(",").map(&:strip).reject(&:empty?)
|
|
75
|
+
return nil if patterns.empty?
|
|
76
|
+
|
|
77
|
+
root = File.expand_path(resources_dir)
|
|
78
|
+
files = patterns.flat_map { |pattern| Dir.glob(pattern, base: root) }.uniq.sort
|
|
79
|
+
stats = files.filter_map do |file|
|
|
80
|
+
stat = File.stat(File.join(root, file))
|
|
81
|
+
"#{file}:#{stat.size}:#{stat.mtime.to_f}" if stat.file?
|
|
82
|
+
end
|
|
83
|
+
require "digest"
|
|
84
|
+
Digest::SHA256.hexdigest(stats.join("\n"))[0, 16]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Whether a `import/dependsOn` entry is a glob pattern (`Import:Repo:*`)
|
|
88
|
+
def self.pattern?(entry)
|
|
89
|
+
entry.match?(/[*?\[]/)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Whether an import name matches a `import/dependsOn` entry
|
|
93
|
+
def self.dependency_match?(entry, name)
|
|
94
|
+
pattern?(entry) ? File.fnmatch?(entry, name, File::FNM_EXTGLOB) : entry == name
|
|
95
|
+
end
|
|
8
96
|
end
|
|
9
97
|
end
|
|
10
98
|
|
|
11
99
|
require_relative "import/registry"
|
|
12
100
|
require_relative "import/handler"
|
|
13
101
|
require_relative "import/executor"
|
|
14
|
-
|
|
102
|
+
require_relative "import/handler_loader"
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archsight
|
|
4
|
+
class Linter
|
|
5
|
+
# Structural checks over the declared relations, the rules of "Choosing the Kind" and "Choosing the Kind:
|
|
6
|
+
# Infrastructure" in the modeling guide. All findings are warnings: they point at modeling decisions to review and
|
|
7
|
+
# have legitimate exceptions (internal services, capabilities that no product carries yet).
|
|
8
|
+
class Structure
|
|
9
|
+
# Network types that run over another network and need one that serves them
|
|
10
|
+
OVERLAY_TYPES = %w[overlay vpn].freeze
|
|
11
|
+
NETWORK_DEVICE_TYPES = %w[switch router].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(database)
|
|
14
|
+
@database = database
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Findings in a stable order, each starting with the path reference of the resource
|
|
18
|
+
def warnings
|
|
19
|
+
[
|
|
20
|
+
component_in_several_services,
|
|
21
|
+
artifact_in_several_components,
|
|
22
|
+
service_with_same_named_component,
|
|
23
|
+
product_without_service,
|
|
24
|
+
service_reached_from_nothing,
|
|
25
|
+
capability_without_product,
|
|
26
|
+
generated_component_in_several_repositories,
|
|
27
|
+
link_not_between_two_facilities,
|
|
28
|
+
overlay_without_underlay,
|
|
29
|
+
facility_without_location,
|
|
30
|
+
equipment_without_facility,
|
|
31
|
+
device_without_facility,
|
|
32
|
+
device_connected_to_itself,
|
|
33
|
+
network_device_realizing_no_network,
|
|
34
|
+
device_served_apart_from_its_equipment
|
|
35
|
+
].flatten.sort
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def instances(kind)
|
|
41
|
+
@database.instances_by_kind(kind).values
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def targets(instance, verb, key)
|
|
45
|
+
instance.relations(verb, key)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def warning(instance, message)
|
|
49
|
+
"#{instance.path_ref}: #{instance.klass} '#{instance.name}' #{message}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def names(list)
|
|
53
|
+
list.map { |item| "'#{item.name}'" }.join(", ")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# { target => [owners that list it under verb/key] } for the instances of `kind`
|
|
57
|
+
def owners_by_target(kind, verb, key)
|
|
58
|
+
owners = Hash.new { |hash, target| hash[target] = [] }
|
|
59
|
+
instances(kind).each { |owner| targets(owner, verb, key).each { |target| owners[target] << owner } }
|
|
60
|
+
owners
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def component_in_several_services
|
|
64
|
+
shared("ApplicationService", :realizedThrough, :applicationComponents, "services")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def artifact_in_several_components
|
|
68
|
+
shared("ApplicationComponent", :realizedThrough, :technologyArtifacts, "components")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def shared(kind, verb, key, plural)
|
|
72
|
+
owners_by_target(kind, verb, key).filter_map do |target, owners|
|
|
73
|
+
next if owners.length < 2
|
|
74
|
+
|
|
75
|
+
warning(target, "is realized by #{owners.length} #{plural}: #{names(owners.sort_by(&:name))}")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def service_with_same_named_component
|
|
80
|
+
instances("ApplicationService").filter_map do |service|
|
|
81
|
+
components = targets(service, :realizedThrough, :applicationComponents)
|
|
82
|
+
next unless components.length == 1 && components.first.name == service.name
|
|
83
|
+
|
|
84
|
+
warning(service, "has one component with the same name")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def product_without_service
|
|
89
|
+
instances("BusinessProduct").filter_map do |product|
|
|
90
|
+
next unless targets(product, :servedBy, :applicationServices).empty?
|
|
91
|
+
|
|
92
|
+
warning(product, "is served by no ApplicationService")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A service is reached when a product or process is served by it, directly or through services that depend on it
|
|
97
|
+
def service_reached_from_nothing
|
|
98
|
+
reached = reached_services
|
|
99
|
+
instances("ApplicationService").filter_map do |service|
|
|
100
|
+
next if reached.include?(service)
|
|
101
|
+
|
|
102
|
+
warning(service, "is not served to any product or process")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def reached_services
|
|
107
|
+
reached = []
|
|
108
|
+
queue = (instances("BusinessProduct") + instances("BusinessProcess")).flat_map { |item| targets(item, :servedBy, :applicationServices) }
|
|
109
|
+
until queue.empty?
|
|
110
|
+
service = queue.shift
|
|
111
|
+
next if reached.include?(service)
|
|
112
|
+
|
|
113
|
+
reached << service
|
|
114
|
+
queue.concat(targets(service, :dependsOn, :applicationServices))
|
|
115
|
+
end
|
|
116
|
+
reached
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def capability_without_product
|
|
120
|
+
realized = owners_by_target("BusinessProduct", :realizes, :strategyCapabilities)
|
|
121
|
+
instances("StrategyCapability").filter_map do |capability|
|
|
122
|
+
next if realized.key?(capability)
|
|
123
|
+
|
|
124
|
+
warning(capability, "is realized by no product")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# A generated component realized by several repositories is two components merged into one: the repositories
|
|
129
|
+
# declare the same module path. A re-import gives each its own name (GoModuleParser#owned_component_name).
|
|
130
|
+
def generated_component_in_several_repositories
|
|
131
|
+
instances("ApplicationComponent").filter_map do |component|
|
|
132
|
+
next unless component.annotations.key?("generated/script")
|
|
133
|
+
|
|
134
|
+
repositories = targets(component, :realizedThrough, :technologyArtifacts).select { |a| a.name.start_with?("Repo:") }
|
|
135
|
+
next if repositories.length < 2
|
|
136
|
+
|
|
137
|
+
warning(component, "is generated from #{repositories.length} repositories with the same module path: " \
|
|
138
|
+
"#{names(repositories.sort_by(&:name))}")
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# ---- infrastructure
|
|
143
|
+
|
|
144
|
+
def link_not_between_two_facilities
|
|
145
|
+
instances("TechnologyEquipment").filter_map do |equipment|
|
|
146
|
+
next unless equipment.annotations["equipment/type"] == "link"
|
|
147
|
+
|
|
148
|
+
count = targets(equipment, :locatedAt, :technologyFacilities).length
|
|
149
|
+
next if count == 2
|
|
150
|
+
|
|
151
|
+
warning(equipment, "is a link located at #{count} #{count == 1 ? "facility" : "facilities"}, not 2")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def overlay_without_underlay
|
|
156
|
+
instances("TechnologyCommunicationNetwork").filter_map do |network|
|
|
157
|
+
type = network.annotations["network/type"]
|
|
158
|
+
next unless OVERLAY_TYPES.include?(type)
|
|
159
|
+
next unless targets(network, :servedBy, :technologyCommunicationNetworks).empty?
|
|
160
|
+
|
|
161
|
+
warning(network, "is #{type == "overlay" ? "an" : "a"} #{type} network served by no network")
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Only the outermost facility needs a location; nested halls inherit it from their campus
|
|
166
|
+
def facility_without_location
|
|
167
|
+
nested = owners_by_target("TechnologyFacility", :composedOf, :technologyFacilities)
|
|
168
|
+
instances("TechnologyFacility").filter_map do |facility|
|
|
169
|
+
next if nested.key?(facility) || targets(facility, :locatedAt, :locations).any?
|
|
170
|
+
|
|
171
|
+
warning(facility, "is located at no Location")
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# TechnologyEquipment that is part of other equipment is placed through its whole, and a whole (a backbone) through its
|
|
176
|
+
# parts (the links)
|
|
177
|
+
def equipment_without_facility
|
|
178
|
+
parts = owners_by_target("TechnologyEquipment", :composedOf, :technologyEquipment)
|
|
179
|
+
instances("TechnologyEquipment").filter_map do |equipment|
|
|
180
|
+
next if parts.key?(equipment) || targets(equipment, :composedOf, :technologyEquipment).any?
|
|
181
|
+
next if targets(equipment, :locatedAt, :technologyFacilities).any?
|
|
182
|
+
|
|
183
|
+
warning(equipment, "is located at no TechnologyFacility")
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# A device that is part of equipment (a rack) or of another device (a virtual chassis) is placed through its
|
|
188
|
+
# whole, and a chassis through its members
|
|
189
|
+
def device_without_facility
|
|
190
|
+
parts = owners_by_target("TechnologyDevice", :composedOf, :technologyDevices)
|
|
191
|
+
racked = owners_by_target("TechnologyEquipment", :composedOf, :technologyDevices)
|
|
192
|
+
instances("TechnologyDevice").filter_map do |device|
|
|
193
|
+
next if parts.key?(device) || racked.key?(device) || targets(device, :composedOf, :technologyDevices).any?
|
|
194
|
+
next if targets(device, :locatedAt, :technologyFacilities).any?
|
|
195
|
+
|
|
196
|
+
warning(device, "is located at no TechnologyFacility")
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def device_connected_to_itself
|
|
201
|
+
instances("TechnologyDevice").filter_map do |device|
|
|
202
|
+
next unless targets(device, :connectedTo, :technologyDevices).include?(device)
|
|
203
|
+
|
|
204
|
+
warning(device, "is connectedTo itself")
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# The members of a virtual chassis or stack realize the network through the chassis
|
|
209
|
+
def network_device_realizing_no_network
|
|
210
|
+
realized = owners_by_target("TechnologyCommunicationNetwork", :realizedBy, :technologyDevices)
|
|
211
|
+
members = owners_by_target("TechnologyDevice", :composedOf, :technologyDevices)
|
|
212
|
+
instances("TechnologyDevice").filter_map do |device|
|
|
213
|
+
type = device.annotations["device/type"]
|
|
214
|
+
next unless NETWORK_DEVICE_TYPES.include?(type)
|
|
215
|
+
next if realized.key?(device) || members.key?(device)
|
|
216
|
+
|
|
217
|
+
warning(device, "is a #{type} that realizes no TechnologyCommunicationNetwork")
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# A device in a partition (equipment servedBy a team) serves that team; one that names another team has been
|
|
222
|
+
# handed over without moving it. A device that names no team is served through its equipment.
|
|
223
|
+
def device_served_apart_from_its_equipment
|
|
224
|
+
holders = owners_by_target("TechnologyEquipment", :composedOf, :technologyDevices)
|
|
225
|
+
instances("TechnologyDevice").filter_map do |device|
|
|
226
|
+
teams = targets(device, :servedBy, :businessActors)
|
|
227
|
+
equipment_teams = holders.fetch(device, []).flat_map { |equipment| targets(equipment, :servedBy, :businessActors) }
|
|
228
|
+
next if teams.empty? || equipment_teams.empty?
|
|
229
|
+
|
|
230
|
+
others = teams - equipment_teams
|
|
231
|
+
next if others.empty?
|
|
232
|
+
|
|
233
|
+
warning(device, "is servedBy #{names(others.sort_by(&:name))}, not by the team of its equipment " \
|
|
234
|
+
"(#{names(equipment_teams.uniq.sort_by(&:name))})")
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|