archsight 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +5 -4
  3. data/README.md +44 -59
  4. data/chart/archsight/Chart.yaml +6 -0
  5. data/chart/archsight/README.md +3 -0
  6. data/chart/archsight/templates/NOTES.txt +22 -0
  7. data/chart/archsight/templates/_helpers.tpl +62 -0
  8. data/chart/archsight/templates/deployment.yaml +114 -0
  9. data/chart/archsight/templates/ingress.yaml +56 -0
  10. data/chart/archsight/templates/resources-configmap.yaml +10 -0
  11. data/chart/archsight/templates/resources-pvc.yaml +23 -0
  12. data/chart/archsight/templates/service.yaml +15 -0
  13. data/chart/archsight/templates/serviceaccount.yaml +12 -0
  14. data/chart/archsight/values.yaml +162 -0
  15. data/docs/architecture.md +39 -0
  16. data/docs/docker.md +49 -0
  17. data/{lib/archsight/web/doc → docs}/import.md +10 -2
  18. data/{lib/archsight/web/doc → docs}/index.md.erb +3 -1
  19. data/docs/kubernetes.md +149 -0
  20. data/docs/licenses.md +307 -0
  21. data/lib/archsight/analysis/executor.rb +0 -10
  22. data/lib/archsight/annotations/annotation.rb +85 -36
  23. data/lib/archsight/annotations/architecture_annotations.rb +1 -34
  24. data/lib/archsight/annotations/computed.rb +1 -1
  25. data/lib/archsight/annotations/generated_annotations.rb +6 -3
  26. data/lib/archsight/annotations/git_annotations.rb +8 -4
  27. data/lib/archsight/annotations/interface_annotations.rb +35 -0
  28. data/lib/archsight/cli.rb +4 -2
  29. data/lib/archsight/editor/content_hasher.rb +37 -0
  30. data/lib/archsight/editor/file_writer.rb +79 -0
  31. data/lib/archsight/editor.rb +237 -0
  32. data/lib/archsight/graph.rb +1 -51
  33. data/lib/archsight/helpers.rb +0 -20
  34. data/lib/archsight/import/handlers/github.rb +16 -6
  35. data/lib/archsight/import/handlers/gitlab.rb +28 -10
  36. data/lib/archsight/import/handlers/repository.rb +56 -6
  37. data/lib/archsight/import/handlers/rest_api.rb +13 -1
  38. data/lib/archsight/import/license_analyzer.rb +650 -0
  39. data/lib/archsight/import/team_matcher.rb +111 -61
  40. data/lib/archsight/linter.rb +1 -1
  41. data/lib/archsight/mcp/base.rb +11 -0
  42. data/lib/archsight/mcp/execute_analysis_tool.rb +100 -0
  43. data/lib/archsight/mcp.rb +1 -0
  44. data/lib/archsight/renderer.rb +4 -4
  45. data/lib/archsight/resources/analysis.rb +1 -17
  46. data/lib/archsight/resources/application_interface.rb +1 -5
  47. data/lib/archsight/resources/base.rb +14 -14
  48. data/lib/archsight/resources/business_actor.rb +18 -3
  49. data/lib/archsight/resources/technology_artifact.rb +48 -0
  50. data/lib/archsight/resources/technology_interface.rb +1 -1
  51. data/lib/archsight/resources/technology_service.rb +5 -0
  52. data/lib/archsight/version.rb +1 -1
  53. data/lib/archsight/web/api/docs.rb +37 -2
  54. data/lib/archsight/web/api/json_helpers.rb +79 -13
  55. data/lib/archsight/web/api/openapi/spec.yaml +699 -0
  56. data/lib/archsight/web/api/routes.rb +23 -0
  57. data/lib/archsight/web/application.rb +48 -128
  58. data/lib/archsight/web/editor/form_builder.rb +100 -0
  59. data/lib/archsight/web/editor/helpers.rb +150 -0
  60. data/lib/archsight/web/editor/routes.rb +166 -0
  61. data/lib/archsight/web/public/vue/ApiDocsPage-B1RqTNqh.js +1 -0
  62. data/lib/archsight/web/public/vue/ApiDocsPage-DhNTOH4o.css +1 -0
  63. data/lib/archsight/web/public/vue/DocPage-DzwBgBd4.js +1 -0
  64. data/lib/archsight/web/public/vue/EditorPage-D_miHSv4.js +34 -0
  65. data/lib/archsight/web/public/vue/EditorPage-Dq0MuTnp.css +1 -0
  66. data/lib/archsight/web/public/vue/ErrorPage-CQQtPey3.js +2 -0
  67. data/lib/archsight/web/public/vue/ErrorPage-CwPT3JUr.css +1 -0
  68. data/lib/archsight/web/public/vue/GraphView-DRcIqAiR.css +1 -0
  69. data/lib/archsight/web/public/vue/GraphView-T9jFH_qg.js +1 -0
  70. data/lib/archsight/web/public/vue/InstanceRouter-1Sm-CRhf.js +2 -0
  71. data/lib/archsight/web/public/vue/InstanceRouter-BJkDRXZY.css +1 -0
  72. data/lib/archsight/web/public/vue/KindList-JA_L_-Cz.js +1 -0
  73. data/lib/archsight/web/public/vue/ResourceList-8iqavWdg.js +1 -0
  74. data/lib/archsight/web/public/vue/ResourceList-DP-z-j71.css +1 -0
  75. data/lib/archsight/web/public/vue/SearchResults-BGHbg48-.css +1 -0
  76. data/lib/archsight/web/public/vue/SearchResults-BdgFeHcm.js +1 -0
  77. data/lib/archsight/web/public/vue/_basePickBy-CVgieyx-.js +1 -0
  78. data/lib/archsight/web/public/vue/_baseUniq-BNfrOSaP.js +1 -0
  79. data/lib/archsight/web/public/vue/architectureDiagram-VXUJARFQ-CJXNpTr5.js +36 -0
  80. data/lib/archsight/web/public/vue/blockDiagram-VD42YOAC-B5488Hes.js +122 -0
  81. data/lib/archsight/web/public/vue/c4Diagram-YG6GDRKO-eYY3hprM.js +10 -0
  82. data/lib/archsight/web/public/vue/chunk-4BX2VUAB-ZoXeL4D1.js +1 -0
  83. data/lib/archsight/web/public/vue/chunk-55IACEB6-rNtQYnu_.js +1 -0
  84. data/lib/archsight/web/public/vue/chunk-B4BG7PRW-DolAeVV9.js +165 -0
  85. data/lib/archsight/web/public/vue/chunk-DI55MBZ5-DnN0f_hj.js +220 -0
  86. data/lib/archsight/web/public/vue/chunk-FMBD7UC4-BQWOCMuR.js +15 -0
  87. data/lib/archsight/web/public/vue/chunk-QN33PNHL-DId301Kb.js +1 -0
  88. data/lib/archsight/web/public/vue/chunk-QZHKN3VN-xbY0NLgv.js +1 -0
  89. data/lib/archsight/web/public/vue/chunk-TZMSLE5B-CgF9_37b.js +1 -0
  90. data/lib/archsight/web/public/vue/classDiagram-2ON5EDUG-jGlvI-Za.js +1 -0
  91. data/lib/archsight/web/public/vue/classDiagram-v2-WZHVMYZB-jGlvI-Za.js +1 -0
  92. data/lib/archsight/web/public/vue/clone-6iRPe1-W.js +1 -0
  93. data/lib/archsight/web/public/vue/cose-bilkent-S5V4N54A-CB9Zfu50.js +1 -0
  94. data/lib/archsight/web/public/vue/cytoscape.esm-5J0xJHOV.js +321 -0
  95. data/lib/archsight/web/public/vue/dagre-6UL2VRFP-BqkmE-LI.js +4 -0
  96. data/lib/archsight/web/public/vue/diagram-PSM6KHXK-CKBfqtw3.js +24 -0
  97. data/lib/archsight/web/public/vue/diagram-QEK2KX5R-B78rOlvK.js +43 -0
  98. data/lib/archsight/web/public/vue/diagram-S2PKOQOG-BlXC6Cia.js +24 -0
  99. data/lib/archsight/web/public/vue/erDiagram-Q2GNP2WA-BnliyziJ.js +60 -0
  100. data/lib/archsight/web/public/vue/flowDiagram-NV44I4VS-wuqPowTd.js +162 -0
  101. data/lib/archsight/web/public/vue/ganttDiagram-JELNMOA3-GSffAIH3.js +267 -0
  102. data/lib/archsight/web/public/vue/gitGraphDiagram-V2S2FVAM-OA7VyugW.js +65 -0
  103. data/lib/archsight/web/public/vue/graph-BXHAtA0S.js +1 -0
  104. data/lib/archsight/web/public/vue/graphviz-CJms5bxZ.js +13 -0
  105. data/lib/archsight/web/public/vue/index-DsEsN0_K.js +2 -0
  106. data/lib/archsight/web/public/vue/index-Tiu4C-Sb.css +1 -0
  107. data/lib/archsight/web/public/vue/infoDiagram-HS3SLOUP-nlVe2qgv.js +2 -0
  108. data/lib/archsight/web/public/vue/journeyDiagram-XKPGCS4Q-CtTIcKwf.js +139 -0
  109. data/lib/archsight/web/public/vue/kanban-definition-3W4ZIXB7-837KX0sW.js +89 -0
  110. data/lib/archsight/web/public/vue/katex-C-M49wc6.js +261 -0
  111. data/lib/archsight/web/public/vue/layout-DtE0QdL6.js +1 -0
  112. data/lib/archsight/web/public/vue/mermaid-DpPHPFQh.js +250 -0
  113. data/lib/archsight/web/public/vue/mindmap-definition-VGOIOE7T-9gLF2AoY.js +68 -0
  114. data/lib/archsight/web/public/vue/pieDiagram-ADFJNKIX-CyCNgw3u.js +30 -0
  115. data/lib/archsight/web/public/vue/quadrantDiagram-AYHSOK5B-CkPh8g02.js +7 -0
  116. data/lib/archsight/web/public/vue/requirementDiagram-UZGBJVZJ-Dkt6OSlY.js +64 -0
  117. data/lib/archsight/web/public/vue/sankeyDiagram-TZEHDZUN-BqprTk8x.js +10 -0
  118. data/lib/archsight/web/public/vue/sequenceDiagram-WL72ISMW-CTmTe1FQ.js +145 -0
  119. data/lib/archsight/web/public/vue/stateDiagram-FKZM4ZOC-CphqmkEU.js +1 -0
  120. data/lib/archsight/web/public/vue/stateDiagram-v2-4FDKWEC3-CxaDW5sW.js +1 -0
  121. data/lib/archsight/web/public/vue/timeline-definition-IT6M3QCI-CSQUZkyE.js +61 -0
  122. data/lib/archsight/web/public/vue/treemap-GDKQZRPO-DTojm7Yr.js +162 -0
  123. data/lib/archsight/web/public/{css/graph.css → vue/useGraphviz-A5s4h76R.js} +2 -1
  124. data/lib/archsight/web/public/vue/useHighlight-C6Kb5G3l.js +10 -0
  125. data/lib/archsight/web/public/vue/useMermaid-DqxTrLRB.js +1 -0
  126. data/lib/archsight/web/public/vue/usePanZoom-BybZ_rfh.js +11 -0
  127. data/lib/archsight/web/public/vue/xychartDiagram-PRI3JC2R-B1ZJZtDC.js +7 -0
  128. data/lib/archsight/web/public/vue.html +15 -0
  129. data/media/artifact.jpg +0 -0
  130. data/media/service.jpg +0 -0
  131. metadata +104 -77
  132. data/lib/archsight/web/public/css/artifact.css +0 -995
  133. data/lib/archsight/web/public/css/base.css +0 -201
  134. data/lib/archsight/web/public/css/highlight.min.css +0 -10
  135. data/lib/archsight/web/public/css/iconoir.css +0 -22
  136. data/lib/archsight/web/public/css/instance.css +0 -818
  137. data/lib/archsight/web/public/css/layout.css +0 -421
  138. data/lib/archsight/web/public/css/mermaid-layers.css +0 -188
  139. data/lib/archsight/web/public/css/pico.min.css +0 -4
  140. data/lib/archsight/web/public/img/archimate.png +0 -0
  141. data/lib/archsight/web/public/img/togaf-high-level.png +0 -0
  142. data/lib/archsight/web/public/js/graph-zoom.js +0 -18
  143. data/lib/archsight/web/public/js/highlight.min.js +0 -3899
  144. data/lib/archsight/web/public/js/htmx.min.js +0 -1
  145. data/lib/archsight/web/public/js/mermaid-init.js +0 -88
  146. data/lib/archsight/web/public/js/mermaid.min.js +0 -2811
  147. data/lib/archsight/web/public/js/sparkline.js +0 -42
  148. data/lib/archsight/web/public/js/svg-pan-zoom.min.js +0 -3
  149. data/lib/archsight/web/public/js/svg-zoom-controls.js +0 -93
  150. data/lib/archsight/web/views/api_docs.erb +0 -19
  151. data/lib/archsight/web/views/index.haml +0 -12
  152. data/lib/archsight/web/views/partials/artifact/_activity.haml +0 -55
  153. data/lib/archsight/web/views/partials/artifact/_agentic.haml +0 -25
  154. data/lib/archsight/web/views/partials/artifact/_deployment.haml +0 -29
  155. data/lib/archsight/web/views/partials/artifact/_git_info.haml +0 -16
  156. data/lib/archsight/web/views/partials/artifact/_language_stats.haml +0 -53
  157. data/lib/archsight/web/views/partials/artifact/_links.haml +0 -24
  158. data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +0 -32
  159. data/lib/archsight/web/views/partials/artifact/_repositories.haml +0 -55
  160. data/lib/archsight/web/views/partials/artifact/_team.haml +0 -83
  161. data/lib/archsight/web/views/partials/artifact/_workflow.haml +0 -69
  162. data/lib/archsight/web/views/partials/components/_activity.haml +0 -37
  163. data/lib/archsight/web/views/partials/components/_git.haml +0 -17
  164. data/lib/archsight/web/views/partials/components/_jira.haml +0 -18
  165. data/lib/archsight/web/views/partials/components/_languages.haml +0 -29
  166. data/lib/archsight/web/views/partials/components/_owner.haml +0 -15
  167. data/lib/archsight/web/views/partials/components/_repositories.haml +0 -37
  168. data/lib/archsight/web/views/partials/components/_status.haml +0 -23
  169. data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +0 -74
  170. data/lib/archsight/web/views/partials/instance/_analysis_result.haml +0 -64
  171. data/lib/archsight/web/views/partials/instance/_detail.haml +0 -103
  172. data/lib/archsight/web/views/partials/instance/_graph.haml +0 -6
  173. data/lib/archsight/web/views/partials/instance/_import_detail.haml +0 -87
  174. data/lib/archsight/web/views/partials/instance/_list.haml +0 -84
  175. data/lib/archsight/web/views/partials/instance/_relations.haml +0 -43
  176. data/lib/archsight/web/views/partials/instance/_requirements.haml +0 -41
  177. data/lib/archsight/web/views/partials/instance/_view_detail.haml +0 -57
  178. data/lib/archsight/web/views/partials/layout/_content.haml +0 -44
  179. data/lib/archsight/web/views/partials/layout/_error.haml +0 -22
  180. data/lib/archsight/web/views/partials/layout/_head.haml +0 -24
  181. data/lib/archsight/web/views/partials/layout/_navigation.haml +0 -21
  182. data/lib/archsight/web/views/partials/layout/_sidebar.haml +0 -27
  183. data/lib/archsight/web/views/search.haml +0 -53
  184. /data/{lib/archsight/web/doc → docs}/archimate.md +0 -0
  185. /data/{lib/archsight/web/doc → docs}/computed_annotations.md +0 -0
  186. /data/{lib/archsight/web/doc → docs}/icons.md +0 -0
  187. /data/{lib/archsight/web/doc → docs}/modeling.md +0 -0
  188. /data/{lib/archsight/web/doc → docs}/search.md +0 -0
  189. /data/{lib/archsight/web/doc → docs}/togaf.md +0 -0
  190. /data/{lib/archsight/web/doc → docs}/tool.md +0 -0
