archsight 0.4.0 → 0.4.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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/docs/configuration.md +1 -1
  3. data/docs/modeling.md +39 -10
  4. data/docs/pages.md +2 -2
  5. data/lib/archsight/analysis/typst_renderer.rb +2 -3
  6. data/lib/archsight/export/confluence/analysis_output.rb +3 -4
  7. data/lib/archsight/export/confluence/exporter.rb +3 -1
  8. data/lib/archsight/export/confluence/page_properties.rb +2 -3
  9. data/lib/archsight/export/confluence/storage.rb +25 -12
  10. data/lib/archsight/export/confluence/tables.rb +2 -3
  11. data/lib/archsight/git_source.rb +81 -0
  12. data/lib/archsight/helpers/asset_images.rb +3 -4
  13. data/lib/archsight/import/executor.rb +1 -0
  14. data/lib/archsight/import.rb +27 -2
  15. data/lib/archsight/linter/structure.rb +4 -1
  16. data/lib/archsight/linter.rb +2 -2
  17. data/lib/archsight/markdown.rb +48 -0
  18. data/lib/archsight/openapi.rb +169 -0
  19. data/lib/archsight/page_tree.rb +2 -3
  20. data/lib/archsight/resources/application_component.rb +8 -0
  21. data/lib/archsight/resources/application_interface.rb +12 -0
  22. data/lib/archsight/resources/technology_communication_network.rb +2 -0
  23. data/lib/archsight/resources/technology_device.rb +56 -4
  24. data/lib/archsight/resources/technology_service.rb +7 -0
  25. data/lib/archsight/version.rb +1 -1
  26. data/lib/archsight/web/api/docs.rb +2 -1
  27. data/lib/archsight/web/api/openapi/spec.yaml +136 -27
  28. data/lib/archsight/web/api/routes.rb +2 -2
  29. data/lib/archsight/web/application.rb +2 -2
  30. data/lib/archsight/web/public/vendor/redoc/LICENSE +22 -0
  31. data/lib/archsight/web/public/vendor/redoc/VERSION +1 -0
  32. data/lib/archsight/web/public/vendor/redoc/api.html +27 -0
  33. data/lib/archsight/web/public/vendor/redoc/api.js +19 -0
  34. data/lib/archsight/web/public/vendor/redoc/redoc.standalone.js +1851 -0
  35. data/lib/archsight/web/public/vendor/redoc/redoc.standalone.js.LICENSE.txt +102 -0
  36. data/lib/archsight/web/public/vue/{DocPage-D52OwaKc.js → DocPage-C8Ysway-.js} +1 -1
  37. data/lib/archsight/web/public/vue/EditorPage-pr2MR6_b.js +35 -0
  38. data/lib/archsight/web/public/vue/ErrorPage-FAkpW-hn.js +2 -0
  39. data/lib/archsight/web/public/vue/GraphView-liQJzFyo.js +1 -0
  40. data/lib/archsight/web/public/vue/HomePage-DYpzSXG8.js +2 -0
  41. data/lib/archsight/web/public/vue/InstanceRouter-BdkBTmGO.js +1 -0
  42. data/lib/archsight/web/public/vue/{InstanceRouter-ZaCcjlIX.css → InstanceRouter-Ca_C8izS.css} +1 -1
  43. data/lib/archsight/web/public/vue/KindList-CrgI4qDb.js +1 -0
  44. data/lib/archsight/web/public/vue/PageView-BIxT_t49.js +1 -0
  45. data/lib/archsight/web/public/vue/QueryError-ChdyV8B1.js +1 -0
  46. data/lib/archsight/web/public/vue/ResourceList-DdtaHY4k.js +2 -0
  47. data/lib/archsight/web/public/vue/SearchResults-BYRAcK_o.js +1 -0
  48. data/lib/archsight/web/public/vue/WikiPage-CgvRN6R2.js +3 -0
  49. data/lib/archsight/web/public/vue/architecture-WOLXFQ4H-BGN-TV5r.js +1 -0
  50. data/lib/archsight/web/public/vue/cynefin-EF2NZ3EQ-CZd6g_tq.js +1 -0
  51. data/lib/archsight/web/public/vue/eventmodeling-K75KTNOO-CYnbEx8O.js +1 -0
  52. data/lib/archsight/web/public/vue/gitGraph-VSP46ZUC-DialfQSk.js +1 -0
  53. data/lib/archsight/web/public/vue/graphviz-1lWBhpEX.js +3 -0
  54. data/lib/archsight/web/public/vue/{index-eUHmJ7K2.css → index-BLSE5DSv.css} +1 -1
  55. data/lib/archsight/web/public/vue/index-yiZbbvAs.js +3 -0
  56. data/lib/archsight/web/public/vue/info-OHQRW6UA-BuhOW7ss.js +1 -0
  57. data/lib/archsight/web/public/vue/katex-B_JcIwqj.js +257 -0
  58. data/lib/archsight/web/public/vue/mermaid-C3bOamfn.js +3999 -0
  59. data/lib/archsight/web/public/vue/packet-JDAUHWVQ-CUeRz7w4.js +1 -0
  60. data/lib/archsight/web/public/vue/pie-XZMESJXO-DnuewJI6.js +1 -0
  61. data/lib/archsight/web/public/vue/radar-ABXABTNO-Bi9l8bcv.js +1 -0
  62. data/lib/archsight/web/public/vue/railroad-I3PHUGI6-DSlxFbxk.js +1 -0
  63. data/lib/archsight/web/public/vue/railroad-abnf-LNEFI6M7-D6Kqu8wu.js +1 -0
  64. data/lib/archsight/web/public/vue/railroad-ebnf-DWYD2UWJ-MR7uCYT0.js +1 -0
  65. data/lib/archsight/web/public/vue/railroad-peg-JR7BVNR7-BL7aKIc-.js +1 -0
  66. data/lib/archsight/web/public/vue/rolldown-runtime-COiEfhsA.js +1 -0
  67. data/lib/archsight/web/public/vue/treeView-D4JQ5SDB-CamtB04D.js +1 -0
  68. data/lib/archsight/web/public/vue/treemap-SAJKECNS-DeQxu_HM.js +1 -0
  69. data/lib/archsight/web/public/vue/{useGraphviz-4evusK9i.js → useGraphviz-Dnm2pkon.js} +2 -2
  70. data/lib/archsight/web/public/vue/useHighlight-BH32C7yt.js +10 -0
  71. data/lib/archsight/web/public/vue/usePanZoom-HxgEsqq2.js +12 -0
  72. data/lib/archsight/web/public/vue/wardley-7MLQ67FV-BvKIB6Tv.js +1 -0
  73. data/lib/archsight/web/public/vue.html +4 -4
  74. metadata +47 -40
  75. data/lib/archsight/web/public/vue/ApiDocsPage-COp4onwn.js +0 -1
  76. data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +0 -1
  77. data/lib/archsight/web/public/vue/EditorPage-3aqL7yVf.js +0 -35
  78. data/lib/archsight/web/public/vue/ErrorPage--b9XeSYa.js +0 -2
  79. data/lib/archsight/web/public/vue/GraphView-qh_EtRrQ.js +0 -1
  80. data/lib/archsight/web/public/vue/HomePage-C2TNh2U-.js +0 -2
  81. data/lib/archsight/web/public/vue/InstanceRouter-uzb_OMG0.js +0 -1
  82. data/lib/archsight/web/public/vue/KindList-CyJuP_uq.js +0 -1
  83. data/lib/archsight/web/public/vue/PageView-FyygrS-b.js +0 -1
  84. data/lib/archsight/web/public/vue/QueryError-B4Ex_csZ.js +0 -1
  85. data/lib/archsight/web/public/vue/ResourceList-BY6Z1Kcb.js +0 -2
  86. data/lib/archsight/web/public/vue/SearchResults-CkFpHF8m.js +0 -1
  87. data/lib/archsight/web/public/vue/WikiPage-BoHNewCX.js +0 -3
  88. data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BvzKZE4e.js +0 -1
  89. data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-D-xs_Hxh.js +0 -1
  90. data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-nbjxG01S.js +0 -1
  91. data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-CW6mw2ab.js +0 -1
  92. data/lib/archsight/web/public/vue/graphviz-SXWTgZH5.js +0 -3
  93. data/lib/archsight/web/public/vue/index-BGlYCbi7.js +0 -3
  94. data/lib/archsight/web/public/vue/info-A6RAGUB7-C12sjlHe.js +0 -1
  95. data/lib/archsight/web/public/vue/katex-CXMH3UgJ.js +0 -257
  96. data/lib/archsight/web/public/vue/mermaid-DjsXQ_XN.js +0 -3334
  97. data/lib/archsight/web/public/vue/packet-AYTQ26CC-wMwKwHmA.js +0 -1
  98. data/lib/archsight/web/public/vue/pie-WAS4IAKB-w1-sAU_K.js +0 -1
  99. data/lib/archsight/web/public/vue/radar-RG4KPBEZ-DpwqIFoy.js +0 -1
  100. data/lib/archsight/web/public/vue/railroad-74A4TZTK-Ce5XC24d.js +0 -1
  101. data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-BSQO2pds.js +0 -1
  102. data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-Dq1c2YtD.js +0 -1
  103. data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DKj6dli0.js +0 -1
  104. data/lib/archsight/web/public/vue/rolldown-runtime-hePW80VL.js +0 -1
  105. data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-BmHNTWxU.js +0 -1
  106. data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DsEI7sSD.js +0 -1
  107. data/lib/archsight/web/public/vue/useHighlight-DPguaxfm.js +0 -10
  108. data/lib/archsight/web/public/vue/usePanZoom-seQE1TPk.js +0 -12
  109. data/lib/archsight/web/public/vue/wardley-WFR3VGLG-CSCW83-M.js +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00045b0530964a0ff8be08ceef27b47fe7e91b630ba09a243b087f4bce523add
