archsight 0.3.0 → 0.3.2
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/CONTRIBUTING.md +1 -1
- data/README.md +7 -2
- data/docs/computed_annotations.md +7 -5
- data/docs/icons.md +3 -2
- data/docs/index.md.erb +5 -4
- data/docs/licenses.md +1 -2
- data/docs/modeling.md +127 -6
- data/docs/pages.md +72 -6
- data/docs/search.md +26 -2
- data/docs/togaf.md +4 -0
- data/lib/archsight/annotations/annotation.rb +5 -4
- data/lib/archsight/annotations/computed.rb +5 -1
- data/lib/archsight/annotations/relation_resolver.rb +109 -83
- data/lib/archsight/cli.rb +9 -2
- data/lib/archsight/database.rb +59 -4
- data/lib/archsight/diagram.rb +7 -0
- data/lib/archsight/documentation.rb +9 -5
- data/lib/archsight/editor.rb +2 -2
- data/lib/archsight/export/confluence/exporter.rb +11 -2
- data/lib/archsight/export/confluence/storage.rb +99 -8
- data/lib/archsight/export/confluence/tables.rb +78 -0
- data/lib/archsight/helpers/fenced_blocks.rb +61 -0
- data/lib/archsight/helpers/requirements_blocks.rb +90 -0
- data/lib/archsight/helpers/resource_resolver.rb +1 -1
- data/lib/archsight/helpers/view_blocks.rb +108 -0
- data/lib/archsight/helpers/wiki_links.rb +50 -5
- data/lib/archsight/helpers.rb +3 -0
- data/lib/archsight/import/handlers/go_grapher.rb +4 -1
- data/lib/archsight/import/handlers/go_module_parser.rb +4 -1
- data/lib/archsight/linter.rb +31 -3
- data/lib/archsight/mcp/analyze_resource_tool.rb +2 -2
- data/lib/archsight/mcp/base.rb +38 -0
- data/lib/archsight/mcp/resource_doc_tool.rb +2 -2
- data/lib/archsight/query/ast.rb +2 -1
- data/lib/archsight/query/evaluator.rb +2 -2
- data/lib/archsight/references.rb +129 -0
- data/lib/archsight/requirements.rb +70 -0
- data/lib/archsight/resources/analysis.rb +2 -1
- data/lib/archsight/resources/application_component.rb +8 -3
- data/lib/archsight/resources/application_interface.rb +5 -3
- data/lib/archsight/resources/application_service.rb +8 -7
- data/lib/archsight/resources/base.rb +67 -17
- data/lib/archsight/resources/business_actor.rb +5 -3
- data/lib/archsight/resources/business_control.rb +80 -0
- data/lib/archsight/resources/business_process.rb +3 -2
- data/lib/archsight/resources/business_product.rb +6 -5
- data/lib/archsight/resources/compliance_evidence.rb +40 -3
- data/lib/archsight/resources/data_object.rb +3 -2
- data/lib/archsight/resources/import.rb +5 -2
- data/lib/archsight/resources/{business_constraint.rb → motivation_constraint.rb} +4 -4
- data/lib/archsight/resources/motivation_goal.rb +1 -1
- data/lib/archsight/resources/{business_requirement.rb → motivation_requirement.rb} +19 -8
- data/lib/archsight/resources/motivation_stakeholder.rb +2 -2
- data/lib/archsight/resources/page.rb +4 -2
- data/lib/archsight/resources/strategy_capability.rb +2 -2
- data/lib/archsight/resources/technology_artifact.rb +4 -3
- data/lib/archsight/resources/technology_node.rb +2 -2
- data/lib/archsight/resources/technology_service.rb +4 -0
- data/lib/archsight/resources/technology_system_software.rb +4 -0
- data/lib/archsight/resources/view.rb +2 -1
- data/lib/archsight/resources.rb +34 -3
- data/lib/archsight/template.rb +2 -2
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/view_table.rb +102 -0
- data/lib/archsight/web/api/docs.rb +1 -1
- data/lib/archsight/web/api/json_helpers.rb +21 -15
- data/lib/archsight/web/api/openapi/spec.yaml +135 -2
- data/lib/archsight/web/api/page_helpers.rb +8 -7
- data/lib/archsight/web/api/requirements_helpers.rb +26 -0
- data/lib/archsight/web/api/routes.rb +19 -0
- data/lib/archsight/web/application.rb +12 -2
- data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +1 -0
- data/lib/archsight/web/public/vue/ApiDocsPage-DoOxKjG0.js +1 -0
- data/lib/archsight/web/public/vue/{DocPage-uaT8CdFm.js → DocPage-CfsC3CeQ.js} +1 -1
- data/lib/archsight/web/public/vue/EditorPage-CbG4mc9T.css +1 -0
- data/lib/archsight/web/public/vue/EditorPage-CsJA0q8n.js +35 -0
- data/lib/archsight/web/public/vue/ErrorPage-Ck9izEUS.css +1 -0
- data/lib/archsight/web/public/vue/ErrorPage-OJpJz9df.js +2 -0
- data/lib/archsight/web/public/vue/GraphView-BvWAbUAl.css +1 -0
- data/lib/archsight/web/public/vue/GraphView-CBq6oFRV.js +1 -0
- data/lib/archsight/web/public/vue/HomePage-BwgMLgVC.js +2 -0
- data/lib/archsight/web/public/vue/InstanceRouter-DQz-SsQq.js +1 -0
- data/lib/archsight/web/public/vue/InstanceRouter-jeavM6PW.css +1 -0
- data/lib/archsight/web/public/vue/KindList-4q0K9Cll.js +1 -0
- data/lib/archsight/web/public/vue/PageView-DzeGnvoS.js +1 -0
- data/lib/archsight/web/public/vue/QueryError-3EqjpbyV.css +1 -0
- data/lib/archsight/web/public/vue/QueryError-kg6Yf-pW.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-B0FLaFR3.css +1 -0
- data/lib/archsight/web/public/vue/ResourceList-Df_0e0qt.js +2 -0
- data/lib/archsight/web/public/vue/SearchResults-CKgY6_wH.js +1 -0
- data/lib/archsight/web/public/vue/SearchResults-CyPUZZSC.css +1 -0
- data/lib/archsight/web/public/vue/WikiPage-DoFJ0dbW.css +1 -0
- data/lib/archsight/web/public/vue/WikiPage-DrvCHwJ7.js +13 -0
- data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BfA1TPxQ.js +1 -0
- data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-BmgdUKVD.js +1 -0
- data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-DbXDvAWF.js +1 -0
- data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-BkVhrKS1.js +1 -0
- data/lib/archsight/web/public/vue/index-D0Q5GZRs.js +3 -0
- data/lib/archsight/web/public/vue/index-oF-iyZvk.css +1 -0
- data/lib/archsight/web/public/vue/info-A6RAGUB7-BIlCVuUY.js +1 -0
- data/lib/archsight/web/public/vue/mermaid-BjEi5URd.js +3334 -0
- data/lib/archsight/web/public/vue/packet-AYTQ26CC-eoQTjY3j.js +1 -0
- data/lib/archsight/web/public/vue/pie-WAS4IAKB-DEOotGhh.js +1 -0
- data/lib/archsight/web/public/vue/radar-RG4KPBEZ-C2yucuUN.js +1 -0
- data/lib/archsight/web/public/vue/railroad-74A4TZTK-CzONK5VY.js +1 -0
- data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-9zRPsDsx.js +1 -0
- data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-BJ4dkz9l.js +1 -0
- data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DFy_rqAj.js +1 -0
- data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-2cq5CyD2.js +1 -0
- data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DK-e9Ez4.js +1 -0
- data/lib/archsight/web/public/vue/{useGraphviz-C71SdG-N.js → useGraphviz-DweKV7Kg.js} +1 -1
- data/lib/archsight/web/public/vue/wardley-WFR3VGLG-BQwqUqfB.js +1 -0
- data/lib/archsight/web/public/vue.html +3 -3
- data/lib/archsight.rb +2 -0
- metadata +53 -42
- data/lib/archsight/web/public/vue/ApiDocsPage-C0y953v0.css +0 -1
- data/lib/archsight/web/public/vue/ApiDocsPage-C_4tAWis.js +0 -1
- data/lib/archsight/web/public/vue/EditorPage-C557BJC-.js +0 -35
- data/lib/archsight/web/public/vue/EditorPage-df5N-p21.css +0 -1
- data/lib/archsight/web/public/vue/ErrorPage-Vdebmife.js +0 -2
- data/lib/archsight/web/public/vue/ErrorPage-uMDnfY5_.css +0 -1
- data/lib/archsight/web/public/vue/GraphView-BduUql2N.js +0 -1
- data/lib/archsight/web/public/vue/GraphView-Cj2V2stN.css +0 -1
- data/lib/archsight/web/public/vue/HomePage-BHUTg8Ap.js +0 -2
- data/lib/archsight/web/public/vue/InstanceRouter-1lSigA58.js +0 -1
- data/lib/archsight/web/public/vue/InstanceRouter-Di7f3Rya.css +0 -1
- data/lib/archsight/web/public/vue/KindList-BDZs0j6d.js +0 -1
- data/lib/archsight/web/public/vue/PageView-BYzJDwof.js +0 -1
- data/lib/archsight/web/public/vue/ResourceList-CnbhU9wm.js +0 -1
- data/lib/archsight/web/public/vue/ResourceList-xyBwu7fh.css +0 -1
- data/lib/archsight/web/public/vue/SearchResults-CNhf6VOx.js +0 -1
- data/lib/archsight/web/public/vue/SearchResults-DOHzqAy3.css +0 -1
- data/lib/archsight/web/public/vue/WikiPage-DbmWkM7W.js +0 -13
- data/lib/archsight/web/public/vue/WikiPage-GV67QmNJ.css +0 -1
- data/lib/archsight/web/public/vue/architecture-TIHT7OUA-Bf-MWFmn.js +0 -1
- data/lib/archsight/web/public/vue/cynefin-VYW2F7L2-CkKt8qqs.js +0 -1
- data/lib/archsight/web/public/vue/eventmodeling-45OFAUF4-NwSTYPHj.js +0 -1
- data/lib/archsight/web/public/vue/gitGraph-TEB2WS4Q-DLRegrRG.js +0 -1
- data/lib/archsight/web/public/vue/index-CyVWObLU.js +0 -3
- data/lib/archsight/web/public/vue/index-DtKeHT3S.css +0 -1
- data/lib/archsight/web/public/vue/info-DKCQHKI2-EV39NzoN.js +0 -1
- data/lib/archsight/web/public/vue/mermaid-BMkGfnhm.js +0 -3279
- data/lib/archsight/web/public/vue/packet-7NZHBO7P-USljV3MZ.js +0 -1
- data/lib/archsight/web/public/vue/pie-RZYD4A2V-DChciBW2.js +0 -1
- data/lib/archsight/web/public/vue/radar-I7S5WNFK-CgGJdW1t.js +0 -1
- data/lib/archsight/web/public/vue/railroad-3IZDKUUU-Dy43vF5c.js +0 -1
- data/lib/archsight/web/public/vue/railroad-abnf-AHOZXSZD-BhsTH-sE.js +0 -1
- data/lib/archsight/web/public/vue/railroad-ebnf-EBAXGLYW-BBTd_u0u.js +0 -1
- data/lib/archsight/web/public/vue/railroad-peg-LSFZ7HO6-BiNTEzf0.js +0 -1
- data/lib/archsight/web/public/vue/treeView-QDETBFTQ-BfnCcf-q.js +0 -1
- data/lib/archsight/web/public/vue/treemap-6X3UGDF4-BsAORhvk.js +0 -1
- data/lib/archsight/web/public/vue/wardley-OPB4EBWU-DQvnEhRj.js +0 -1
|
@@ -36,7 +36,8 @@ class Archsight::Resources::BusinessActor < Archsight::Resources::Base
|
|
|
36
36
|
sidebar: false,
|
|
37
37
|
filter: :word,
|
|
38
38
|
format: :tag_word,
|
|
39
|
-
type: Archsight::Annotations::EmailRecipient
|
|
39
|
+
type: Archsight::Annotations::EmailRecipient,
|
|
40
|
+
summary: true
|
|
40
41
|
|
|
41
42
|
annotation "team/members",
|
|
42
43
|
description: 'Team members (format: "Name <email>" or "email")',
|
|
@@ -108,7 +109,7 @@ class Archsight::Resources::BusinessActor < Archsight::Resources::Base
|
|
|
108
109
|
computed_annotation "team/size",
|
|
109
110
|
title: "Team Size",
|
|
110
111
|
description: "Number of team members (including sub-teams)",
|
|
111
|
-
|
|
112
|
+
summary: true,
|
|
112
113
|
type: Integer do
|
|
113
114
|
# Count members from this team
|
|
114
115
|
members = @instance.annotations["team/members"]
|
|
@@ -147,7 +148,8 @@ class Archsight::Resources::BusinessActor < Archsight::Resources::Base
|
|
|
147
148
|
computed_annotation "repository/artifacts/total",
|
|
148
149
|
title: "Maintained Repositories",
|
|
149
150
|
description: "Number of git repositories maintained by this team",
|
|
150
|
-
type: Integer
|
|
151
|
+
type: Integer,
|
|
152
|
+
summary: true do
|
|
151
153
|
count(incoming_transitive('TechnologyArtifact: artifact/type == "repo"'))
|
|
152
154
|
end
|
|
153
155
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# BusinessControl represents a control that guides a business process
|
|
4
|
+
class Archsight::Resources::BusinessControl < Archsight::Resources::Base
|
|
5
|
+
include_annotations :git, :architecture
|
|
6
|
+
|
|
7
|
+
description <<~MD
|
|
8
|
+
Represents a control: a safeguard or decision step with an owner that guides how a business process is carried out.
|
|
9
|
+
|
|
10
|
+
## ArchiMate / TOGAF Definition
|
|
11
|
+
|
|
12
|
+
**Layer:** Business
|
|
13
|
+
**Aspect:** Behavior (guidance)
|
|
14
|
+
|
|
15
|
+
ArchiMate has no control element. TOGAF's content metamodel has one: a decision-making step with
|
|
16
|
+
accountability and authority, applied to a process or function. A control is therefore modelled as
|
|
17
|
+
a business-layer kind that a `BusinessProcess` is guided by (`guidedBy`).
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Use BusinessControl to represent:
|
|
22
|
+
|
|
23
|
+
- Security and compliance controls (access review, network documentation, backup verification)
|
|
24
|
+
- Operational checks that a process must pass (change approval, four-eyes principle)
|
|
25
|
+
- Controls of a standard or framework (BSI C5, ISO 27001, SOC 2), linked to the requirements they satisfy
|
|
26
|
+
|
|
27
|
+
## How it connects
|
|
28
|
+
|
|
29
|
+
- A `BusinessProcess` is `guidedBy` the control
|
|
30
|
+
- The control is `ownedBy` the actor that is accountable for it and `executedBy` the actors that carry it out
|
|
31
|
+
- The control `satisfies` requirements and is `evidencedBy` compliance evidence
|
|
32
|
+
|
|
33
|
+
A control addresses requirements from the **process side**. Whether an application implements a requirement is
|
|
34
|
+
stated on the application (`realizes`, `plans`, `evidencedBy`), not on the control. Link evidence to a control
|
|
35
|
+
only for the records the control itself produces (reviews, diagrams, change history, audit logs), and set the
|
|
36
|
+
`evidence/type` of that evidence to `process`, `documentation` or `audit-log`.
|
|
37
|
+
|
|
38
|
+
Put the best practices and the evidence requirements of a control in the description.
|
|
39
|
+
MD
|
|
40
|
+
|
|
41
|
+
icon "shield-search"
|
|
42
|
+
layer "business"
|
|
43
|
+
|
|
44
|
+
annotation "control/id",
|
|
45
|
+
description: "Identifier of the control in its catalogue (e.g. COS-07)",
|
|
46
|
+
title: "Control ID",
|
|
47
|
+
summary: true
|
|
48
|
+
|
|
49
|
+
annotation "control/status",
|
|
50
|
+
description: "Implementation status of the control",
|
|
51
|
+
enum: %w[implemented partial planned not-implemented],
|
|
52
|
+
filter: :word,
|
|
53
|
+
summary: true
|
|
54
|
+
|
|
55
|
+
annotation "control/frequency",
|
|
56
|
+
description: "How often the control is carried out",
|
|
57
|
+
enum: %w[continuous event-based daily weekly monthly quarterly semi-annually annually],
|
|
58
|
+
filter: :word,
|
|
59
|
+
summary: true
|
|
60
|
+
|
|
61
|
+
annotation "control/objective",
|
|
62
|
+
description: "What the control achieves, in one short paragraph",
|
|
63
|
+
title: "Objective",
|
|
64
|
+
format: :markdown
|
|
65
|
+
|
|
66
|
+
annotation "control/last-review",
|
|
67
|
+
description: "When the control was last reviewed (ISO 8601 date or time)",
|
|
68
|
+
title: "Last review",
|
|
69
|
+
validator: ->(value) { Archsight::Resources::Page.timestamp_error(value) }
|
|
70
|
+
|
|
71
|
+
annotation "control/next-review",
|
|
72
|
+
description: "When the control is due for review (ISO 8601 date or time)",
|
|
73
|
+
title: "Next review",
|
|
74
|
+
validator: ->(value) { Archsight::Resources::Page.timestamp_error(value) }
|
|
75
|
+
|
|
76
|
+
relation :ownedBy, :businessActors, :BusinessActor
|
|
77
|
+
relation :executedBy, :businessActors, :BusinessActor
|
|
78
|
+
relation :satisfies, :motivationRequirements, :MotivationRequirement
|
|
79
|
+
relation :evidencedBy, :complianceEvidences, :ComplianceEvidence
|
|
80
|
+
end
|
|
@@ -30,8 +30,9 @@ class Archsight::Resources::BusinessProcess < Archsight::Resources::Base
|
|
|
30
30
|
icon "kanban-board"
|
|
31
31
|
layer "business"
|
|
32
32
|
|
|
33
|
-
relation :realizes, :
|
|
34
|
-
relation :realizes, :
|
|
33
|
+
relation :realizes, :motivationConstraints, :MotivationConstraint
|
|
34
|
+
relation :realizes, :motivationRequirements, :MotivationRequirement
|
|
35
35
|
relation :servedBy, :applicationServices, :ApplicationService
|
|
36
36
|
relation :performedBy, :businessActors, :BusinessActor
|
|
37
|
+
relation :guidedBy, :businessControls, :BusinessControl
|
|
37
38
|
end
|
|
@@ -33,7 +33,8 @@ class Archsight::Resources::BusinessProduct < Archsight::Resources::Base
|
|
|
33
33
|
computed_annotation "repository/artifacts/total",
|
|
34
34
|
title: "Total Git Repositories",
|
|
35
35
|
description: "Number of related git repositories",
|
|
36
|
-
type: Integer
|
|
36
|
+
type: Integer,
|
|
37
|
+
summary: true do
|
|
37
38
|
count(outgoing_transitive('TechnologyArtifact: artifact/type == "repo"'))
|
|
38
39
|
end
|
|
39
40
|
|
|
@@ -110,7 +111,7 @@ class Archsight::Resources::BusinessProduct < Archsight::Resources::Base
|
|
|
110
111
|
computed_annotation "activity/createdAt",
|
|
111
112
|
title: "Created",
|
|
112
113
|
description: "Earliest repository creation date across all related application services",
|
|
113
|
-
|
|
114
|
+
summary: true,
|
|
114
115
|
type: Time do
|
|
115
116
|
services = outgoing_transitive(:ApplicationService)
|
|
116
117
|
next nil if services.empty?
|
|
@@ -170,7 +171,7 @@ class Archsight::Resources::BusinessProduct < Archsight::Resources::Base
|
|
|
170
171
|
computed_annotation "activity/contributors/6m",
|
|
171
172
|
title: "Contributors (6 months)",
|
|
172
173
|
description: "Sum of unique contributors in the last 6 months across related services",
|
|
173
|
-
|
|
174
|
+
summary: true,
|
|
174
175
|
type: Integer do
|
|
175
176
|
services = outgoing_transitive(:ApplicationService)
|
|
176
177
|
next nil if services.empty?
|
|
@@ -197,8 +198,8 @@ class Archsight::Resources::BusinessProduct < Archsight::Resources::Base
|
|
|
197
198
|
end
|
|
198
199
|
|
|
199
200
|
relation :realizes, :strategyCapabilities, :StrategyCapability
|
|
200
|
-
relation :realizes, :
|
|
201
|
-
relation :realizes, :
|
|
201
|
+
relation :realizes, :motivationConstraints, :MotivationConstraint
|
|
202
|
+
relation :realizes, :motivationRequirements, :MotivationRequirement
|
|
202
203
|
relation :servedBy, :businessActors, :BusinessActor
|
|
203
204
|
relation :servedBy, :applicationServices, :ApplicationService
|
|
204
205
|
relation :exposes, :applicationInterfaces, :ApplicationInterface
|
|
@@ -25,6 +25,13 @@ class Archsight::Resources::ComplianceEvidence < Archsight::Resources::Base
|
|
|
25
25
|
- Test results and reports
|
|
26
26
|
- Configuration documentation
|
|
27
27
|
- Process documentation
|
|
28
|
+
|
|
29
|
+
## Evidence of what
|
|
30
|
+
|
|
31
|
+
- **Of an application:** linked with `evidencedBy` from a service, component or technology element; it says how
|
|
32
|
+
that resource meets the requirement it `satisfies`
|
|
33
|
+
- **Of a control:** linked with `evidencedBy` from a `BusinessControl`; the records the control produces
|
|
34
|
+
(reviews, diagrams, change history, audit logs). Use `evidence/type` `process`, `documentation` or `audit-log`
|
|
28
35
|
MD
|
|
29
36
|
|
|
30
37
|
icon "shield-check"
|
|
@@ -32,11 +39,41 @@ class Archsight::Resources::ComplianceEvidence < Archsight::Resources::Base
|
|
|
32
39
|
|
|
33
40
|
annotation "evidence/type",
|
|
34
41
|
description: "Type of evidence",
|
|
35
|
-
enum: %w[documentation process configuration audit-log technical-control]
|
|
42
|
+
enum: %w[documentation process configuration audit-log technical-control],
|
|
43
|
+
summary: true
|
|
36
44
|
|
|
37
45
|
annotation "evidence/status",
|
|
38
46
|
description: "Current status of evidence",
|
|
39
|
-
enum: %w[implemented partial not-implemented]
|
|
47
|
+
enum: %w[implemented partial not-implemented],
|
|
48
|
+
summary: true
|
|
49
|
+
|
|
50
|
+
# The structured answer to "how does the evidenced resource meet the requirement": one markdown field per
|
|
51
|
+
# question. `architecture/description` stays a short summary; the fields hold the detail.
|
|
52
|
+
annotation "evidence/mechanism",
|
|
53
|
+
description: "How the requirement is implemented: the concrete mechanism and where it lives " \
|
|
54
|
+
"(code, chart, configuration), as a short markdown list",
|
|
55
|
+
format: :markdown
|
|
56
|
+
|
|
57
|
+
annotation "evidence/coverage",
|
|
58
|
+
description: "What the mechanism covers and what it does not (data, flows, tenants, environments)",
|
|
59
|
+
format: :markdown
|
|
60
|
+
|
|
61
|
+
annotation "evidence/operatorView",
|
|
62
|
+
description: "Whether the mechanism holds against operators (admins, platform staff) or only against " \
|
|
63
|
+
"other tenants; names the privileged paths",
|
|
64
|
+
format: :markdown
|
|
65
|
+
|
|
66
|
+
annotation "evidence/verification",
|
|
67
|
+
description: "How effectiveness is verified: tests, audits, documents; says so when there are none",
|
|
68
|
+
format: :markdown
|
|
69
|
+
|
|
70
|
+
annotation "evidence/gaps",
|
|
71
|
+
description: "Remaining gaps and open questions, the most important first",
|
|
72
|
+
format: :markdown
|
|
73
|
+
|
|
74
|
+
annotation "evidence/sources",
|
|
75
|
+
description: "Where the statements come from: repositories and files, wiki pages, tickets",
|
|
76
|
+
format: :markdown
|
|
40
77
|
|
|
41
|
-
relation :satisfies, :
|
|
78
|
+
relation :satisfies, :motivationRequirements, :MotivationRequirement
|
|
42
79
|
end
|
|
@@ -38,12 +38,13 @@ class Archsight::Resources::DataObject < Archsight::Resources::Base
|
|
|
38
38
|
annotation "data/visibility",
|
|
39
39
|
description: "API visibility level",
|
|
40
40
|
title: "Visibility",
|
|
41
|
-
enum: %w[public private internal]
|
|
41
|
+
enum: %w[public private internal],
|
|
42
|
+
summary: true
|
|
42
43
|
|
|
43
44
|
annotation "generated/variants",
|
|
44
45
|
description: "OpenAPI schema variants compacted into this DataObject",
|
|
45
46
|
title: "Schema Variants",
|
|
46
47
|
sidebar: false
|
|
47
48
|
|
|
48
|
-
relation :realizes, :
|
|
49
|
+
relation :realizes, :motivationConstraints, :MotivationConstraint
|
|
49
50
|
end
|
|
@@ -38,7 +38,9 @@ class Archsight::Resources::Import < Archsight::Resources::Base
|
|
|
38
38
|
# Handler selection
|
|
39
39
|
annotation "import/handler",
|
|
40
40
|
description: "Handler class name to execute this import",
|
|
41
|
-
title: "Handler",
|
|
41
|
+
title: "Handler",
|
|
42
|
+
summary: true,
|
|
43
|
+
enum: %w[
|
|
42
44
|
gitlab github repository
|
|
43
45
|
rest-api rest-api-index
|
|
44
46
|
jira-discover jira-metrics
|
|
@@ -57,7 +59,8 @@ class Archsight::Resources::Import < Archsight::Resources::Base
|
|
|
57
59
|
annotation "import/enabled",
|
|
58
60
|
description: "Whether this import is enabled",
|
|
59
61
|
title: "Enabled",
|
|
60
|
-
enum: %w[true false]
|
|
62
|
+
enum: %w[true false],
|
|
63
|
+
summary: true
|
|
61
64
|
|
|
62
65
|
annotation "import/priority",
|
|
63
66
|
description: "Execution priority (lower runs first among ready imports)",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
class Archsight::Resources::
|
|
3
|
+
# MotivationConstraint represents restrictions or limitations on architecture
|
|
4
|
+
class Archsight::Resources::MotivationConstraint < Archsight::Resources::Base
|
|
5
5
|
include_annotations :git, :architecture
|
|
6
6
|
|
|
7
7
|
description <<~MD
|
|
@@ -18,7 +18,7 @@ class Archsight::Resources::BusinessConstraint < Archsight::Resources::Base
|
|
|
18
18
|
|
|
19
19
|
## Usage
|
|
20
20
|
|
|
21
|
-
Use
|
|
21
|
+
Use MotivationConstraint to represent:
|
|
22
22
|
|
|
23
23
|
- Regulatory requirements (GDPR, SOX, PCI-DSS)
|
|
24
24
|
- Security policies
|
|
@@ -28,5 +28,5 @@ class Archsight::Resources::BusinessConstraint < Archsight::Resources::Base
|
|
|
28
28
|
MD
|
|
29
29
|
|
|
30
30
|
icon "prohibition"
|
|
31
|
-
layer "
|
|
31
|
+
layer "motivation"
|
|
32
32
|
end
|
|
@@ -33,5 +33,5 @@ class Archsight::Resources::MotivationGoal < Archsight::Resources::Base
|
|
|
33
33
|
|
|
34
34
|
relation :realizes, :outcomes, :MotivationOutcome
|
|
35
35
|
relation :refinedBy, :goals, :MotivationGoal
|
|
36
|
-
relation :realizes, :
|
|
36
|
+
relation :realizes, :motivationRequirements, :MotivationRequirement
|
|
37
37
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
class Archsight::Resources::
|
|
3
|
+
# MotivationRequirement represents functional or non-functional requirements
|
|
4
|
+
class Archsight::Resources::MotivationRequirement < Archsight::Resources::Base
|
|
5
5
|
include_annotations :git, :architecture
|
|
6
6
|
|
|
7
7
|
description <<~MD
|
|
@@ -18,34 +18,45 @@ class Archsight::Resources::BusinessRequirement < Archsight::Resources::Base
|
|
|
18
18
|
|
|
19
19
|
## Usage
|
|
20
20
|
|
|
21
|
-
Use
|
|
21
|
+
Use MotivationRequirement to represent:
|
|
22
22
|
|
|
23
23
|
- Compliance requirements (C5, ISO 27001)
|
|
24
24
|
- Security requirements
|
|
25
25
|
- Performance requirements
|
|
26
26
|
- Functional specifications
|
|
27
27
|
- Legal obligations (GDPR, NIS2)
|
|
28
|
+
|
|
29
|
+
## Who addresses it
|
|
30
|
+
|
|
31
|
+
A requirement is where the process side and the application side meet:
|
|
32
|
+
|
|
33
|
+
- **Process side:** a `BusinessControl` `satisfies` the requirement; a process is `guidedBy` the control
|
|
34
|
+
- **Application side:** applications `realize` or `plan` the requirement and are `evidencedBy` `ComplianceEvidence`,
|
|
35
|
+
which `satisfies` it
|
|
36
|
+
|
|
37
|
+
Both show as incoming relations on the requirement's page.
|
|
28
38
|
MD
|
|
29
39
|
|
|
30
40
|
icon "task-list"
|
|
31
|
-
layer "
|
|
41
|
+
layer "motivation"
|
|
32
42
|
|
|
33
43
|
annotation "requirement/type",
|
|
34
44
|
description: "Type of requirement (business or legal)",
|
|
35
|
-
enum: %w[business legal compliance functional non-functional]
|
|
45
|
+
enum: %w[business legal compliance functional non-functional],
|
|
46
|
+
summary: true
|
|
36
47
|
|
|
37
48
|
annotation "requirement/reference",
|
|
38
49
|
description: "Regulatory or standard reference (comma-separated for multiple)",
|
|
39
50
|
filter: :list,
|
|
40
51
|
enum: %w[c5-2020 itgs-2023 gdpr-2018 nis1 nis2 iso27001 sox pci-dss hipaa eu-data-act-2025 ens
|
|
41
|
-
iso27001-2022],
|
|
42
|
-
|
|
52
|
+
iso27001-2022 vsa-2023 con-11-1],
|
|
53
|
+
summary: true
|
|
43
54
|
|
|
44
55
|
annotation "requirement/priority",
|
|
45
56
|
description: "Implementation priority (must, should, may)",
|
|
46
57
|
filter: :word,
|
|
47
58
|
enum: %w[must should may],
|
|
48
|
-
|
|
59
|
+
summary: true
|
|
49
60
|
|
|
50
61
|
annotation "requirement/story",
|
|
51
62
|
description: "One-line business value statement explaining what the requirement enables",
|
|
@@ -32,7 +32,7 @@ class Archsight::Resources::MotivationStakeholder < Archsight::Resources::Base
|
|
|
32
32
|
layer "motivation"
|
|
33
33
|
|
|
34
34
|
relation :hasConcern, :strategyCapabilities, :StrategyCapability
|
|
35
|
-
relation :hasConcern, :
|
|
36
|
-
relation :hasConcern, :
|
|
35
|
+
relation :hasConcern, :motivationRequirements, :MotivationRequirement
|
|
36
|
+
relation :hasConcern, :motivationConstraints, :MotivationConstraint
|
|
37
37
|
relation :hasConcern, :goals, :MotivationGoal
|
|
38
38
|
end
|
|
@@ -38,11 +38,13 @@ class Archsight::Resources::Page < Archsight::Resources::Base
|
|
|
38
38
|
description: "Owner responsible for keeping the page up to date (Name <email@domain.com>, or just a name)",
|
|
39
39
|
title: "Owner",
|
|
40
40
|
type: Archsight::Annotations::Person,
|
|
41
|
-
sidebar: false
|
|
41
|
+
sidebar: false,
|
|
42
|
+
summary: true
|
|
42
43
|
annotation "page/status",
|
|
43
44
|
description: "Lifecycle status (e.g. rfc, wip, approved)",
|
|
44
45
|
filter: :word,
|
|
45
|
-
title: "Status"
|
|
46
|
+
title: "Status",
|
|
47
|
+
summary: true
|
|
46
48
|
annotation "page/tags",
|
|
47
49
|
description: "Comma-separated tags",
|
|
48
50
|
filter: :list,
|
|
@@ -30,8 +30,8 @@ class Archsight::Resources::StrategyCapability < Archsight::Resources::Base
|
|
|
30
30
|
icon "strategy"
|
|
31
31
|
layer "strategy"
|
|
32
32
|
|
|
33
|
-
relation :realizes, :
|
|
34
|
-
relation :realizes, :
|
|
33
|
+
relation :realizes, :motivationConstraints, :MotivationConstraint
|
|
34
|
+
relation :realizes, :motivationRequirements, :MotivationRequirement
|
|
35
35
|
relation :servedBy, :businessActors, :BusinessActor
|
|
36
36
|
relation :servedBy, :applicationServices, :ApplicationService
|
|
37
37
|
relation :servedBy, :businessProcesses, :BusinessProcess
|
|
@@ -74,7 +74,7 @@ class Archsight::Resources::TechnologyArtifact < Archsight::Resources::Base
|
|
|
74
74
|
description: "Repository activity status",
|
|
75
75
|
title: "Activity Status",
|
|
76
76
|
enum: %w[active abandoned bot-only archived inaccessible empty no-code],
|
|
77
|
-
|
|
77
|
+
summary: true
|
|
78
78
|
annotation "activity/reason",
|
|
79
79
|
description: "Reason for activity status (for non-standard statuses)",
|
|
80
80
|
title: "Status Reason",
|
|
@@ -83,7 +83,7 @@ class Archsight::Resources::TechnologyArtifact < Archsight::Resources::Base
|
|
|
83
83
|
description: "Bus factor assessment",
|
|
84
84
|
title: "Bus Factor",
|
|
85
85
|
enum: %w[high medium low unknown],
|
|
86
|
-
|
|
86
|
+
summary: true
|
|
87
87
|
annotation "activity/createdAt",
|
|
88
88
|
description: "Date of first commit (repository creation)",
|
|
89
89
|
title: "Created",
|
|
@@ -130,7 +130,8 @@ class Archsight::Resources::TechnologyArtifact < Archsight::Resources::Base
|
|
|
130
130
|
annotation "repository/visibility",
|
|
131
131
|
description: "Repository visibility classification",
|
|
132
132
|
title: "Visibility",
|
|
133
|
-
enum: %w[private internal open-source public]
|
|
133
|
+
enum: %w[private internal open-source public],
|
|
134
|
+
summary: true
|
|
134
135
|
annotation "repository/recentTags",
|
|
135
136
|
description: "Recent git tags (releases)",
|
|
136
137
|
title: "Recent Tags",
|
|
@@ -34,9 +34,9 @@ class Archsight::Resources::TechnologyNode < Archsight::Resources::Base
|
|
|
34
34
|
description: "Type of infrastructure node",
|
|
35
35
|
title: "Infrastructure Type",
|
|
36
36
|
enum: %w[vm bare-metal kubernetes-node network-appliance storage-array],
|
|
37
|
-
|
|
37
|
+
summary: true
|
|
38
38
|
|
|
39
|
-
relation :realizes, :
|
|
39
|
+
relation :realizes, :motivationConstraints, :MotivationConstraint
|
|
40
40
|
relation :servedBy, :technologyServices, :TechnologyService
|
|
41
41
|
relation :servedBy, :businessActors, :BusinessActor
|
|
42
42
|
end
|
|
@@ -37,4 +37,8 @@ class Archsight::Resources::TechnologyService < Archsight::Resources::Base
|
|
|
37
37
|
|
|
38
38
|
relation :suppliedBy, :technologyComponents, :TechnologySystemSoftware
|
|
39
39
|
relation :servedBy, :businessActors, :BusinessActor
|
|
40
|
+
relation :realizes, :motivationRequirements, :MotivationRequirement
|
|
41
|
+
relation :partiallyRealizes, :motivationRequirements, :MotivationRequirement
|
|
42
|
+
relation :plans, :motivationRequirements, :MotivationRequirement
|
|
43
|
+
relation :evidencedBy, :complianceEvidences, :ComplianceEvidence
|
|
40
44
|
end
|
|
@@ -34,4 +34,8 @@ class Archsight::Resources::TechnologySystemSoftware < Archsight::Resources::Bas
|
|
|
34
34
|
relation :realizedThrough, :technologyArtifacts, :TechnologyArtifact
|
|
35
35
|
relation :exposes, :applicationInterfaces, :ApplicationInterface
|
|
36
36
|
relation :dependsOn, :applicationInterfaces, :ApplicationInterface
|
|
37
|
+
relation :realizes, :motivationRequirements, :MotivationRequirement
|
|
38
|
+
relation :partiallyRealizes, :motivationRequirements, :MotivationRequirement
|
|
39
|
+
relation :plans, :motivationRequirements, :MotivationRequirement
|
|
40
|
+
relation :evidencedBy, :complianceEvidences, :ComplianceEvidence
|
|
37
41
|
end
|
|
@@ -42,7 +42,8 @@ class Archsight::Resources::View < Archsight::Resources::Base
|
|
|
42
42
|
description: "Display type for results",
|
|
43
43
|
title: "Display Type",
|
|
44
44
|
enum: %w[list:name list:name+kind],
|
|
45
|
-
sidebar: false
|
|
45
|
+
sidebar: false,
|
|
46
|
+
summary: true
|
|
46
47
|
|
|
47
48
|
annotation "view/sort",
|
|
48
49
|
description: 'Comma-separated list of fields to sort by. Prefix with - for descending (e.g., "-scc/language/Go/loc,name"). Special fields: name, kind',
|
data/lib/archsight/resources.rb
CHANGED
|
@@ -6,6 +6,37 @@ module Archsight
|
|
|
6
6
|
# Store the class mapping
|
|
7
7
|
@resource_classes = {}
|
|
8
8
|
|
|
9
|
+
# Kinds that were renamed: the old name is still accepted (see Database#create_valid_instance) and reported
|
|
10
|
+
# as deprecated by `archsight lint`. The table is only consulted by lookups; `each` and `resource_classes`
|
|
11
|
+
# list every kind once, under its new name. Remove an entry (and its relation keys below) to end the transition.
|
|
12
|
+
KIND_ALIASES = {
|
|
13
|
+
"BusinessRequirement" => "MotivationRequirement",
|
|
14
|
+
"BusinessConstraint" => "MotivationConstraint"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
# Relation keys (the plural kind names under `spec.<verb>`) that were renamed together with their kind
|
|
18
|
+
RELATION_KEY_ALIASES = {
|
|
19
|
+
"businessRequirements" => "motivationRequirements",
|
|
20
|
+
"businessConstraints" => "motivationConstraints"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
# Relations that are never written in a file: they are derived from what a resource's text and diagram say
|
|
24
|
+
# (see Archsight::References). `mentions` comes from links in text, `depicts` from the nodes of a diagram.
|
|
25
|
+
# Every kind can have them towards every kind, so instead of one relation per target kind a class gets one per
|
|
26
|
+
# verb, under the key DERIVED_KEY, that holds resources of any kind (relations are followed by looking at the
|
|
27
|
+
# resources they hold, not at the kind they were declared for).
|
|
28
|
+
DERIVED_VERBS = %i[mentions depicts].freeze
|
|
29
|
+
DERIVED_KEY = :resources
|
|
30
|
+
|
|
31
|
+
# The derived relations of every class, next to the ones it declares: [verb, key, kind]
|
|
32
|
+
DERIVED_RELATIONS = DERIVED_VERBS.map { |verb| [verb, DERIVED_KEY, "Resource"] }.freeze
|
|
33
|
+
|
|
34
|
+
# The current name of a kind, whichever of its names is given
|
|
35
|
+
def self.canonical(klass_name)
|
|
36
|
+
name = klass_name.to_s
|
|
37
|
+
KIND_ALIASES.fetch(name, name)
|
|
38
|
+
end
|
|
39
|
+
|
|
9
40
|
# Register a resource class
|
|
10
41
|
def self.register(klass)
|
|
11
42
|
# Skip anonymous classes (used in tests)
|
|
@@ -20,9 +51,9 @@ module Archsight
|
|
|
20
51
|
@resource_classes
|
|
21
52
|
end
|
|
22
53
|
|
|
23
|
-
# Returns the class by name
|
|
54
|
+
# Returns the class by name; a renamed kind is found under its old name too
|
|
24
55
|
def self.[](klass_name)
|
|
25
|
-
@resource_classes[klass_name
|
|
56
|
+
@resource_classes[canonical(klass_name)]
|
|
26
57
|
end
|
|
27
58
|
|
|
28
59
|
# Iterate over all resource class names (sorted)
|
|
@@ -32,7 +63,7 @@ module Archsight
|
|
|
32
63
|
|
|
33
64
|
# Get the constant by name (for backward compatibility with const_get)
|
|
34
65
|
def self.const_get(name)
|
|
35
|
-
@resource_classes[name
|
|
66
|
+
@resource_classes[canonical(name)] || super
|
|
36
67
|
end
|
|
37
68
|
end
|
|
38
69
|
end
|
data/lib/archsight/template.rb
CHANGED
|
@@ -35,10 +35,10 @@ module Archsight
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def add_relations(yaml, klass)
|
|
38
|
-
return if klass.
|
|
38
|
+
return if klass.declared_relations.empty?
|
|
39
39
|
|
|
40
40
|
yaml["spec"] = {} if yaml["spec"].nil?
|
|
41
|
-
klass.
|
|
41
|
+
klass.declared_relations.each do |verb, relation_kind, _relation_klass|
|
|
42
42
|
relation_verb = verb.to_s.delete_prefix(":")
|
|
43
43
|
yaml["spec"][relation_verb] ||= {}
|
|
44
44
|
yaml["spec"][relation_verb][relation_kind.to_s] = []
|
data/lib/archsight/version.rb
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archsight
|
|
4
|
+
# The result of a view as a plain table (columns and rows of text), for places that cannot run the frontend, such
|
|
5
|
+
# as the Confluence export. It follows what the web UI shows for a View (`ViewResults.vue`, `ResourceList.vue`):
|
|
6
|
+
# a Name column, a Kind column unless the view is `list:name`, one column per `view/fields` entry that is not
|
|
7
|
+
# `name` or `kind`, rows sorted by `view/sort`.
|
|
8
|
+
module ViewTable
|
|
9
|
+
# Rows beyond this are cut (the table says how many); a Confluence page is no place for thousands of rows
|
|
10
|
+
LIMIT = 200
|
|
11
|
+
|
|
12
|
+
# @param as [Symbol] :text, :status or :priority (a requirement's, shown as a lozenge) or :markdown
|
|
13
|
+
# @param resources [Array] resources the cell names (shown one per line, linked where a target exists)
|
|
14
|
+
Cell = Struct.new(:text, :resources, :as, keyword_init: true) do
|
|
15
|
+
def initialize(text: "", resources: [], as: :text) = super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param title [String] shown above the table, may be empty
|
|
19
|
+
# @param columns [Array<String>] header texts
|
|
20
|
+
# @param rows [Array<Array<Cell>>] at most LIMIT
|
|
21
|
+
# @param total [Integer] number of rows before cutting
|
|
22
|
+
Table = Struct.new(:title, :columns, :rows, :total, keyword_init: true) do
|
|
23
|
+
def cut = total - rows.length
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
IDENTITY_FIELDS = %w[name kind].freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# @raise [Archsight::Query::QueryError] if the query does not parse
|
|
31
|
+
def build(database, query:, fields: [], sort: [], show_kind: true, title: "", limit: LIMIT)
|
|
32
|
+
resources = sort_resources(Archsight::Query.parse(query).filter(database), sort)
|
|
33
|
+
columns = fields.reject { |f| IDENTITY_FIELDS.include?(f) }
|
|
34
|
+
header = ["Name", (show_kind ? "Kind" : nil), *columns.map { |f| column_title(f) }].compact
|
|
35
|
+
rows = resources.first(limit).map do |resource|
|
|
36
|
+
[Cell.new(text: resource.name, resources: [resource]),
|
|
37
|
+
(Cell.new(text: kind_of(resource)) if show_kind),
|
|
38
|
+
*columns.map { |f| Cell.new(text: resource.annotations[f].to_s) }].compact
|
|
39
|
+
end
|
|
40
|
+
Table.new(title: title, columns: header, rows: rows, total: resources.length)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The table of a ```view block (see Helpers::ViewBlocks)
|
|
44
|
+
# @raise [Helpers::ViewBlocks::Error, Archsight::Query::QueryError]
|
|
45
|
+
def from_block(database, source)
|
|
46
|
+
spec = Helpers::ViewBlocks.parse(source)
|
|
47
|
+
build(database, query: spec[:query], fields: spec[:fields], sort: spec[:sort], show_kind: spec[:type] == "list:name+kind", title: spec[:title])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The table of a View resource
|
|
51
|
+
# @raise [Helpers::ViewBlocks::Error, Archsight::Query::QueryError]
|
|
52
|
+
def from_view(database, view)
|
|
53
|
+
annotations = view.annotations
|
|
54
|
+
query = annotations["view/query"].to_s.strip
|
|
55
|
+
raise Helpers::ViewBlocks::Error, "view #{view.name} has no query" if query.empty?
|
|
56
|
+
|
|
57
|
+
build(database, query: query, fields: list(annotations["view/fields"]), sort: list(annotations["view/sort"]),
|
|
58
|
+
show_kind: (annotations["view/type"] || "list:name+kind") == "list:name+kind", title: view.name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# "scc/language/Go/loc" -> "Go loc", "activity/createdAt" -> "Activity created At" (as ResourceList.vue titles columns)
|
|
62
|
+
def column_title(key)
|
|
63
|
+
segments = key.split("/")
|
|
64
|
+
title = segments.length >= 2 ? "#{segments[-2]} #{segments[-1]}" : segments.last.to_s
|
|
65
|
+
title.gsub(/([a-z])([A-Z])/, '\1 \2').sub(/\A./, &:upcase)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def kind_of(resource) = resource.class.name.split("::").last
|
|
69
|
+
|
|
70
|
+
def list(value) = value.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
71
|
+
|
|
72
|
+
# `view/sort` fields: name, kind or an annotation, "-" for descending; numbers inside text compare as numbers.
|
|
73
|
+
# Without a sort the order is by name, like the search API.
|
|
74
|
+
def sort_resources(resources, sort)
|
|
75
|
+
return resources.sort_by(&:name) if sort.empty?
|
|
76
|
+
|
|
77
|
+
resources.sort { |left, right| compare(left, right, sort) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def compare(left, right, sort)
|
|
81
|
+
sort.each do |field|
|
|
82
|
+
key = field.delete_prefix("-")
|
|
83
|
+
order = natural(value_of(left, key)) <=> natural(value_of(right, key))
|
|
84
|
+
return field.start_with?("-") ? -order : order unless order.zero?
|
|
85
|
+
end
|
|
86
|
+
0
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def value_of(resource, key)
|
|
90
|
+
case key
|
|
91
|
+
when "name" then resource.name
|
|
92
|
+
when "kind" then kind_of(resource)
|
|
93
|
+
else resource.annotations[key].to_s
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# A key for sorting text with numbers in it the way people expect (item2 < item10, case-insensitive first)
|
|
98
|
+
def natural(text)
|
|
99
|
+
[text.scan(/\d+|\D+/).map { |part| part.match?(/\A\d/) ? [0, part.to_i] : [1, part.downcase] }, text]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|