data/docs/licenses.md ADDED
@@ -0,0 +1,307 @@
1
+ # License Scanning
2
+
3
+ The repository import automatically detects and analyzes software licenses for each imported repository. This produces a "Layer 1 Licensing Profile" that characterizes license usage across your portfolio.
4
+
5
+ ## Overview
6
+
7
+ When the `repository` handler imports a git repository, it runs license analysis alongside code metrics and git history analysis. The license analyzer:
8
+
9
+ 1. **Detects the repository's own license** from LICENSE/COPYING files, SPDX headers, and package manifests
10
+ 2. **Scans dependency licenses** using language-specific tools (when available) or falls back to manifest-based counting
11
+ 3. **Classifies risk** based on copyleft presence and unknown license ratios
12
+
13
+ Results are stored as `license/*` annotations on the TechnologyArtifact resource.
14
+
15
+ ## How License Detection Works
16
+
17
+ The analyzer uses a priority-based detection strategy:
18
+
19
+ ### 1. SPDX-License-Identifier Headers (highest priority)
20
+
21
+ Source files are scanned for standardized SPDX headers:
22
+
23
+ ```go
24
+ // SPDX-License-Identifier: Apache-2.0
25
+ package main
26
+ ```
27
+
28
+ This is the most reliable signal and takes priority over all other methods.
29
+
30
+ ### 2. LICENSE / COPYING Files
31
+
32
+ The analyzer searches for these files in the repository root (in order):
33
+
34
+ - `LICENSE`, `LICENSE.md`, `LICENSE.txt`
35
+ - `LICENCE`, `LICENCE.md`, `LICENCE.txt`
36
+ - `COPYING`, `COPYING.md`, `COPYING.txt`
37
+
38
+ File content is matched against known license patterns for:
39
+
40
+ | License | Category |
41
+ |---------|----------|
42
+ | Apache-2.0 | permissive |
43
+ | MIT | permissive |
44
+ | BSD-3-Clause, BSD-2-Clause | permissive |
45
+ | ISC | permissive |
46
+ | Unlicense, CC0-1.0, BSL-1.0 | permissive |
47
+ | GPL-3.0, GPL-2.0, AGPL-3.0 | copyleft |
48
+ | LGPL-3.0, LGPL-2.1 | weak-copyleft |
49
+ | MPL-2.0, EUPL-1.2 | weak-copyleft |
50
+
51
+ ### 3. Package Manifest Fallback (lowest priority)
52
+
53
+ If no LICENSE file is found, the analyzer checks manifest files:
54
+
55
+ - `package.json` `"license"` field (Node.js)
56
+ - `*.gemspec` `spec.license` (Ruby)
57
+ - `Cargo.toml` `license` key (Rust)
58
+ - `pyproject.toml` `license` key (Python)
59
+
60
+ ## Dependency License Scanning
61
+
62
+ ### Supported Ecosystems
63
+
64
+ The analyzer detects ecosystems from manifest files and attempts to scan dependencies:
65
+
66
+ | Ecosystem | Detected By | External Tool | Fallback |
67
+ |-----------|-------------|---------------|----------|
68
+ | Go | `go.mod` | `go-licenses` | Count `go.sum` entries |
69
+ | Python | `requirements.txt`, `pyproject.toml`, `Pipfile` | `pip-licenses` | Count requirements entries |
70
+ | Ruby | `Gemfile`, `Gemfile.lock` | `license_finder` | Count lockfile entries |
71
+ | Java | `pom.xml`, `build.gradle` | Maven license plugin | Count `<dependency>` blocks |
72
+ | Node.js | `package.json` | `license-checker` | Count dependency keys |
73
+ | Rust | `Cargo.toml` | `cargo-license` | Count `Cargo.lock` packages |
74
+
75
+ ### External Tools (Soft Dependencies)
76
+
77
+ When external tools are available, they provide per-dependency license identification. The analyzer tries multiple command variants for each ecosystem and falls back gracefully:
78
+
79
+ | Ecosystem | Direct command | Runner fallback |
80
+ |-----------|---------------|-----------------|
81
+ | Go | `go-licenses` | — |
82
+ | Python | `pip-licenses` | `python -m piplicenses` / `python3 -m piplicenses` |
83
+ | Ruby | `license_finder` | `gem exec license_finder` |
84
+ | Node.js | `license-checker` | — |
85
+ | Rust | `cargo-license` | `cargo license` |
86
+
87
+ For Python projects, the analyzer also checks for `pip-licenses` inside `.venv/` or `venv/` virtual environments before trying system commands.
88
+
89
+ If no tool is available, the analyzer falls back to counting dependencies from lockfiles and manifests. These dependencies are recorded with an "unknown" license type.
90
+
91
+ ### Performance
92
+
93
+ The license analyzer is optimized for batch imports of many repositories:
94
+
95
+ - **Command caching**: The first repository probes which tool variant works for each ecosystem. All subsequent repositories reuse the cached result instantly — no repeated failed process spawns.
96
+ - **No download-on-demand**: Runners like `npx --yes` or `go run ...@latest` that download/compile tools are not used, as they add 10-30s per repo. Install tools globally instead.
97
+ - **Parallel execution**: License analysis and team matching run concurrently in the repository handler.
98
+ - **Fast fallback**: When no external tool is found, manifest-based dependency counting is near-instant (file reads only).
99
+
100
+ To get the best results, pre-install tools globally:
101
+
102
+ ```bash
103
+ go install github.com/google/go-licenses@latest # Go
104
+ pip install pip-licenses # Python
105
+ gem install license_finder # Ruby
106
+ npm install -g license-checker # Node.js
107
+ cargo install cargo-license # Rust
108
+ ```
109
+
110
+ ### Risk Classification
111
+
112
+ Dependency risk is assessed based on detected license types:
113
+
114
+ | Risk Level | Criteria |
115
+ |------------|----------|
116
+ | **low** | All dependency licenses are permissive |
117
+ | **medium** | Weak-copyleft licenses present (LGPL, MPL, EUPL) but no strong copyleft |
118
+ | **high** | Strong copyleft (GPL, AGPL) present, or majority of licenses are unknown |
119
+ | **unknown** | No dependency license data available |
120
+
121
+ ## License Normalization
122
+
123
+ Dependency tools return messy license strings — copyright notices, long-form names, leading parentheses, dual licenses, and domain names. The analyzer normalizes these into canonical SPDX identifiers in five phases:
124
+
125
+ ### 1. Proprietary Detection
126
+
127
+ Before any cleanup, the raw string is checked for proprietary markers. If matched, the result is `proprietary`:
128
+
129
+ | Pattern | Examples |
130
+ |---------|----------|
131
+ | Copyright notice | `Copyright (C) 2024 Acme Corp`, `(c) 1&1 IONOS Cloud GmbH` |
132
+ | Proprietary keyword | `Proprietary License`, `UNLICENSED` |
133
+ | Internal marker | `IONOS internal`, `internal` |
134
+ | Custom URL prefix | `Custom: https://example.com/license` |
135
+ | Domain-as-license | `ionos.com`, `profitbricks.com`, `example.io` |
136
+
137
+ ### 2. Clean
138
+
139
+ Strip leading `("(`, trailing `,;)"*`, and surrounding whitespace. This handles messy tool output like `"MIT`, `(GPL-2.0`, `Apache*`.
140
+
141
+ ### 3. Dual License Split
142
+
143
+ If the string contains `/` or ` OR `, it is split and the first recognized SPDX ID is returned:
144
+
145
+ | Input | Output |
146
+ |-------|--------|
147
+ | `MIT/Apache-2.0` | `MIT` |
148
+ | `(MIT OR CC0-1.0)` | `MIT` |
149
+
150
+ ### 4. Long-form Pattern Matching
151
+
152
+ Common long-form names and aliases are mapped to canonical SPDX IDs:
153
+
154
+ | Input | Output |
155
+ |-------|--------|
156
+ | `Apache License, Version 2.0` / `Apache 2.0` / `Apache` | `Apache-2.0` |
157
+ | `The MIT License` | `MIT` |
158
+ | `New BSD` | `BSD-3-Clause` |
159
+ | `Simplified BSD` / `BSD 2-Clause` | `BSD-2-Clause` |
160
+ | `0BSD` | `0BSD` |
161
+ | `GNU General Public License v2` / `GPLv2` | `GPL-2.0` |
162
+ | `GNU LGPL 3` / `GNU Lesser General Public License` | `LGPL-3.0` / `LGPL-2.1` |
163
+ | `CDDL + GPLv2 with classpath exception` | `CDDL-1.0` |
164
+ | `UNKNOWN` | `unknown` |
165
+ | `ruby` | `Ruby` |
166
+
167
+ ### 5. Fallback
168
+
169
+ If no pattern matches, the cleaned string is returned as-is.
170
+
171
+ ## Generated Annotations
172
+
173
+ The license analyzer produces these annotations on TechnologyArtifact resources:
174
+
175
+ ### Repository License
176
+
177
+ | Annotation | Description | Example |
178
+ |------------|-------------|---------|
179
+ | `license/spdx` | SPDX license identifier | `Apache-2.0` |
180
+ | `license/file` | Detected license file name | `LICENSE` |
181
+ | `license/category` | License category | `permissive` |
182
+
183
+ ### Dependency Licenses
184
+
185
+ | Annotation | Description | Example |
186
+ |------------|-------------|---------|
187
+ | `license/dependencies/count` | Total dependency count | `142` |
188
+ | `license/dependencies/ecosystems` | Detected ecosystems (comma-separated) | `go,python` |
189
+ | `license/dependencies/licenses` | Unique license types found | `Apache-2.0,BSD-3-Clause,MIT` |
190
+ | `license/dependencies/copyleft` | Whether copyleft deps exist | `false` |
191
+ | `license/dependencies/risk` | Overall risk level | `low` |
192
+ | `license/dependencies/*/count` | Per-license-type count | `license/dependencies/MIT/count: 80` |
193
+
194
+ ## Querying License Data
195
+
196
+ Use the search and query system to find repositories by license characteristics:
197
+
198
+ ```
199
+ # Find all repositories with copyleft licenses
200
+ TechnologyArtifact: license/category == "copyleft"
201
+
202
+ # Find repos with high dependency risk
203
+ TechnologyArtifact: license/dependencies/risk == "high"
204
+
205
+ # Find repos with copyleft dependencies
206
+ TechnologyArtifact: license/dependencies/copyleft == "true"
207
+
208
+ # Find repos using a specific license
209
+ TechnologyArtifact: license/spdx == "MIT"
210
+
211
+ # Find repos with Go dependencies
212
+ TechnologyArtifact: license/dependencies/ecosystems *= "go"
213
+ ```
214
+
215
+ ## Aggregating License Profiles
216
+
217
+ Use [computed annotations](/doc/computed_annotations) to build a licensing profile across products or services. For example, on an ApplicationComponent that groups multiple repositories:
218
+
219
+ ```ruby
220
+ # Count repos per license category
221
+ computed_annotation 'computed/license_permissive_count',
222
+ title: 'Permissive License Repos',
223
+ type: Integer do
224
+ artifacts = outgoing_transitive('TechnologyArtifact: license/category == "permissive"')
225
+ count(artifacts)
226
+ end
227
+
228
+ # Detect if any copyleft dependencies exist in the portfolio
229
+ computed_annotation 'computed/license_copyleft_present',
230
+ title: 'Copyleft Dependencies',
231
+ filter: :word do
232
+ artifacts = outgoing_transitive('TechnologyArtifact: license/dependencies/copyleft == "true"')
233
+ count(artifacts).positive? ? "true" : "false"
234
+ end
235
+
236
+ # Collect all unique dependency licenses across the portfolio
237
+ computed_annotation 'computed/license_types',
238
+ title: 'All License Types',
239
+ filter: :list,
240
+ list: true do
241
+ collect(outgoing_transitive(:TechnologyArtifact), 'license/dependencies/licenses')
242
+ end
243
+
244
+ # Highest dependency risk across all repos
245
+ computed_annotation 'computed/license_max_risk',
246
+ title: 'Max License Risk',
247
+ filter: :word do
248
+ risks = collect(outgoing_transitive(:TechnologyArtifact), 'license/dependencies/risk')
249
+ %w[high medium low unknown].find { |r| risks.include?(r) }
250
+ end
251
+ ```
252
+
253
+ ## Example Output
254
+
255
+ After importing a Go repository with an Apache-2.0 license:
256
+
257
+ ```yaml
258
+ apiVersion: architecture/v1alpha1
259
+ kind: TechnologyArtifact
260
+ metadata:
261
+ name: "Repo:my-service"
262
+ annotations:
263
+ artifact/type: repo
264
+ repository/git: git@github.com:org/my-service.git
265
+ license/spdx: Apache-2.0
266
+ license/file: LICENSE
267
+ license/category: permissive
268
+ license/dependencies/count: "87"
269
+ license/dependencies/ecosystems: go
270
+ license/dependencies/licenses: Apache-2.0,BSD-3-Clause,MIT
271
+ license/dependencies/copyleft: "false"
272
+ license/dependencies/risk: low
273
+ license/dependencies/MIT/count: "42"
274
+ license/dependencies/Apache-2.0/count: "30"
275
+ license/dependencies/BSD-3-Clause/count: "15"
276
+ spec:
277
+ suppliedBy:
278
+ technologyComponents:
279
+ - "Git:Github"
280
+ ```
281
+
282
+ ## Web UI
283
+
284
+ License information is displayed in the TechnologyArtifact detail view with:
285
+
286
+ - A color-coded badge for the repository license (green = permissive, yellow = weak-copyleft, red = copyleft)
287
+ - Dependency count and ecosystem badges
288
+ - Risk level indicator
289
+ - Expandable per-license-type breakdown with counts
290
+
291
+ ## Troubleshooting
292
+
293
+ ### License Not Detected
294
+
295
+ If a repository shows `NOASSERTION` for its license:
296
+
297
+ 1. Verify the LICENSE file exists in the repository root
298
+ 2. Check that the license text matches a known pattern (some custom licenses won't match)
299
+ 3. Add an SPDX header to a source file for reliable detection
300
+
301
+ ### All Dependencies Show "unknown" License
302
+
303
+ This means no external scanning tool was available. The analyzer fell back to counting dependencies from manifest/lockfiles. Install the appropriate tool for the ecosystem (see [External Tools](#external-tools) above).
304
+
305
+ ### High Risk Due to Unknown Licenses
306
+
307
+ A high proportion of "unknown" licenses triggers a "high" risk classification. This is by design - unknown licenses should be investigated. Install ecosystem-specific tools to resolve unknowns into actual license identifiers.
@@ -79,9 +79,6 @@ module Archsight
79
79
  # Filter by name pattern if provided
80
80
  analyses = analyses.select { |a| filter.match?(a.name) } if filter
81
81
 
82
- # Filter to enabled analyses
83
- analyses = analyses.select { |a| analysis_enabled?(a) }
84
-
85
82
  analyses.map { |analysis| execute(analysis) }
86
83
  end
87
84
 
@@ -100,13 +97,6 @@ module Archsight
100
97
 
101
98
  DEFAULT_TIMEOUT
102
99
  end
103
-
104
- # Check if analysis is enabled
105
- # @param analysis [Object] Analysis instance
106
- # @return [Boolean] true if enabled
107
- def analysis_enabled?(analysis)
108
- analysis.annotations["analysis/enabled"] != "false"
109
- end
110
100
  end
111
101
  end
112
102
  end
@@ -4,7 +4,7 @@ require_relative "email_recipient"
4
4
 
5
5
  # Annotation represents a single annotation definition with its schema and behavior
6
6
  class Archsight::Annotations::Annotation
7
- attr_reader :key, :description, :filter, :format, :enum, :sidebar, :type, :list
7
+ attr_reader :key, :description, :filter, :format, :enum, :sidebar, :type, :list, :editor
8
8
 
9
9
  def initialize(key, options = {})
10
10
  @key = key
@@ -14,6 +14,7 @@ class Archsight::Annotations::Annotation
14
14
  @enum = options[:enum]
15
15
  @sidebar = options.fetch(:sidebar, true)
16
16
  @list = options.fetch(:list, false)
17
+ @editor = options.fetch(:editor, true)
17
18
  @type = options[:type]
18
19
 
19
20
  # Auto-add filter if enum present
@@ -80,43 +81,12 @@ class Archsight::Annotations::Annotation
80
81
 
81
82
  # Validate a value and return array of error messages (empty if valid)
82
83
  def validate(value)
83
- errors = []
84
+ errors = [] #: Array[String]
84
85
  return errors if value.nil?
85
86
 
86
- # Check enum constraint
87
- if @enum
88
- values = list? ? value.to_s.split(",").map(&:strip) : [value.to_s]
89
- invalid_values = values.reject { |v| @enum.include?(v) }
90
- invalid_values.each do |v|
91
- errors << "invalid value '#{v}'. Expected one of: #{@enum.join(", ")}"
92
- end
93
- end
94
-
95
- # Check type constraint
96
- if @type.is_a?(Class) && errors.empty?
97
- values_to_check = list? ? value.to_s.split(/,|\n/).map(&:strip).reject(&:empty?) : [value.to_s]
98
-
99
- values_to_check.each do |string_value|
100
- valid = case @type.to_s
101
- when "Integer"
102
- string_value.match?(/\A-?\d+\z/)
103
- when "Float"
104
- string_value.match?(/\A-?\d+(\.\d+)?\z/)
105
- when "URI"
106
- begin
107
- URI.parse(string_value)
108
- string_value.match?(%r{\Ahttps?://})
109
- rescue URI::InvalidURIError
110
- false
111
- end
112
- when "Archsight::Annotations::EmailRecipient"
113
- Archsight::Annotations::EmailRecipient.valid?(string_value)
114
- else
115
- true
116
- end
117
- errors << "invalid value '#{string_value}'. #{type_error_message}" unless valid
118
- end
119
- end
87
+ validate_enum(value, errors)
88
+ validate_type(value, errors) if errors.empty?
89
+ validate_code(value, errors) if errors.empty?
120
90
 
121
91
  errors
122
92
  end
@@ -130,6 +100,18 @@ class Archsight::Annotations::Annotation
130
100
  @format == :markdown
131
101
  end
132
102
 
103
+ def code?
104
+ @format == :ruby
105
+ end
106
+
107
+ def multiline?
108
+ @format == :multiline
109
+ end
110
+
111
+ def code_language
112
+ @format if code?
113
+ end
114
+
133
115
  # Example value for templates
134
116
  def example_value
135
117
  if @enum
@@ -155,6 +137,49 @@ class Archsight::Annotations::Annotation
155
137
  end
156
138
  end
157
139
 
140
+ def validate_enum(value, errors)
141
+ return unless @enum
142
+
143
+ values = list? ? value.to_s.split(",").map(&:strip) : [value.to_s]
144
+ invalid_values = values.reject { |v| @enum.include?(v) }
145
+ invalid_values.each do |v|
146
+ errors << "invalid value '#{v}'. Expected one of: #{@enum.join(", ")}"
147
+ end
148
+ end
149
+
150
+ def validate_type(value, errors)
151
+ return unless @type.is_a?(Class)
152
+
153
+ values_to_check = list? ? value.to_s.split(/,|\n/).map(&:strip).reject(&:empty?) : [value.to_s]
154
+ values_to_check.each do |string_value|
155
+ errors << "invalid value '#{string_value}'. #{type_error_message}" unless valid_type_value?(string_value)
156
+ end
157
+ end
158
+
159
+ def valid_type_value?(string_value)
160
+ case @type.to_s
161
+ when "Integer" then string_value.match?(/\A-?\d+\z/)
162
+ when "Float" then string_value.match?(/\A-?\d+(\.\d+)?\z/)
163
+ when "URI" then valid_uri?(string_value)
164
+ when "Archsight::Annotations::EmailRecipient" then Archsight::Annotations::EmailRecipient.valid?(string_value)
165
+ else true
166
+ end
167
+ end
168
+
169
+ def valid_uri?(string_value)
170
+ URI.parse(string_value)
171
+ string_value.match?(%r{\Ahttps?://})
172
+ rescue URI::InvalidURIError
173
+ false
174
+ end
175
+
176
+ def validate_code(value, errors)
177
+ return unless code? && !value.to_s.strip.empty?
178
+
179
+ syntax_error = validate_code_syntax(value.to_s)
180
+ errors << syntax_error if syntax_error
181
+ end
182
+
158
183
  def derive_format
159
184
  case @filter
160
185
  when :word then :tag_word
@@ -165,4 +190,28 @@ class Archsight::Annotations::Annotation
165
190
  def build_regex
166
191
  Regexp.new("^#{Regexp.escape(key).gsub('\*', ".+")}$")
167
192
  end
193
+
194
+ # Validate code syntax based on format
195
+ # @param code [String] The code to validate
196
+ # @return [String, nil] Error message or nil if valid
197
+ def validate_code_syntax(code)
198
+ case @format
199
+ when :ruby
200
+ validate_ruby_syntax(code)
201
+ end
202
+ end
203
+
204
+ # Validate Ruby syntax using RubyVM
205
+ # @param code [String] Ruby code to validate
206
+ # @return [String, nil] Error message or nil if valid
207
+ def validate_ruby_syntax(code)
208
+ # steep:ignore:start
209
+ RubyVM::InstructionSequence.compile(code)
210
+ # steep:ignore:end
211
+ nil
212
+ rescue SyntaxError => e
213
+ # Extract just the error message without the full backtrace
214
+ message = e.message.lines.first&.strip || "Syntax error"
215
+ "Ruby syntax error: #{message}"
216
+ end
168
217
  end
@@ -9,12 +9,8 @@ module Archsight::Annotations::Architecture
9
9
  annotation "architecture/abbr",
10
10
  description: "Abbreviation or short name",
11
11
  title: "Abbreviation"
12
- annotation "architecture/evidence",
13
- description: "Supporting evidence or notes",
14
- title: "Evidence",
15
- format: :markdown
16
12
  annotation "architecture/description",
17
- description: "Textual description of the interface",
13
+ description: "Textual description of the resource",
18
14
  title: "Description",
19
15
  format: :markdown
20
16
  annotation "architecture/documentation",
@@ -25,35 +21,6 @@ module Archsight::Annotations::Architecture
25
21
  description: "Comma-separated tags",
26
22
  filter: :list,
27
23
  title: "Tags"
28
- annotation "architecture/encoding",
29
- description: "Data encoding format",
30
- filter: :list,
31
- title: "Encoding"
32
- annotation "architecture/title",
33
- description: "Interface title",
34
- title: "Title"
35
- annotation "architecture/openapi",
36
- description: "OpenAPI specification version",
37
- filter: :word,
38
- title: "OpenAPI"
39
- annotation "architecture/version",
40
- description: "API or interface version",
41
- filter: :word,
42
- title: "Version",
43
- sidebar: false
44
- annotation "architecture/status",
45
- description: "Lifecycle status (General-Availability, Early-Access, Development)",
46
- filter: :word,
47
- title: "Status"
48
- annotation "architecture/visibility",
49
- description: "API visibility (public, private, internal)",
50
- filter: :word,
51
- enum: %w[public private internal],
52
- title: "Visibility"
53
- annotation "architecture/applicationSets",
54
- description: "Related ArgoCD ApplicationSets",
55
- title: "ApplicationSets",
56
- format: :markdown
57
24
  end
58
25
  end
59
26
  end
@@ -212,7 +212,7 @@ class Archsight::Annotations::ComputedManager
212
212
  @computing.add(cache_key)
213
213
  begin
214
214
  evaluator = Archsight::Annotations::ComputedEvaluator.new(instance, @database, self)
215
- value = evaluator.instance_eval(&definition.block)
215
+ value = evaluator.instance_eval(&definition.block) # steep:ignore BlockTypeMismatch
216
216
 
217
217
  # Apply type coercion if specified
218
218
  value = coerce_value(value, definition.type) if definition.type
@@ -7,15 +7,18 @@ module Archsight::Annotations::Generated
7
7
  annotation "generated/script",
8
8
  description: "Name of the script that generated this resource",
9
9
  title: "Generated By",
10
- sidebar: false
10
+ sidebar: false,
11
+ editor: false
11
12
  annotation "generated/at",
12
13
  description: "Timestamp when this resource was generated (ISO8601)",
13
14
  title: "Generated At",
14
- sidebar: false
15
+ sidebar: false,
16
+ editor: false
15
17
  annotation "generated/configHash",
16
18
  description: "Hash of configuration used to generate this resource (for change detection)",
17
19
  title: "Config Hash",
18
- sidebar: false
20
+ sidebar: false,
21
+ editor: false
19
22
  end
20
23
  end
21
24
  end
@@ -6,16 +6,20 @@ module Archsight::Annotations::Git
6
6
  base.class_eval do
7
7
  annotation "git/updatedAt",
8
8
  description: "Date when the resource was last updated",
9
- title: "Updated At"
9
+ title: "Updated At",
10
+ editor: false
10
11
  annotation "git/updatedBy",
11
12
  description: "Email of person who last updated the resource",
12
- title: "Updated By"
13
+ title: "Updated By",
14
+ editor: false
13
15
  annotation "git/reviewedAt",
14
16
  description: "Date when the resource was last reviewed",
15
- title: "Reviewed At"
17
+ title: "Reviewed At",
18
+ editor: false
16
19
  annotation "git/reviewedBy",
17
20
  description: "Email of person who last reviewed the resource",
18
- title: "Reviewed By"
21
+ title: "Reviewed By",
22
+ editor: false
19
23
  end
20
24
  end
21
25
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Interface module adds interface-specific annotations to resource classes
4
+ module Archsight::Annotations::Interface
5
+ def self.included(base)
6
+ base.class_eval do
7
+ annotation "architecture/encoding",
8
+ description: "Data encoding format",
9
+ filter: :list,
10
+ title: "Encoding"
11
+ annotation "architecture/title",
12
+ description: "Interface title",
13
+ title: "Title"
14
+ annotation "architecture/openapi",
15
+ description: "OpenAPI specification version",
16
+ filter: :word,
17
+ title: "OpenAPI"
18
+ annotation "architecture/version",
19
+ description: "API or interface version",
20
+ filter: :word,
21
+ title: "Version",
22
+ sidebar: false
23
+ annotation "architecture/status",
24
+ description: "Lifecycle status",
25
+ filter: :word,
26
+ enum: %w[General-Availability Early-Access Development],
27
+ title: "Status"
28
+ annotation "architecture/visibility",
29
+ description: "API visibility (public, private, internal)",
30
+ filter: :word,
31
+ enum: %w[public private internal],
32
+ title: "Visibility"
33
+ end
34
+ end
35
+ end
data/lib/archsight/cli.rb CHANGED
@@ -19,6 +19,7 @@ module Archsight
19
19
  option :production, type: :boolean, default: false, desc: "Run in production mode"
20
20
  option :disable_reload, type: :boolean, default: false, desc: "Disable the reload button in the UI"
21
21
  option :enable_logging, type: :boolean, default: nil, desc: "Enable request logging (default: false in dev, true in prod)"
22
+ option :inline_edit, type: :boolean, default: false, desc: "Enable inline editing (save directly to source files)"
22
23
  def web
23
24
  configure_resources
24
25
  require "archsight/web/application"
@@ -26,6 +27,7 @@ module Archsight
26
27
  env = options[:production] ? :production : :development
27
28
  Archsight::Web::Application.configure_environment!(env, logging: options[:enable_logging])
28
29
  Archsight::Web::Application.set :reload_enabled, !options[:disable_reload]
30
+ Archsight::Web::Application.set :inline_edit_enabled, options[:inline_edit]
29
31
  Archsight::Web::Application.setup_mcp!
30
32
  Archsight::Web::Application.run!(port: options[:port], bind: options[:host])
31
33
  rescue Archsight::ResourceError => e
@@ -102,7 +104,7 @@ module Archsight
102
104
 
103
105
  # Create database that loads from resources directory
104
106
  # Only load Import resources to avoid validation errors on incomplete resources
105
- db = Archsight::Database.new(resources_dir, verbose: options[:verbose], only_kinds: ["Import"], verify: false)
107
+ db = Archsight::Database.new(resources_dir, verbose: options[:verbose], only_kinds: %w[Import BusinessActor], verify: false)
106
108
 
107
109
  if options[:dry_run]
108
110
  puts "Execution Plan:"
@@ -191,7 +193,7 @@ module Archsight
191
193
  def filter_analyses(db)
192
194
  analyses = db.instances_by_kind("Analysis").values
193
195
  analyses = analyses.select { |a| Regexp.new(options[:filter], Regexp::IGNORECASE).match?(a.name) } if options[:filter]
194
- analyses.reject { |a| a.annotations["analysis/enabled"] == "false" }
196
+ analyses
195
197
  end
196
198
 
197
199
  def print_analysis_dry_run(analyses)