4
- data.tar.gz: 0abb8b4cf7d274318451a094f40a48992e6759ac2d56f6a5f70fd8d18e59cd06
3
+ metadata.gz: c249926b6ff6855b19ace38f11b9f256340e4d77cd00fe4d5f981fcbb0fd12f1
4
+ data.tar.gz: a0d7267da0223f8faed9a478a2c5ca029cdc23205fdff4feb945adb4d2b78f2e
5
5
  SHA512:
6
- metadata.gz: 756394456261aca49ea0c828d9bf61996a8175da001c335ac802e06f9232641a587732765140395dc55e224a6f6d5d4facda26cdb26ddde27bb8272a6b3d09ee
7
- data.tar.gz: 2f9584331b16063497e712f4b48b20abec72e946932af5986418d8e137ffa4eb55f393e05802974a7eec47edff0d6cf84b6de6d2614b2b553f8bcad51ebeed27
6
+ metadata.gz: 18ae964be21eded1e7be2560cd70a0ea63cb714c76b2d5295b3ec4b1f2b1fc46f5ec418591f53c5856babe389696b96be1436e905ee5ee2bc4b922ecee43d816
7
+ data.tar.gz: c8c8fbcba5d1f9db3b9d22c9fb2b1182569354fd08e2fd2cf44ba0bfe4b787ffac0446d48db5bd802f01269ea9ff6746383b2887bdf0a0f4b8d9e97e2ee2f9f6
@@ -37,7 +37,7 @@ that is set but empty counts as not set.
37
37
  | `jira.server` | `ARCHSIGHT_JIRA_SERVER` | Name of the Jira server as Confluence knows it (the `server` parameter of its Jira macro). Needed by the Confluence export when Confluence has more than one Jira server: without it the exported `{jira:KEY}` macros have no server and Confluence shows an error |
38
38
  | `jira.server_id` | `ARCHSIGHT_JIRA_SERVER_ID` | Id of that server (the `serverId` parameter). Copy both from the storage format of a Jira macro that already works in Confluence (page "Edit in storage format") |
39
39
  | `jira.token` | `ARCHSIGHT_JIRA_TOKEN` | Reserved, nothing uses it yet |
40
- | `import.cache_root` | `ARCHSIGHT_IMPORT_CACHE_ROOT` | Where the GitHub, GitLab and repository imports clone repositories, and what importers of the resources repository get as `cache_root` / `ARCHSIGHT_CACHE_DIR`. Default `~/.cache/archsight/git`; an import's own `import/config/cacheRoot` wins, see [Importers of the Resources Repository](/doc/import#importers-of-the-resources-repository) |
40
+ | `import.cache_root` | `ARCHSIGHT_IMPORT_CACHE_ROOT` | Where the GitHub, GitLab and repository imports clone repositories, and what importers of the resources repository get as `cache_root` / `ARCHSIGHT_CACHE_DIR`. Default `~/.cache/archsight/git`; an import's own `import/config/cacheRoot` wins, see [Importers of the Resources Repository](/doc/import#importers-of-the-resources-repository). Every import makes sure the clones are not indexed by Spotlight (macOS): an empty `.metadata_never_index` in `~/.cache/archsight`, or in the cache root when it is elsewhere |
41
41
 
42
42
  ## Command line
43
43
 
data/docs/modeling.md CHANGED
@@ -36,7 +36,7 @@ There are two families of verbs:
36
36
 
37
37
  | Family | Verbs | Written on | Points to |
38
38
  |--------|-------|------------|-----------|
39
- | Responsibility and provider (down) | `performedBy`, `ownedBy`, `executedBy`, `servedBy`, `guidedBy`, `maintainedBy`, `contributedBy`, `hasConcern`, `representedBy`, `realizedThrough`, `exposes`, `connects`, `mitigatedBy`, `influences`, `triggers`, `affects`, `assesses`, `contains`, `closedBy`, `compares` | the element that needs, uses or is concerned with something | what provides it or is responsible for it |
39
+ | Responsibility and provider (down) | `performedBy`, `ownedBy`, `executedBy`, `servedBy`, `guidedBy`, `maintainedBy`, `contributedBy`, `hasConcern`, `representedBy`, `realizedThrough`, `exposes`, `provisions`, `connects`, `mitigatedBy`, `influences`, `triggers`, `affects`, `assesses`, `contains`, `closedBy`, `compares` | the element that needs, uses or is concerned with something | what provides it or is responsible for it |
40
40
  | Realization (up) | `realizes`, `partiallyRealizes`, `plans`, `satisfies`, `evidencedBy` (on the evidenced element), `provides` | the concrete element | the abstract element it answers to (requirement, goal, capability, plateau) |
41
41
 
42
42
  Rules:
@@ -154,7 +154,7 @@ TechnologySystemSoftware, TechnologyService, TechnologyArtifact). The same princ
154
154
  | Location | In which region or country is it? | A place on a map, not a building; holds facilities | the place ("Location:Germany:Frankfurt") |
155
155
  | TechnologyFacility | In which building or site does hardware stand? | Has an address and can be visited; can be nested (campus, hall) | the site ("Facility:Frankfurt:HallA") |
156
156
  | TechnologyEquipment | Which physical thing can you touch? | Racks, fabrics, fibre routes, carrier circuits; no software of its own | the thing and its place ("Equipment:Link:Frankfurt-Madrid") |
157
- | TechnologyDevice | Which single machine stands in the room? | One server, switch, router, firewall or appliance with a serial number; it is cabled | the host ("Device:Frankfurt:Server01") |
157
+ | TechnologyDevice | Which single machine stands in the room? | One server, switch, router, firewall or appliance with a serial number (it is cabled), or one laptop, workstation or phone of a person or team | the host ("Device:Frankfurt:Server01") |
158
158
  | TechnologyCommunicationNetwork | What carries data between nodes? | Has a network role (`network/type`); exists beyond any single link | role and scope ("Network:Management:Frankfurt") |
159
159
  | TechnologyNode | On which machine does software run? | A VM, host role or logical group of machines with a lifecycle of its own | the host or host role ("Node:Frankfurt:Web01") |
160
160
  | TechnologySystemSoftware | What software hosts other software or data? | An operating system, database cluster, routing stack, platform | the product and role ("Tech:Routing:EVPN") |
@@ -166,8 +166,8 @@ TechnologySystemSoftware, TechnologyService, TechnologyArtifact). The same princ
166
166
  1. Is it a country, metro or region? Location.
167
167
  2. Is it a building, campus, hall or site that can be visited? TechnologyFacility.
168
168
  3. Is it a physical thing (rack, switch fabric, fibre, circuit) without software of its own? TechnologyEquipment.
169
- 4. Is it one physical machine (server, switch, router, firewall, appliance) whose room or cabling matters?
170
- TechnologyDevice.
169
+ 4. Is it one physical machine (server, switch, router, firewall, appliance, laptop, workstation, phone) whose room,
170
+ cabling or operating system matters? TechnologyDevice.
171
171
  5. Does it carry data between nodes, with a role such as underlay, overlay, management, public or VPN?
172
172
  TechnologyCommunicationNetwork.
173
173
  6. Is it a machine (virtual, or a host role) that software runs on? TechnologyNode.
@@ -190,6 +190,9 @@ TechnologySystemSoftware, TechnologyService, TechnologyArtifact). The same princ
190
190
  with `composedOf` (metro network → site networks, campus → halls, backbone → links).
191
191
  7. A network that supports a metro region is `locatedAt` the Location and `composedOf` the networks of its sites.
192
192
  8. Nodes attach to networks with `servedBy` and stand in facilities with `locatedAt`.
193
+ An `ApplicationInterface` is `servedBy` the networks on which it can be reached. Write them only when they are
194
+ known: an interface without a network makes no claim about its reachability, and an analysis cannot tell "not
195
+ reachable" from "not recorded".
193
196
  9. An overlay that software builds (an SDN agent wiring tenant networks on each host, an operator creating VXLAN
194
197
  segments, a load-balancer controller announcing public addresses) is `realizedBy` that `ApplicationComponent` or
195
198
  `ApplicationService` (ArchiMate cross-layer realization). This holds even when the software runs on the nodes the
@@ -198,7 +201,8 @@ TechnologySystemSoftware, TechnologyService, TechnologyArtifact). The same princ
198
201
  10. One `TechnologyDevice` per physical machine, `locatedAt` its room (a TechnologyFacility of `facility/type: room`
199
202
  the data center is `composedOf`). A group of machines with one role stays a logical `TechnologyNode` that is
200
203
  `realizedBy` its devices; a rack or hardware block is TechnologyEquipment `composedOf` them; a virtual chassis or
201
- stack is a device `composedOf` its members.
204
+ stack is a device `composedOf` its members. A client device (`device/type` laptop, desktop, workstation,
205
+ thin-client, tablet, mobile) moves and needs no room; it may be `locatedAt` an office.
202
206
  11. A network is `realizedBy` the devices that build it (switches, routers) and `connects` the devices attached to it.
203
207
  Both are written on the network, so a switch attached to the network it builds closes no cycle.
204
208
  12. Cables are `connectedTo` between devices, declared once on one of the two ends. Cabling runs both ways by nature
@@ -210,6 +214,17 @@ TechnologySystemSoftware, TechnologyService, TechnologyArtifact). The same princ
210
214
  `servedBy` its team and `composedOf` its devices, which then need no team of their own. Both relations point
211
215
  down to the team, which points to nothing further. On a TechnologyFacility, `servedBy` stays the operator of
212
216
  the site.
217
+ 14. A client device (developer workstation, laptop, phone) is a `TechnologyDevice` with a client `device/type` and
218
+ `device/os`, `device/osVersion`, `device/arch`, `device/ownership`, `device/managed` and `device/encryption`; the
219
+ vendor and product are in `device/model` (also for a hardened workstation that runs the user's operating system
220
+ virtualized beside other isolated sessions, which is a `workstation`). Model the individual machine when it is
221
+ inventoried, otherwise one `TechnologyNode` per client platform `realizedBy` the devices. The operating system is
222
+ `TechnologySystemSoftware` `realizedBy` the node, the image it is installed from an artifact the node is
223
+ `assignedTo`; each isolated session of a hardened workstation can be a node of its own. Network access is written
224
+ on the node (`servedBy` the networks it uses: the office, the VPN) or, for a single named machine, on the network
225
+ (`connects`), never on the device, because a network is `realizedBy` its switches and the reverse arrow would
226
+ close a cycle. A machine that reaches no network (air-gapped) has neither relation. Who uses it is the team or role
227
+ (`servedBy`), not a person; groups of machines are tags and the end of support is `lifecycle/endOfLifeAt`.
213
228
 
214
229
  **Checked by `archsight lint`**
215
230
 
@@ -221,7 +236,7 @@ As above, these are **warnings** in the "Modeling warnings" section and never fa
221
236
  | `TechnologyCommunicationNetwork 'Network:Overlay:EVPN' is an overlay network served by no network` | 3 |
222
237
  | `TechnologyFacility 'Facility:Madrid:DC' is located at no Location` | where |
223
238
  | `TechnologyEquipment 'Equipment:Rack01' is located at no TechnologyFacility` | where |
224
- | `TechnologyDevice 'Device:Server01' is located at no TechnologyFacility` | 10 |
239
+ | `TechnologyDevice 'Device:Server01' is located at no TechnologyFacility` (not for client device types) | 10 |
225
240
  | `TechnologyDevice 'Device:Switch01' is connectedTo itself` | 12 |
226
241
  | `TechnologyDevice 'Device:Router01' is a router that realizes no TechnologyCommunicationNetwork` | 11 |
227
242
  | `TechnologyDevice 'Device:a:server-3' is servedBy 'Kernel:Team', not by the team of its equipment ('Compute:Team')` | 13 |
@@ -544,8 +559,9 @@ Which kind to pick is in [Choosing the Kind: Infrastructure](#choosing-the-kind-
544
559
  | `TechnologyCommunicationNetwork` | `realizedBy` | `TechnologyEquipment` (the fabric), `TechnologySystemSoftware` (routing stack, VPN software) |
545
560
  | `TechnologyCommunicationNetwork` | `realizedBy` | `ApplicationComponent`, `ApplicationService` (software that builds the network: an SDN agent, a network operator) |
546
561
  | `TechnologyCommunicationNetwork` | `realizedBy` | `TechnologyDevice` (the switches and routers that build it) |
547
- | `TechnologyCommunicationNetwork` | `connects` | `TechnologyDevice` (the devices attached to it) |
562
+ | `TechnologyCommunicationNetwork` | `connects` | `TechnologyDevice` (the devices attached to it, a named workstation behind a VPN) |
548
563
  | `TechnologyNode` | `servedBy` | `TechnologyCommunicationNetwork` |
564
+ | `ApplicationInterface` | `servedBy` | `TechnologyCommunicationNetwork` (the networks on which it can be reached) |
549
565
 
550
566
  - **Underlay and overlay:** the underlay *serves* the overlay, so the overlay is `servedBy` the underlay. A BGP-EVPN
551
567
  network that carries customer workloads is one `overlay` network served by the underlay and realized by the routing
@@ -560,6 +576,10 @@ Which kind to pick is in [Choosing the Kind: Infrastructure](#choosing-the-kind-
560
576
  - **Attachment:** a node is `servedBy` the networks it is attached to; the network page lists them. A physical
561
577
  device is attached the other way round: the network `connects` it, so the network can also be `realizedBy` the
562
578
  same switch without a cycle.
579
+ - **Reachability of an interface:** an `ApplicationInterface` is `servedBy` the networks on which it can be reached,
580
+ next to the `TechnologyInterface` that carries it. With `api/authenticationMethod` this is the exposure of the
581
+ interface (see the example analysis `Analysis:Interface:ExposedWithoutAuthentication`). Leave it out when the
582
+ networks are not known.
563
583
 
564
584
  ```
565
585
  # all overlay networks
@@ -585,7 +605,7 @@ center is not a computational resource.
585
605
  | Location | For a country, metro or region |
586
606
  | TechnologyFacility | For a data center, colocation, office or edge site (`facility/type`), also a campus or hall inside one |
587
607
  | TechnologyEquipment | Optional: racks, network fabrics, physical clusters inside a facility (`equipment/type`) |
588
- | TechnologyDevice | Optional: one physical machine, `locatedAt` its room (`facility/type: room`) |
608
+ | TechnologyDevice | Optional: one physical machine, `locatedAt` its room (`facility/type: room`); a client device (laptop, workstation, phone) needs no room |
589
609
 
590
610
  Relations are written on the element that is placed (see [Direction of Relations](#direction-of-relations)):
591
611
 
@@ -680,13 +700,19 @@ depend on each other, so `dependsOn` is exempt from the cycle check. In `example
680
700
 
681
701
  ### Where a Component Runs
682
702
 
683
- Three relations say what something runs on; each answers a different question:
703
+ These relations say what something runs on or builds; each answers a different question:
684
704
 
685
705
  | Relation | Means | Example |
686
706
  |----------|-------|---------|
687
707
  | ApplicationComponent `servedBy technologyServices` | the platform service the component is deployed on | `Payments:API` runs in `Payments:Cluster` |
688
708
  | ApplicationComponent `servedBy technologySystemSoftware` | the runtime it runs on | a Kubernetes distribution, a JVM |
689
709
  | ApplicationService `servedBy technologyServices` | a platform service the service as a whole uses | a managed database, the clusters of its own components |
710
+ | ApplicationComponent `provisions technologyServices` | the platform service the component creates and keeps in its desired state | an operator that builds the clusters of a product |
711
+
712
+ `provisions` is written on the component, like every relation, so it points down to what the component creates: the
713
+ service page lists its provisioner as incoming, and the impact of a provisioner is its outgoing relations (the
714
+ services it builds). It is not a runtime dependency: a provisioned service keeps running while its provisioner is
715
+ down, only changes stop. `suppliedBy` on the service stays for what it is made of (the system software).
690
716
 
691
717
  Deployment importers (Argo CD, Helm, Cluster API) know every cluster a deployable runs in and write the relation per
692
718
  component. Keep the service-level relation to the clusters of the service's own components: a sidecar or agent that
@@ -989,7 +1015,7 @@ is a sidebar filter and query term (`lifecycle/status == "deprecated"`).
989
1015
  |------------|---------|
990
1016
  | `lifecycle/status` | The state above |
991
1017
  | `lifecycle/activeAt`, `lifecycle/deprecatedAt`, `lifecycle/sunsetAt`, `lifecycle/endOfLifeAt` | When it happened or will happen (ISO 8601 date or time, checked by `archsight lint`) |
992
- | `lifecycle/replacedBy` | The resource that replaces it: a name for a resource of the same kind, `Kind/Name` for another kind (a component replaced by an `ApplicationService/...`). Shown as a link, `archsight lint` checks that it exists |
1018
+ | `lifecycle/replacedBy` | The resource that replaces it: a name for a resource of the same kind, `Kind/Name` for another kind (a component replaced by an `ApplicationService/...`). Shown as a link behind the status, `archsight lint` checks that it exists |
993
1019
  | `lifecycle/reason` | Why, in markdown |
994
1020
 
995
1021
  ```yaml
@@ -1002,6 +1028,9 @@ metadata:
1002
1028
  lifecycle/replacedBy: ApplicationService/ExportV2
1003
1029
  ```
1004
1030
 
1031
+ The detail page shows the status, the replacement as a link and, for a `deprecated`, `sunset` or `end-of-life`
1032
+ resource, the number of resources that still refer to it (a link to its Incoming relations) on one line.
1033
+
1005
1034
  ## Common Patterns
1006
1035
 
1007
1036
  ### Microservice
data/docs/pages.md CHANGED
@@ -339,9 +339,9 @@ confluence:
339
339
  With `drawio` off nothing draw.io-specific is written, so any Confluence shows the diagrams. Rendering a `.drawio` needs the
340
340
  draw.io desktop CLI (`drawio`, or `ARCHSIGHT_DRAWIO_CLI`) on the machine that exports; the preview of an SVG needs
341
341
  `rsvg-convert` or that CLI. A diagram that cannot be rendered fails the page instead of leaving a blank diagram. Attachments an
342
- earlier export added and the page no longer uses are removed; attachments added by others are left alone. `[[links]]` and markdown links to pages (`[text](/pages/name)`) that have a Confluence link
342
+ earlier export added and the page no longer uses are removed (a draw.io diagram is attached under its name and a hash of its content, so a changed diagram is a new attachment, because the draw.io macro would keep showing the first version of an attachment it names); attachments added by others are left alone. `[[links]]` and markdown links to pages (`[text](/pages/name)`) that have a Confluence link
343
343
  become links to them, other links to Archsight (pages without a Confluence link, resources, searches) are plain text. A page with a broken image, a diagram that does not render or an invalid
344
- ```` ```view ````/```` ```requirements ```` block is not exported and reported as failed. The Confluence title is kept. The exported page uses the Confluence layout: the page properties top left, the note that the page is generated by Archsight top right, and the content below in a section of its own.
344
+ ```` ```view ````/```` ```requirements ```` block is not exported and reported as failed. The Confluence title is kept. The exported page uses the Confluence layout: the page properties top left, the note that the page is generated by Archsight top right, and the content below in a section of its own. When the resources are a git checkout with an `origin` remote, the note names the repository and links the source file to its page on the git host (GitHub `/blob/`, GitLab `/-/blob/`, on the checked out branch, or the commit when the HEAD is detached; other hosts get only the repository link, and so does a file git does not track). User names and tokens in the remote URL never appear in the link.
345
345
 
346
346
  **Views and requirements** are live in Archsight, so Confluence gets a regular table with the data of the moment of the export (every
347
347
  export writes the current data; a page whose tables did not change is `unchanged`): `![[View/Name]]` on a line of its own, a
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "kramdown"
4
- require "kramdown-parser-gfm"
3
+ require_relative "../markdown"
5
4
  require_relative "../diagram"
6
5
 
7
6
  module Archsight
@@ -192,7 +191,7 @@ module Archsight
192
191
  # ---- markdown -> Typst
193
192
 
194
193
  def markdown(source, inline: false)
195
- root = Kramdown::Document.new(source.to_s, input: "GFM").root
194
+ root = Archsight::Markdown.document(source).root
196
195
  out = blocks(root.children)
197
196
  inline ? out.gsub(/\n\n+/, " #linebreak() ") : out
198
197
  end
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
- require "kramdown"
5
- require "kramdown-parser-gfm"
4
+ require_relative "../../markdown"
6
5
 
7
6
  module Archsight
8
7
  module Export
@@ -90,8 +89,8 @@ module Archsight
90
89
  return "" if text.strip.empty?
91
90
 
92
91
  safe = text.gsub(%r{<(?!(?:https?://|mailto:))}, "&lt;")
93
- Kramdown::Document.new(safe, input: "GFM", entity_output: :as_char, auto_ids: false, hard_wrap: false).to_html.strip
94
- .gsub(%r{<(br|hr|img)([^>]*?)\s*/?>}, '<\1\2 />')
92
+ Archsight::Markdown.unwrapped(safe, entity_output: :as_char, auto_ids: false).to_html.strip
93
+ .gsub(%r{<(br|hr|img)([^>]*?)\s*/?>}, '<\1\2 />')
95
94
  end
96
95
 
97
96
  def h(text) = ERB::Util.html_escape(text)
@@ -7,6 +7,7 @@ require "time"
7
7
  require_relative "../../version"
8
8
  require_relative "../../assets"
9
9
  require_relative "../../helpers"
10
+ require_relative "../../git_source"
10
11
  require_relative "client"
11
12
  require_relative "credentials"
12
13
  require_relative "page_url"
@@ -117,7 +118,8 @@ module Archsight
117
118
  storage = Storage.new(page_name: page.name, source: relative_source(page),
118
119
  base_dir: Assets.base_dir_for(page, resources_dir: @resources_dir).to_s,
119
120
  resources_dir: @resources_dir, wiki: Helpers::WikiLinks.new(@database), page_id: id, base: url.base,
120
- drawio: drawio?, diagram_links: DiagramLinks.new(@database), database: @database)
121
+ drawio: drawio?, diagram_links: DiagramLinks.new(@database), database: @database,
122
+ git: GitSource.locate(page.path_ref.path.to_s))
121
123
  storage.convert(body_of(page), toc: page.annotations["page/toc"] == "yes", header: PageProperties.xml(page))
122
124
  end
123
125
 
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
- require "kramdown"
5
- require "kramdown-parser-gfm"
4
+ require_relative "../../markdown"
6
5
  require_relative "../../helpers/macros"
7
6
 
8
7
  module Archsight
@@ -69,7 +68,7 @@ module Archsight
69
68
  kept << live(match)
70
69
  "ARCHSIGHTKEPT#{kept.length - 1}X"
71
70
  end.gsub("<", "&lt;") # raw HTML is not allowed, it could break the storage XML
72
- html = Kramdown::Document.new(marked, input: "GFM", auto_ids: false, entity_output: :as_char, smart_quotes: %w[apos apos quot quot]).to_html.strip
71
+ html = Archsight::Markdown.unwrapped(marked, auto_ids: false, entity_output: :as_char, smart_quotes: %w[apos apos quot quot]).to_html.strip
73
72
  html.sub(%r{\A<p>(.*)</p>\z}m, '\1').gsub(/ARCHSIGHTKEPT(\d+)X/) { kept.fetch(Regexp.last_match(1).to_i) }
74
73
  end
75
74
 
@@ -3,8 +3,7 @@
3
3
  require "erb"
4
4
  require "uri"
5
5
  require "digest"
6
- require "kramdown"
7
- require "kramdown-parser-gfm"
6
+ require_relative "../../markdown"
8
7
  require_relative "../../assets"
9
8
  require_relative "../../diagram"
10
9
  require_relative "../../helpers/macros"
@@ -80,8 +79,9 @@ module Archsight
80
79
 
81
80
  MXFILE = "application/vnd.jgraph.mxfile"
82
81
 
83
- BANNER = "This page is generated by Archsight from <code>%<source>s</code>. " \
84
- "Changes made here are overwritten by the next export: edit the source instead."
82
+ HOMEPAGE = "https://github.com/ionos-cloud/archsight"
83
+ BANNER = "This page is generated by <a href=\"#{HOMEPAGE}\">Archsight</a> from %<source>s. " \
84
+ "Changes made here are overwritten by the next export: edit the source instead.".freeze
85
85
 
86
86
  LANGUAGES = {
87
87
  "sh" => "bash", "shell" => "bash", "zsh" => "bash", "yml" => "yaml", "json" => "javascript", "js" => "javascript",
@@ -95,6 +95,8 @@ module Archsight
95
95
 
96
96
  # @param page_name [String] used to name generated attachments
97
97
  # @param source [String] the markdown file, shown in the banner
98
+ # @param git [Archsight::GitSource::Location, nil] where the file is in its git repository: the banner then
99
+ # links it to the file in the web UI of the git host and names the repository
98
100
  # @param base_dir [String] directory of the markdown file relative to the resources directory
99
101
  # @param resources_dir [String]
100
102
  # @param wiki [Archsight::Helpers::WikiLinks] resolves [[Targets]]
@@ -103,8 +105,10 @@ module Archsight
103
105
  # @param database [Archsight::Database, nil] lets macros that name pages (pagetree) find them
104
106
  # @param diagram_links [#call, nil] resolves a diagram's `resource` references (DiagramLinks); none leaves them inert
105
107
  # @param drawio [Boolean] the Confluence has the draw.io app: diagrams go in as draw.io macros, else as images
106
- def initialize(page_name:, source:, base_dir:, resources_dir:, wiki:, page_id: nil, base: nil, drawio: false, diagram_links: nil, database: nil)
108
+ def initialize(page_name:, source:, base_dir:, resources_dir:, wiki:, page_id: nil, base: nil, drawio: false, diagram_links: nil, database: nil,
109
+ git: nil)
107
110
  @database = database
111
+ @git = git
108
112
  @page_name = page_name
109
113
  @source = source
110
114
  @base_dir = base_dir
@@ -125,7 +129,7 @@ module Archsight
125
129
  # @param header [String] storage format of the page properties, shown top left next to the banner
126
130
  # @return [Converted]
127
131
  def convert(markdown, toc: false, header: "")
128
- document = Kramdown::Document.new(protect(markdown), input: "GFM", auto_ids: false, entity_output: :as_char, smart_quotes: %w[apos apos quot quot])
132
+ document = Archsight::Markdown.unwrapped(protect(markdown), auto_ids: false, entity_output: :as_char, smart_quotes: %w[apos apos quot quot])
129
133
  html, = Converter.convert(document.root, document.options.merge(storage: self))
130
134
  Converted.new(body: layout(header, banner, (toc ? %(<ac:structured-macro ac:name="toc" />\n) : "") + html), attachments: @attachments, problems: @problems)
131
135
  end
@@ -226,7 +230,17 @@ module Archsight
226
230
  end
227
231
 
228
232
  def banner
229
- %(<ac:structured-macro ac:name="info"><ac:rich-text-body><p>#{format(BANNER, source: h(@source))}</p></ac:rich-text-body></ac:structured-macro>\n)
233
+ %(<ac:structured-macro ac:name="info"><ac:rich-text-body><p>#{format(BANNER, source: banner_source)}</p></ac:rich-text-body></ac:structured-macro>\n)
234
+ end
235
+
236
+ # `<code>path</code>`, in a git repository the path from its root, linked to the file on the git host, and
237
+ # the repository
238
+ def banner_source
239
+ return "<code>#{h(@source)}</code>" unless @git
240
+
241
+ file = "<code>#{h(@git.path)}</code>"
242
+ file = %(<a href="#{h(@git.url)}">#{file}</a>) if @git.url
243
+ %(#{file} in the repository <a href="#{h(@git.repository_url)}">#{h(@git.repository)}</a>)
230
244
  end
231
245
 
232
246
  # {macros}, [[Target|label]] and ![[View/Name]] become placeholders before kramdown sees them (it would split the
@@ -394,13 +408,12 @@ module Archsight
394
408
  %(<p><ac:image><ri:attachment ri:filename="#{h(filename)}" /></ac:image></p>\n)
395
409
  end
396
410
 
397
- # The diagram file of a draw.io macro and its `.png` preview, which must share the name
411
+ # The diagram file of a draw.io macro and its `.png` preview, which must share the name. The name carries a hash
412
+ # of the content: the macro shows revision 1 of the attachment it names, so a changed diagram uploaded under the
413
+ # old name (as a new version of it) would not be shown. A new name is a new attachment, the old one is removed.
398
414
  # @return [String] the name the macro refers to
399
415
  def attach_diagram(stem, xml, png)
400
- sha = Digest::SHA256.hexdigest(xml)
401
- name = stem
402
- taken = ->(file, data) { @attachments.key?(file) && @attachments[file].data != data }
403
- name = "#{stem}-#{sha[0, 8]}" if taken.call(stem, xml) || taken.call("#{stem}.png", png)
416
+ name = "#{stem}-#{Digest::SHA256.hexdigest(xml)[0, 8]}"
404
417
  @attachments[name] = Attachment.new(filename: name, data: xml, content_type: MXFILE)
405
418
  @attachments["#{name}.png"] = Attachment.new(filename: "#{name}.png", data: png, content_type: "image/png")
406
419
  name
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
- require "kramdown"
5
- require "kramdown-parser-gfm"
4
+ require_relative "../../markdown"
6
5
  require_relative "../../view_table"
7
6
  require_relative "../../helpers/macros"
8
7
  require_relative "diagram_links"
@@ -67,7 +66,7 @@ module Archsight
67
66
  return "" if text.to_s.strip.empty?
68
67
 
69
68
  safe = text.gsub(%r{<(?!(?:https?://|mailto:))}, "&lt;")
70
- html = Kramdown::Document.new(safe, input: "GFM", entity_output: :as_char, auto_ids: false).to_html.strip
69
+ html = Archsight::Markdown.unwrapped(safe, entity_output: :as_char, auto_ids: false).to_html.strip
71
70
  html.match(%r{\A<p>((?:(?!</?p>).)*)</p>\z}m) ? Regexp.last_match(1) : html
72
71
  end
73
72
 
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "open3"
5
+ require "pathname"
6
+ require "uri"
7
+
8
+ module Archsight
9
+ # Where a file lives in its git repository and its page in the web UI of the git host, for links to the source of
10
+ # generated content (the banner of an exported Confluence page). Only local git is asked (the repository of the
11
+ # file, its `origin` remote, the checked out branch), nothing goes over the network.
12
+ #
13
+ # The web URL is `https://<host>/<owner>/<repo>` of the remote, without the user, password or token a remote URL
14
+ # may carry. GitHub files are `/blob/<ref>/<path>`, GitLab files `/-/blob/<ref>/<path>`; on other hosts, and for a
15
+ # file git does not track, only the repository is linked. The ref is the checked out branch, the commit when the
16
+ # HEAD is detached (CI).
17
+ module GitSource
18
+ # repository: "owner/repo"; repository_url: its web page; path: the file relative to the repository root;
19
+ # url: the web page of the file, nil if the host is not known
20
+ Location = Struct.new(:repository, :repository_url, :path, :url, keyword_init: true)
21
+
22
+ module_function
23
+
24
+ # @param file [String] path of a file
25
+ # @return [Location, nil] nil if the file is not in a git repository with an `origin` remote
26
+ def locate(file)
27
+ dir = File.dirname(File.expand_path(file))
28
+ root = git(dir, "rev-parse", "--show-toplevel") or return nil
29
+ remote = git(dir, "remote", "get-url", "origin") or return nil
30
+ host, repository = host_and_repository(remote)
31
+ return nil unless host && repository
32
+
33
+ path = Pathname.new(File.realpath(file)).relative_path_from(Pathname.new(File.realpath(root))).to_s
34
+ ref = git(dir, "symbolic-ref", "--short", "-q", "HEAD") || git(dir, "rev-parse", "HEAD")
35
+ repository_url = "https://#{host}/#{repository}"
36
+ url = tracked?(root, path) ? file_url(host, repository_url, ref, path) : nil
37
+ Location.new(repository: repository, repository_url: repository_url, path: path, url: url)
38
+ rescue SystemCallError, ArgumentError
39
+ nil
40
+ end
41
+
42
+ # `git@host:owner/repo.git`, `ssh://git@host:22/owner/repo.git`, `https://user:token@host/owner/repo` ->
43
+ # ["host", "owner/repo"]; the port of an ssh remote is not the port of the web UI and is left out
44
+ def host_and_repository(remote)
45
+ if (scp = remote.match(%r{\A(?:[^@/\s]+@)?([^:/\s]+):(?!//)(.+)\z}))
46
+ host, path = scp.captures
47
+ else
48
+ uri = URI.parse(remote)
49
+ return [nil, nil] unless uri.host && %w[http https ssh git].include?(uri.scheme)
50
+
51
+ host = uri.scheme.start_with?("http") && uri.port != uri.default_port ? "#{uri.host}:#{uri.port}" : uri.host
52
+ path = uri.path
53
+ end
54
+ repository = path.delete_prefix("/").delete_suffix("/").delete_suffix(".git")
55
+ repository.empty? ? [nil, nil] : [host, repository]
56
+ rescue URI::InvalidURIError
57
+ [nil, nil]
58
+ end
59
+
60
+ def file_url(host, repository_url, ref, path)
61
+ blob = if host.match?(/(\A|\.)github\./) then "blob"
62
+ elsif host.match?(/(\A|\.)gitlab\./) then "-/blob"
63
+ end
64
+ return nil unless blob && ref
65
+
66
+ "#{repository_url}/#{blob}/#{escape(ref)}/#{escape(path)}"
67
+ end
68
+
69
+ # An untracked or ignored file is not on the git host: no link that leads nowhere
70
+ def tracked?(root, path) = !git(root, "ls-files", "--", path).nil?
71
+
72
+ # each segment escaped, the slashes kept
73
+ def escape(path) = path.split("/").map { |segment| ERB::Util.url_encode(segment) }.join("/")
74
+
75
+ def git(dir, *)
76
+ out, _err, status = Open3.capture3("git", "-C", dir, *)
77
+ value = out.strip
78
+ status.success? && !value.empty? ? value : nil
79
+ end
80
+ end
81
+ end
@@ -2,8 +2,7 @@
2
2
 
3
3
  require "cgi"
4
4
  require "erb"
5
- require "kramdown"
6
- require "kramdown-parser-gfm"
5
+ require_relative "../markdown"
7
6
  require_relative "../assets"
8
7
 
9
8
  module Archsight
@@ -52,7 +51,7 @@ module Archsight
52
51
  # Problems with the image references of a markdown text.
53
52
  # @return [Array<Hash>] `{ reference:, status: :outside|:type|:missing, path: }`
54
53
  def audit(markdown, base_dir:, resources_dir:)
55
- html = Kramdown::Document.new(markdown.to_s, input: "GFM").to_html
54
+ html = Archsight::Markdown.document(markdown).to_html
56
55
  html.scan(IMG).filter_map do |tag|
57
56
  reference = attribute(tag, SRC)
58
57
  next if reference.nil? || Assets.external?(reference)
@@ -65,7 +64,7 @@ module Archsight
65
64
  # The existing .asd files a markdown text embeds (for the linter)
66
65
  # @return [Array<Array(String, String)>] normalized asset path and real file
67
66
  def asd_files(markdown, base_dir:, resources_dir:)
68
- html = Kramdown::Document.new(markdown.to_s, input: "GFM").to_html
67
+ html = Archsight::Markdown.document(markdown).to_html
69
68
  html.scan(IMG).filter_map do |tag|
70
69
  reference = attribute(tag, SRC)
71
70
  next if reference.nil? || Assets.external?(reference)
@@ -442,6 +442,7 @@ class Archsight::Import::Executor
442
442
  progress: import_progress,
443
443
  shared_writer: @shared_writer
444
444
  )
445
+ Archsight::Import.never_index(handler.cache_root)
445
446
  handler.execute
446
447
  remove_stale_outputs(import, handler.owned_outputs)
447
448
  @mutex.synchronize { @ran_this_run.add(import.name) }
@@ -1,5 +1,7 @@
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
@@ -13,8 +15,31 @@ module Archsight
13
15
  # expand it before use)
14
16
  def self.cache_root(override = nil)
15
17
  require "archsight/user_config"
16
- override || Archsight::UserConfig.setting("import", "cache_root")&.to_s ||
17
- File.join(Dir.home, ".cache", "archsight", "git")
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
18
43
  end
19
44
 
20
45
  # The entries of `import/dependsOn`: import names and glob patterns
@@ -9,6 +9,8 @@ module Archsight
9
9
  # Network types that run over another network and need one that serves them
10
10
  OVERLAY_TYPES = %w[overlay vpn].freeze
11
11
  NETWORK_DEVICE_TYPES = %w[switch router].freeze
12
+ # Client machines move: they need no room
13
+ CLIENT_DEVICE_TYPES = %w[laptop desktop workstation thin-client tablet mobile].freeze
12
14
 
13
15
  def initialize(database)
14
16
  @database = database
@@ -185,12 +187,13 @@ module Archsight
185
187
  end
186
188
 
187
189
  # 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
190
+ # whole, and a chassis through its members. A client device (laptop, workstation, phone) is not placed.
189
191
  def device_without_facility
190
192
  parts = owners_by_target("TechnologyDevice", :composedOf, :technologyDevices)
191
193
  racked = owners_by_target("TechnologyEquipment", :composedOf, :technologyDevices)
192
194
  instances("TechnologyDevice").filter_map do |device|
193
195
  next if parts.key?(device) || racked.key?(device) || targets(device, :composedOf, :technologyDevices).any?
196
+ next if CLIENT_DEVICE_TYPES.include?(device.annotations["device/type"])
194
197
  next if targets(device, :locatedAt, :technologyFacilities).any?
195
198
 
196
199
  warning(device, "is located at no TechnologyFacility")
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "kramdown"
3
+ require_relative "markdown"
4
4
  require_relative "helpers"
5
5
  require_relative "linter/structure"
6
6
 
@@ -319,7 +319,7 @@ module Archsight
319
319
  return if instance.annotations.key?("generated/script")
320
320
 
321
321
  begin
322
- Kramdown::Document.new(value, input: "GFM")
322
+ Archsight::Markdown.document(value)
323
323
  rescue StandardError => e
324
324
  @errors << "#{instance.path_ref}: Markdown syntax error in annotation '#{key}': #{e.message}"
325
325
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kramdown"
4
+ require "kramdown-parser-gfm"
5
+
6
+ module Archsight
7
+ # The one way markdown is parsed, for the web, the export, the linter and the analysis output alike. GFM, but a
8
+ # single newline inside a paragraph is a space, as in CommonMark and GitHub's rendering of `.md` files (the GFM
9
+ # parser of kramdown would make it a `<br />`, as GitHub does in comments). A line break is two trailing spaces,
10
+ # a trailing backslash or `<br>`.
11
+ module Markdown
12
+ OPTIONS = { input: "GFM", hard_wrap: false }.freeze
13
+
14
+ # Raw HTML whose text is kept as it is
15
+ VERBATIM = %w[pre code script style textarea].freeze
16
+
17
+ def self.document(text, **)
18
+ Kramdown::Document.new(text.to_s, **OPTIONS, **)
19
+ end
20
+
21
+ # The document without the newlines of the source in its text, for a target that keeps a newline instead of
22
+ # reading it as a space (Confluence): a soft line break becomes a space, the newline after a line break goes.
23
+ # Inline code over two lines is one line (as in CommonMark). Paragraphs, breaks and code blocks are unchanged.
24
+ def self.unwrapped(text, **)
25
+ document(text, **).tap { |doc| unwrap(doc.root) }
26
+ end
27
+
28
+ def self.unwrap(element)
29
+ element.children.each_with_index do |child, index|
30
+ if child.type == :text
31
+ previous = index.positive? ? element.children[index - 1] : nil
32
+ joined = child.value.gsub(/[ \t]*\r?\n[ \t]*/, " ")
33
+ child.value = line_break?(previous) ? joined.lstrip : joined
34
+ elsif child.type == :codespan
35
+ child.value = child.value.gsub(/\r?\n/, " ")
36
+ elsif !(child.type == :html_element && VERBATIM.include?(child.value))
37
+ unwrap(child)
38
+ end
39
+ end
40
+ end
41
+ private_class_method :unwrap
42
+
43
+ def self.line_break?(element)
44
+ element && (element.type == :br || (element.type == :html_element && element.value == "br"))
45
+ end
46
+ private_class_method :line_break?
47
+ end
48
+ end