archsight 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/docs/computed_annotations.md +7 -5
- data/docs/licenses.md +1 -2
- data/docs/modeling.md +5 -1
- data/docs/pages.md +63 -6
- data/docs/search.md +17 -0
- data/lib/archsight/annotations/annotation.rb +5 -4
- data/lib/archsight/annotations/computed.rb +5 -1
- data/lib/archsight/annotations/relation_resolver.rb +96 -79
- data/lib/archsight/cli.rb +3 -2
- data/lib/archsight/database.rb +2 -1
- data/lib/archsight/documentation.rb +2 -1
- 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/view_blocks.rb +108 -0
- data/lib/archsight/helpers/wiki_links.rb +39 -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 +25 -3
- data/lib/archsight/mcp/base.rb +38 -0
- data/lib/archsight/requirements.rb +70 -0
- data/lib/archsight/resources/analysis.rb +2 -1
- data/lib/archsight/resources/application_component.rb +5 -3
- data/lib/archsight/resources/application_interface.rb +4 -2
- data/lib/archsight/resources/application_service.rb +4 -3
- data/lib/archsight/resources/base.rb +27 -12
- data/lib/archsight/resources/business_actor.rb +5 -3
- data/lib/archsight/resources/business_product.rb +4 -3
- data/lib/archsight/resources/business_requirement.rb +4 -3
- data/lib/archsight/resources/compliance_evidence.rb +4 -2
- data/lib/archsight/resources/data_object.rb +2 -1
- data/lib/archsight/resources/import.rb +5 -2
- data/lib/archsight/resources/page.rb +4 -2
- data/lib/archsight/resources/technology_artifact.rb +4 -3
- data/lib/archsight/resources/technology_node.rb +1 -1
- 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/version.rb +1 -1
- data/lib/archsight/view_table.rb +102 -0
- data/lib/archsight/web/api/json_helpers.rb +7 -5
- data/lib/archsight/web/api/openapi/spec.yaml +135 -2
- 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 +6 -1
- data/lib/archsight/web/public/vue/ApiDocsPage-D-cPRZCT.js +1 -0
- data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +1 -0
- data/lib/archsight/web/public/vue/{DocPage-uaT8CdFm.js → DocPage-DK6vNDbF.js} +1 -1
- data/lib/archsight/web/public/vue/EditorPage-BoJpQaVw.js +35 -0
- data/lib/archsight/web/public/vue/EditorPage-CbG4mc9T.css +1 -0
- data/lib/archsight/web/public/vue/ErrorPage-Ck9izEUS.css +1 -0
- data/lib/archsight/web/public/vue/ErrorPage-PGyjdtEf.js +2 -0
- data/lib/archsight/web/public/vue/GraphView-BLiKR4zP.js +1 -0
- data/lib/archsight/web/public/vue/GraphView-BvWAbUAl.css +1 -0
- data/lib/archsight/web/public/vue/HomePage-C0lR8i2C.js +2 -0
- data/lib/archsight/web/public/vue/InstanceRouter-60Tt3ZNM.css +1 -0
- data/lib/archsight/web/public/vue/InstanceRouter-D3W2jJHV.js +1 -0
- data/lib/archsight/web/public/vue/KindList-BlsaRBNO.js +1 -0
- data/lib/archsight/web/public/vue/PageView-9MgHtrgl.js +1 -0
- data/lib/archsight/web/public/vue/QueryError-3EqjpbyV.css +1 -0
- data/lib/archsight/web/public/vue/QueryError-D1FL1xgA.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-B0FLaFR3.css +1 -0
- data/lib/archsight/web/public/vue/ResourceList-vkgFyeOY.js +2 -0
- data/lib/archsight/web/public/vue/SearchResults-Cl3O_OEr.js +1 -0
- data/lib/archsight/web/public/vue/SearchResults-DiW5XVYW.css +1 -0
- data/lib/archsight/web/public/vue/WikiPage-C-8SG67a.css +1 -0
- data/lib/archsight/web/public/vue/WikiPage-CeCQBTDS.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-D7m61Ahx.js +3 -0
- data/lib/archsight/web/public/vue/index-Dbx3MXWG.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 +49 -40
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 336946fbac9548285ad368125db862c2f1926d99f2f40c1d40ba4155ebe8ca45
|
|
4
|
+
data.tar.gz: 63ff3e1a973432f98b5ffb66ac9c518a204c0f87d90ee495615cf60bba293cd3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e76456b0f0ed4db22057a4df669953dfcdf7747ed8293ca3092be866e4eea6a4e4c63df6ffeaf6f163da552bf163f409dcb1184a1a493d12e565b0304e527202
|
|
7
|
+
data.tar.gz: 60a37bef6e039f2bcb20d23fc1010baacdebc65cdded4e8d2f91992583bf60f8245c74175ef26945f3b67e67d6a3421de962c3d8513536376383b55e98bf8afb
|
data/README.md
CHANGED
|
@@ -97,11 +97,11 @@ claude mcp add --transport sse ionos-architecture http://localhost:4567/mcp/sse
|
|
|
97
97
|
- `analyze_resource` - Get detailed resource information and impact analysis
|
|
98
98
|
- `resource_doc` - Get documentation for resource kinds
|
|
99
99
|
|
|
100
|
-
**Export to Confluence**: `archsight export --to confluence` publishes pages to the Confluence page they link to, with images, diagrams and draw.io, and refuses to overwrite edits made in Confluence unless `--force` ([Wiki pages](docs/pages.md#exporting-to-confluence)).
|
|
100
|
+
**Export to Confluence**: `archsight export --to confluence` publishes pages to the Confluence page they link to, with images, diagrams and draw.io, views and requirements as tables, and refuses to overwrite edits made in Confluence unless `--force` ([Wiki pages](docs/pages.md#exporting-to-confluence)).
|
|
101
101
|
|
|
102
102
|
**Macros** such as `{status:yellow WIP}` and `{emoticon:2705}` work inline in pages ([Wiki pages](docs/pages.md#macros)).
|
|
103
103
|
|
|
104
|
-
**Views and analyses** can be embedded in pages with `![[View/Name]]` / `![[Analysis/Name]]` ([Wiki pages](docs/pages.md#embedding-views-and-analyses)).
|
|
104
|
+
**Views and analyses** can be embedded in pages with `![[View/Name]]` / `![[Analysis/Name]]` ([Wiki pages](docs/pages.md#embedding-views-and-analyses)); a view can also be written in place with a ```` ```view ```` block ([inline views](docs/pages.md#inline-views)), and the business requirements of a selection of resources shown with a ```` ```requirements ```` block ([requirements](docs/pages.md#business-requirements-of-a-selection-of-resources)).
|
|
105
105
|
|
|
106
106
|
**Images and draw.io diagrams** are plain files in the resources directory and are embedded in markdown with relative
|
|
107
107
|
paths (``, ``); only files of image, draw.io and `.asd` diagram types inside the resources
|
|
@@ -40,9 +40,13 @@ Computed annotations support all the same options as regular annotations:
|
|
|
40
40
|
| `enum` | Array | nil | Allowed values |
|
|
41
41
|
| `sidebar` | Boolean | false | Show in sidebar |
|
|
42
42
|
| `type` | Class | nil | Type for value coercion (Integer, Float, String) |
|
|
43
|
-
| `
|
|
43
|
+
| `summary` | Boolean | false | Return the value with every search hit of this kind (at most 3 per kind) |
|
|
44
44
|
| `&block` | Block | required | The computation logic |
|
|
45
45
|
|
|
46
|
+
`summary: true` (also available on regular `annotation` definitions) marks the value as a summary attribute: the
|
|
47
|
+
search API returns it with every hit of the kind (`highlights`) and the result page shows it next to the name.
|
|
48
|
+
A kind can have at most 3 summary annotations; a fourth raises an `ArgumentError` when the kind is defined.
|
|
49
|
+
|
|
46
50
|
### Value Handling
|
|
47
51
|
|
|
48
52
|
- **Nil values**: Not stored (annotation key won't exist)
|
|
@@ -89,8 +93,7 @@ end
|
|
|
89
93
|
computed_annotation 'computed/languages',
|
|
90
94
|
title: 'Languages',
|
|
91
95
|
description: 'All programming languages used',
|
|
92
|
-
filter: :list
|
|
93
|
-
list: true do
|
|
96
|
+
filter: :list do
|
|
94
97
|
collect(outgoing_transitive(:TechnologyArtifact), 'scc/languages')
|
|
95
98
|
end
|
|
96
99
|
```
|
|
@@ -299,8 +302,7 @@ class ApplicationComponent < Base
|
|
|
299
302
|
computed_annotation 'computed/languages',
|
|
300
303
|
title: 'Languages',
|
|
301
304
|
description: 'All programming languages used across related artifacts',
|
|
302
|
-
filter: :list
|
|
303
|
-
list: true do
|
|
305
|
+
filter: :list do
|
|
304
306
|
collect(outgoing_transitive(:TechnologyArtifact), 'scc/languages')
|
|
305
307
|
end
|
|
306
308
|
|
data/docs/licenses.md
CHANGED
|
@@ -236,8 +236,7 @@ end
|
|
|
236
236
|
# Collect all unique dependency licenses across the portfolio
|
|
237
237
|
computed_annotation 'computed/license_types',
|
|
238
238
|
title: 'All License Types',
|
|
239
|
-
filter: :list
|
|
240
|
-
list: true do
|
|
239
|
+
filter: :list do
|
|
241
240
|
collect(outgoing_transitive(:TechnologyArtifact), 'license/dependencies/licenses')
|
|
242
241
|
end
|
|
243
242
|
|
data/docs/modeling.md
CHANGED
|
@@ -144,9 +144,13 @@ BusinessRequirement
|
|
|
144
144
|
↑ satisfies
|
|
145
145
|
ComplianceEvidence
|
|
146
146
|
↑ evidencedBy
|
|
147
|
-
ApplicationService
|
|
147
|
+
ApplicationService (or TechnologyService / TechnologySystemSoftware)
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
Technology elements such as a Kubernetes cluster runtime can plan, realize and be evidenced
|
|
151
|
+
for requirements directly, so the requirement does not have to be attached to a placeholder
|
|
152
|
+
ApplicationService. Applications deployed on them point to the TechnologyService with `servedBy`.
|
|
153
|
+
|
|
150
154
|
## Annotation Best Practices
|
|
151
155
|
|
|
152
156
|
Use annotations to capture metadata:
|
data/docs/pages.md
CHANGED
|
@@ -146,6 +146,53 @@ link to the resource. Other kinds are not embeddable; an unknown name, another k
|
|
|
146
146
|
a sentence (instead of on its own line) is shown as a marker or a link, and `archsight lint` reports embeds that do not
|
|
147
147
|
resolve. Code blocks and inline code are left alone.
|
|
148
148
|
|
|
149
|
+
### Inline views
|
|
150
|
+
|
|
151
|
+
A view that only this page needs does not have to be a resource of its own: a fenced ```` ```view ```` block holds the
|
|
152
|
+
View as it would be written in a YAML file and is shown like an embedded view:
|
|
153
|
+
|
|
154
|
+
````markdown
|
|
155
|
+
```view
|
|
156
|
+
apiVersion: architecture/v1alpha1
|
|
157
|
+
kind: View
|
|
158
|
+
metadata:
|
|
159
|
+
name: Services without backup # the title above the list, optional
|
|
160
|
+
annotations:
|
|
161
|
+
view/query: 'ApplicationService: backup/mode == "none"'
|
|
162
|
+
view/fields: name, @owner
|
|
163
|
+
view/sort: -name
|
|
164
|
+
view/type: list:name
|
|
165
|
+
```
|
|
166
|
+
````
|
|
167
|
+
|
|
168
|
+
Only `view/query` is required; `kind`, if given, must be `View` and `apiVersion` can be left out. Other annotations are
|
|
169
|
+
ignored, so a View's YAML can be pasted in unchanged (to turn the block into a real view later, or the other way round).
|
|
170
|
+
It loads and runs in the browser like any embed; the server only writes `<div class="view-embed" data-title data-query
|
|
171
|
+
data-fields data-sort data-type>` around the source. A block that is not a valid View (broken YAML, missing or
|
|
172
|
+
unparsable query, unknown `view/*` key or type) shows an error box with the source, and `archsight lint` reports it.
|
|
173
|
+
|
|
174
|
+
### Business requirements of a selection of resources
|
|
175
|
+
|
|
176
|
+
The "Business Requirements" table of an instance page (the requirements it `realizes`, `partiallyRealizes` or `plans`,
|
|
177
|
+
with status, priority and story) can be put on a page for any selection of resources with a ```` ```requirements ```` block:
|
|
178
|
+
|
|
179
|
+
````markdown
|
|
180
|
+
```requirements
|
|
181
|
+
title: Requirements of the backup services # optional, default "Business Requirements"
|
|
182
|
+
of: 'ApplicationService: name =~ "Backup"' # required: query selecting the resources
|
|
183
|
+
priority: must # optional: must, should, may (one value or a list)
|
|
184
|
+
status: [implemented, partial] # optional: implemented, partial, planned
|
|
185
|
+
```
|
|
186
|
+
````
|
|
187
|
+
|
|
188
|
+
The table has one row per requirement of all selected resources. Its status is the best one any of them gives it
|
|
189
|
+
(`realizes` = implemented, then `partially`, then `plans` = planned), and a "Realized by" column names the resources with their own
|
|
190
|
+
status (left out when only one resource is selected). Rows are sorted by priority, then name; `priority` and `status` filter the rows.
|
|
191
|
+
Like views, the block is only a placeholder on the server (`<div class="requirements-embed" data-title data-of data-priority
|
|
192
|
+
data-status>` around the source): the frontend loads the rows from `GET /api/v1/requirements?of=&priority=&status=`, so rendering a page
|
|
193
|
+
never runs the query. A block with a missing or unparsable `of`, an unknown key, priority or status shows an error box with the
|
|
194
|
+
source, and `archsight lint` reports it.
|
|
195
|
+
|
|
149
196
|
## Macros
|
|
150
197
|
|
|
151
198
|
Inline macros are written `{name:arguments}` and named like the macros of Confluence. They work inside a sentence, a
|
|
@@ -202,6 +249,8 @@ If several pages qualify, a page named `home` wins over one that is only titled
|
|
|
202
249
|
- Tables, code blocks and other GitHub-flavoured markdown.
|
|
203
250
|
- Diagrams: fenced ```` ```asd ```` blocks (see [Diagrams](/doc/diagram)) replace draw.io drawings.
|
|
204
251
|
- Links: `[[Page title]]`, `[[page-name]]`, `[[Name|label]]` and `[[Kind/Name]]` link to pages and resources.
|
|
252
|
+
A link without a label shows the title of a page and the name of a resource (`[[ApplicationComponent/KubeVirt]]` shows "KubeVirt"); hovering it shows the
|
|
253
|
+
kind and the first line of the description (the status of a page). The export to Confluence writes the same texts, without the hover.
|
|
205
254
|
|
|
206
255
|
## Editing
|
|
207
256
|
|
|
@@ -224,6 +273,7 @@ or `.../display/KEY/Title`). Without `PAGE` every page that has a `confluence:`
|
|
|
224
273
|
|
|
225
274
|
```bash
|
|
226
275
|
archsight export --to confluence -r resources # all linked pages
|
|
276
|
+
archsight export --to confluence -r resources --tag public # only pages tagged `public` (repeat or list: any match)
|
|
227
277
|
archsight export --to confluence handbook-home --dry-run # show what would happen
|
|
228
278
|
archsight export --to confluence handbook-home --force # overwrite changes made in Confluence
|
|
229
279
|
```
|
|
@@ -257,10 +307,17 @@ confluence:
|
|
|
257
307
|
With `drawio` off nothing draw.io-specific is written, so any Confluence shows the diagrams. Rendering a `.drawio` needs the
|
|
258
308
|
draw.io desktop CLI (`drawio`, or `ARCHSIGHT_DRAWIO_CLI`) on the machine that exports; the preview of an SVG needs
|
|
259
309
|
`rsvg-convert` or that CLI. A diagram that cannot be rendered fails the page instead of leaving a blank diagram. Attachments an
|
|
260
|
-
earlier export added and the page no longer uses are removed; attachments added by others are left alone. `[[links]]` to pages that have a Confluence link
|
|
261
|
-
become links to them, other links are plain text
|
|
262
|
-
is
|
|
263
|
-
|
|
310
|
+
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
|
|
311
|
+
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
|
|
312
|
+
```` ```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.
|
|
313
|
+
|
|
314
|
+
**Views and requirements** are live in Archsight, so Confluence gets a regular table with the data of the moment of the export (every
|
|
315
|
+
export writes the current data; a page whose tables did not change is `unchanged`): `![[View/Name]]` on a line of its own, a
|
|
316
|
+
```` ```view ```` block and a ```` ```requirements ```` block each become a table with the title and item count above it, the columns of
|
|
317
|
+
the view (Name, Kind unless `list:name`, the `view/fields`, sorted by `view/sort`; times are the stored values, not "3 days ago") or
|
|
318
|
+
the requirements table (status and priority as status lozenges: implemented green, partial yellow, planned blue; `must` red,
|
|
319
|
+
`should` yellow, `may` grey), and names of pages that have a Confluence link as links. At most 200 rows are exported, a note says how many
|
|
320
|
+
were left out. A `![[View/..]]` that is not alone on its line or names no view stays a note, and so does `![[Analysis/..]]` (it runs a script).
|
|
264
321
|
|
|
265
322
|
**Links in diagrams**: a node with `resource "Some Page"` links to the Confluence page of that wiki page (the page's own
|
|
266
323
|
`confluence:` link) instead of its Archsight address, which means nothing in Confluence. A reference to a page without a
|
|
@@ -276,8 +333,8 @@ changed it and when, and the command exits with 1. `--force` overwrites it anywa
|
|
|
276
333
|
were replaced. A page whose generated content is unchanged is left alone (no new version).
|
|
277
334
|
|
|
278
335
|
**Locking and revisions**: unless `--no-lock`, editing is restricted to the exporting user (if the server does not allow it,
|
|
279
|
-
the export still succeeds and says "NOT locked"). Every exported version has the message "Generated by Archsight
|
|
280
|
-
|
|
336
|
+
the export still succeeds and says "NOT locked"). Every exported version has the message "Generated by Archsight `<version>` from
|
|
337
|
+
`<file>`, do not edit in Confluence", and the page starts with a note that it is generated and that the source file is the place
|
|
281
338
|
to edit.
|
|
282
339
|
|
|
283
340
|
## Pages and AI assistants (MCP)
|
data/docs/search.md
CHANGED
|
@@ -263,3 +263,20 @@ Using `in` to simplify OR conditions:
|
|
|
263
263
|
|
|
264
264
|
# Combined with other conditions:
|
|
265
265
|
activity/status == "active" & repository/artifacts in ("container", "chart")
|
|
266
|
+
|
|
267
|
+
## Search results
|
|
268
|
+
|
|
269
|
+
The result page lists the hits with a count per kind. Click a kind to narrow the list; the query stays the same.
|
|
270
|
+
Every hit also shows its **summary attributes**: up to three annotations per kind (normal or computed) that
|
|
271
|
+
the kind definition marks with `summary: true`, for example the activity status of a repository or the priority
|
|
272
|
+
of a requirement. Clicking a tag value searches for that value.
|
|
273
|
+
|
|
274
|
+
The same data is available from the API: `GET /api/v1/search?q=...` returns `by_kind` (hits per kind, for all
|
|
275
|
+
hits of the query) and, for each hit, `highlights`:
|
|
276
|
+
|
|
277
|
+
"highlights": [
|
|
278
|
+
{ "key": "activity/status", "title": "Activity Status", "value": "active", "format": "tag_word", "type": null }
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
Add `kind=TechnologyArtifact` to narrow the hits to one kind (`total` and paging follow, `by_kind` still counts
|
|
282
|
+
all hits).
|
|
@@ -5,7 +5,7 @@ require_relative "person"
|
|
|
5
5
|
|
|
6
6
|
# Annotation represents a single annotation definition with its schema and behavior
|
|
7
7
|
class Archsight::Annotations::Annotation
|
|
8
|
-
attr_reader :key, :description, :filter, :format, :enum, :sidebar, :type, :
|
|
8
|
+
attr_reader :key, :description, :filter, :format, :enum, :sidebar, :type, :summary, :editor
|
|
9
9
|
|
|
10
10
|
def initialize(key, options = {})
|
|
11
11
|
@key = key
|
|
@@ -15,7 +15,7 @@ class Archsight::Annotations::Annotation
|
|
|
15
15
|
@enum = options[:enum]
|
|
16
16
|
@validator = options[:validator]
|
|
17
17
|
@sidebar = options.fetch(:sidebar, true)
|
|
18
|
-
@
|
|
18
|
+
@summary = options.fetch(:summary, false)
|
|
19
19
|
@editor = options.fetch(:editor, true)
|
|
20
20
|
@type = options[:type]
|
|
21
21
|
|
|
@@ -51,8 +51,9 @@ class Archsight::Annotations::Annotation
|
|
|
51
51
|
@filter == :list
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
# Summary annotations are returned with every search hit (at most MAX_SUMMARY per kind)
|
|
55
|
+
def summary?
|
|
56
|
+
@summary == true
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
def has_validation?
|
|
@@ -28,7 +28,7 @@ class Archsight::Annotations::ComputedEvaluator
|
|
|
28
28
|
@instance = instance
|
|
29
29
|
@database = database
|
|
30
30
|
@manager = manager
|
|
31
|
-
@resolver = Archsight::Annotations::ComputedRelationResolver.new(instance, database)
|
|
31
|
+
@resolver = Archsight::Annotations::ComputedRelationResolver.new(instance, database, manager.traversal_cache)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# Access a regular annotation value from the current instance
|
|
@@ -170,8 +170,12 @@ end
|
|
|
170
170
|
# ComputedManager orchestrates the computation of all computed annotations.
|
|
171
171
|
# It handles lazy evaluation, caching, and cycle detection.
|
|
172
172
|
class Archsight::Annotations::ComputedManager
|
|
173
|
+
# Shared by all resolvers of this run, see ComputedRelationResolver::TraversalCache
|
|
174
|
+
attr_reader :traversal_cache
|
|
175
|
+
|
|
173
176
|
def initialize(database)
|
|
174
177
|
@database = database
|
|
178
|
+
@traversal_cache = Archsight::Annotations::ComputedRelationResolver::TraversalCache.new(database)
|
|
175
179
|
@computed_cache = {} # { [instance_object_id, key] => value }
|
|
176
180
|
@computing = Set.new # For cycle detection
|
|
177
181
|
end
|
|
@@ -13,10 +13,88 @@
|
|
|
13
13
|
class Archsight::Annotations::ComputedRelationResolver
|
|
14
14
|
MAX_DEPTH = 10
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# TraversalCache holds what can be shared between all resolvers of one computation run: the unfiltered
|
|
17
|
+
# transitive neighbourhood of an instance (relations are fixed once the database is verified), parsed filter
|
|
18
|
+
# queries, one query evaluator and the short kind names. Filter results are never cached because they may
|
|
19
|
+
# depend on computed annotations that are set while the run progresses.
|
|
20
|
+
class TraversalCache
|
|
21
|
+
def initialize(database)
|
|
22
|
+
@database = database
|
|
23
|
+
@reach = {}
|
|
24
|
+
@queries = {}
|
|
25
|
+
@kind_names = {}.compare_by_identity
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Short kind name of a resource class ("ApplicationComponent")
|
|
29
|
+
def kind_name(klass)
|
|
30
|
+
@kind_names[klass] ||= klass.name.split("::").last
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Parsed query for a selector string
|
|
34
|
+
def query(selector)
|
|
35
|
+
@queries[selector] ||= begin
|
|
36
|
+
require_relative "../query/lexer"
|
|
37
|
+
require_relative "../query/parser"
|
|
38
|
+
Archsight::Query::Parser.new(Archsight::Query::Lexer.new(selector).tokenize).parse
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def evaluator
|
|
43
|
+
@evaluator ||= begin
|
|
44
|
+
require_relative "../query/evaluator"
|
|
45
|
+
Archsight::Query::Evaluator.new(@database)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# All instances reachable from `start` within max_depth hops (direction :outgoing or :incoming),
|
|
50
|
+
# each once, in breadth-first order. `start` itself is included when a cycle leads back to it.
|
|
51
|
+
def reachable(start, direction, max_depth)
|
|
52
|
+
by_instance = (@reach[[direction, max_depth]] ||= {}.compare_by_identity)
|
|
53
|
+
by_instance[start] ||= walk(start, direction, max_depth)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def walk(start, direction, max_depth)
|
|
59
|
+
results = []
|
|
60
|
+
listed = {}.compare_by_identity
|
|
61
|
+
expanded = { start => true }.compare_by_identity
|
|
62
|
+
frontier = [start]
|
|
63
|
+
depth = 0
|
|
64
|
+
while depth < max_depth && !frontier.empty?
|
|
65
|
+
following = []
|
|
66
|
+
frontier.each do |node|
|
|
67
|
+
neighbours(node, direction).each do |other|
|
|
68
|
+
unless listed.key?(other)
|
|
69
|
+
listed[other] = true
|
|
70
|
+
results << other
|
|
71
|
+
end
|
|
72
|
+
next if expanded.key?(other)
|
|
73
|
+
|
|
74
|
+
expanded[other] = true
|
|
75
|
+
following << other
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
frontier = following
|
|
79
|
+
depth += 1
|
|
80
|
+
end
|
|
81
|
+
results
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def neighbours(inst, direction)
|
|
85
|
+
if direction == :outgoing
|
|
86
|
+
inst.class.relations.flat_map { |verb, kind_name, _klass_name| inst.relations(verb, kind_name) }
|
|
87
|
+
else
|
|
88
|
+
(inst.references || []).filter_map { |ref| ref.is_a?(Hash) ? ref[:instance] : ref }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @param cache [TraversalCache, nil] shared per computation run; a private one is created when omitted
|
|
94
|
+
def initialize(instance, database, cache = nil)
|
|
17
95
|
@instance = instance
|
|
18
96
|
@database = database
|
|
19
|
-
@
|
|
97
|
+
@cache = cache || TraversalCache.new(database)
|
|
20
98
|
end
|
|
21
99
|
|
|
22
100
|
# Get direct outgoing relations (-> Kind)
|
|
@@ -41,11 +119,7 @@ class Archsight::Annotations::ComputedRelationResolver
|
|
|
41
119
|
# @param max_depth [Integer] Maximum traversal depth (default 10)
|
|
42
120
|
# @return [Array] Array of transitively related instances
|
|
43
121
|
def outgoing_transitive(filter = nil, max_depth: MAX_DEPTH)
|
|
44
|
-
|
|
45
|
-
results = []
|
|
46
|
-
|
|
47
|
-
collect_transitive_outgoing(@instance, filter, visited, 0, max_depth, results)
|
|
48
|
-
results.uniq
|
|
122
|
+
filtered(@cache.reachable(@instance, :outgoing, max_depth), filter)
|
|
49
123
|
end
|
|
50
124
|
|
|
51
125
|
# Get direct incoming relations (<- Kind)
|
|
@@ -70,15 +144,23 @@ class Archsight::Annotations::ComputedRelationResolver
|
|
|
70
144
|
# @param max_depth [Integer] Maximum traversal depth (default 10)
|
|
71
145
|
# @return [Array] Array of instances that transitively reference this one
|
|
72
146
|
def incoming_transitive(filter = nil, max_depth: MAX_DEPTH)
|
|
73
|
-
|
|
74
|
-
results = []
|
|
75
|
-
|
|
76
|
-
collect_transitive_incoming(@instance, filter, visited, 0, max_depth, results)
|
|
77
|
-
results.uniq
|
|
147
|
+
filtered(@cache.reachable(@instance, :incoming, max_depth), filter)
|
|
78
148
|
end
|
|
79
149
|
|
|
80
150
|
private
|
|
81
151
|
|
|
152
|
+
def filtered(instances, filter)
|
|
153
|
+
return instances.dup if filter.nil?
|
|
154
|
+
|
|
155
|
+
if filter.is_a?(Symbol)
|
|
156
|
+
kind = filter.to_s
|
|
157
|
+
instances.select { |inst| @cache.kind_name(inst.class) == kind }
|
|
158
|
+
else
|
|
159
|
+
query_node = @cache.query(filter)
|
|
160
|
+
instances.select { |inst| @cache.evaluator.matches?(query_node, inst) }
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
82
164
|
# Check if an instance matches the given filter
|
|
83
165
|
# @param instance [Object] The instance to check
|
|
84
166
|
# @param filter [Symbol, String, nil] Kind filter or query selector
|
|
@@ -86,75 +168,10 @@ class Archsight::Annotations::ComputedRelationResolver
|
|
|
86
168
|
def matches_filter?(instance, filter)
|
|
87
169
|
return true if filter.nil?
|
|
88
170
|
|
|
89
|
-
instance_kind = instance.class.name.split("::").last
|
|
90
|
-
|
|
91
171
|
if filter.is_a?(Symbol)
|
|
92
|
-
|
|
93
|
-
instance_kind == filter.to_s
|
|
172
|
+
@cache.kind_name(instance.class) == filter.to_s
|
|
94
173
|
else
|
|
95
|
-
|
|
96
|
-
query_node = parse_query(filter)
|
|
97
|
-
evaluator.matches?(query_node, instance)
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Parse a query string (with caching)
|
|
102
|
-
def parse_query(query_string)
|
|
103
|
-
@query_cache[query_string] ||= begin
|
|
104
|
-
require_relative "../query/lexer"
|
|
105
|
-
require_relative "../query/parser"
|
|
106
|
-
tokens = Archsight::Query::Lexer.new(query_string).tokenize
|
|
107
|
-
Archsight::Query::Parser.new(tokens).parse
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Get or create the query evaluator
|
|
112
|
-
def evaluator
|
|
113
|
-
@evaluator ||= begin
|
|
114
|
-
require_relative "../query/evaluator"
|
|
115
|
-
Archsight::Query::Evaluator.new(@database)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Recursively collect transitive outgoing relations
|
|
120
|
-
def collect_transitive_outgoing(inst, filter, visited, depth, max_depth, results)
|
|
121
|
-
return if depth >= max_depth
|
|
122
|
-
|
|
123
|
-
key = "#{inst.class}/#{inst.name}"
|
|
124
|
-
return if visited.include?(key)
|
|
125
|
-
|
|
126
|
-
visited.add(key)
|
|
127
|
-
|
|
128
|
-
inst.class.relations.each do |verb, kind_name, _klass_name|
|
|
129
|
-
rels = inst.relations(verb, kind_name)
|
|
130
|
-
rels.each do |rel|
|
|
131
|
-
# Add to results if matches filter (or no filter)
|
|
132
|
-
results << rel if matches_filter?(rel, filter)
|
|
133
|
-
|
|
134
|
-
# Continue traversal (regardless of whether this matched)
|
|
135
|
-
collect_transitive_outgoing(rel, filter, visited.dup, depth + 1, max_depth, results)
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# Recursively collect transitive incoming relations
|
|
141
|
-
def collect_transitive_incoming(inst, filter, visited, depth, max_depth, results)
|
|
142
|
-
return if depth >= max_depth
|
|
143
|
-
|
|
144
|
-
key = "#{inst.class}/#{inst.name}"
|
|
145
|
-
return if visited.include?(key)
|
|
146
|
-
|
|
147
|
-
visited.add(key)
|
|
148
|
-
|
|
149
|
-
refs = inst.references || []
|
|
150
|
-
# Extract instances from reference hashes
|
|
151
|
-
instances = refs.map { |ref| ref.is_a?(Hash) ? ref[:instance] : ref }.compact
|
|
152
|
-
instances.each do |ref|
|
|
153
|
-
# Add to results if matches filter (or no filter)
|
|
154
|
-
results << ref if matches_filter?(ref, filter)
|
|
155
|
-
|
|
156
|
-
# Continue traversal (regardless of whether this matched)
|
|
157
|
-
collect_transitive_incoming(ref, filter, visited.dup, depth + 1, max_depth, results)
|
|
174
|
+
@cache.evaluator.matches?(@cache.query(filter), instance)
|
|
158
175
|
end
|
|
159
176
|
end
|
|
160
177
|
end
|
data/lib/archsight/cli.rb
CHANGED
|
@@ -282,7 +282,7 @@ module Archsight
|
|
|
282
282
|
desc "export [PAGE...]", "Export wiki pages to another system"
|
|
283
283
|
long_desc <<~DESC
|
|
284
284
|
Publishes wiki pages to the system named by --to. With no PAGE, every page that links to a target
|
|
285
|
-
page (for confluence: `confluence: <page URL>` in its frontmatter) is exported.
|
|
285
|
+
page (for confluence: `confluence: <page URL>` in its frontmatter) is exported. --tag TAG limits the export to pages carrying one of the given tags.
|
|
286
286
|
|
|
287
287
|
confluence: a page is only overwritten when Confluence still holds what the last export wrote. Pages
|
|
288
288
|
never exported before, or edited in Confluence since, are reported as blocked and not exported until
|
|
@@ -295,6 +295,7 @@ module Archsight
|
|
|
295
295
|
option :force, type: :boolean, default: false, desc: "Overwrite pages that were edited in the target since the last export"
|
|
296
296
|
option :lock, type: :boolean, default: true, desc: "Restrict editing of exported pages to the exporting user (--no-lock to skip)"
|
|
297
297
|
option :dry_run, type: :boolean, default: false, desc: "Show what would be exported without writing anything"
|
|
298
|
+
option :tag, type: :array, default: [], desc: "Only export pages with at least one of these tags (repeatable, case-insensitive)"
|
|
298
299
|
option :config, type: :string, desc: "Configuration file (default: ARCHSIGHT_CONFIG or ~/.config/archsight/archsight.yaml)"
|
|
299
300
|
option :drawio, type: :boolean, desc: "The target has the draw.io app: export diagrams as draw.io macros, else as images (default: `confluence.drawio` of the configuration)"
|
|
300
301
|
def export(*names)
|
|
@@ -317,7 +318,7 @@ module Archsight
|
|
|
317
318
|
end
|
|
318
319
|
exporter = Archsight::Export.exporter_for(options[:to]).new(
|
|
319
320
|
database: db, resources_dir: Archsight.resources_dir, force: options[:force], lock: options[:lock],
|
|
320
|
-
dry_run: options[:dry_run], settings: settings, drawio: options[:drawio]
|
|
321
|
+
dry_run: options[:dry_run], settings: settings, drawio: options[:drawio], tags: options[:tag]
|
|
321
322
|
)
|
|
322
323
|
results = exporter.run(names)
|
|
323
324
|
print_export_results(results)
|
data/lib/archsight/database.rb
CHANGED
|
@@ -119,7 +119,8 @@ module Archsight
|
|
|
119
119
|
kind = obj["kind"] || raise("kind not defined")
|
|
120
120
|
klass = Archsight::Resources[kind] || raise("#{kind} is not a valid kind")
|
|
121
121
|
inst = klass.new(obj, @current_ref)
|
|
122
|
-
|
|
122
|
+
raise("metadata name of #{kind} not present") if inst.name.to_s.strip.empty?
|
|
123
|
+
|
|
123
124
|
inst
|
|
124
125
|
end
|
|
125
126
|
|
|
@@ -142,7 +142,8 @@ module Archsight
|
|
|
142
142
|
rows = ["| Annotation | Description | Values |", "|------------|-------------|--------|"]
|
|
143
143
|
annotations.each do |a|
|
|
144
144
|
values = format_values(a)
|
|
145
|
-
|
|
145
|
+
description = a.summary? ? "#{a.description || "-"} _(summary: shown with search hits)_" : (a.description || "-")
|
|
146
|
+
rows << "| `#{a.key}` | #{description} | #{values} |"
|
|
146
147
|
end
|
|
147
148
|
rows.join("\n")
|
|
148
149
|
end
|
|
@@ -31,8 +31,9 @@ module Archsight
|
|
|
31
31
|
# @param database [Archsight::Database]
|
|
32
32
|
# @param settings [Credentials::Settings, nil] token and draw.io support; default: Credentials.load
|
|
33
33
|
# @param drawio [Boolean, nil] overrides the settings' draw.io flag
|
|
34
|
+
# @param tags [Array<String>] only export pages that have at least one of these `page/tags` (case-insensitive)
|
|
34
35
|
# @param client_factory [#call, nil] `(base_url, token) -> Client`, for tests
|
|
35
|
-
def initialize(database:, resources_dir:, force: false, lock: true, dry_run: false, settings: nil, drawio: nil, client_factory: nil)
|
|
36
|
+
def initialize(database:, resources_dir:, force: false, lock: true, dry_run: false, settings: nil, drawio: nil, tags: [], client_factory: nil)
|
|
36
37
|
@database = database
|
|
37
38
|
@resources_dir = resources_dir
|
|
38
39
|
@force = force
|
|
@@ -40,6 +41,7 @@ module Archsight
|
|
|
40
41
|
@dry_run = dry_run
|
|
41
42
|
@settings = settings
|
|
42
43
|
@drawio = drawio
|
|
44
|
+
@tags = Array(tags).map { |t| t.to_s.strip.downcase }.reject(&:empty?)
|
|
43
45
|
@client_factory = client_factory || ->(base, secret) { Client.new(base: base, token: secret) }
|
|
44
46
|
@clients = {}
|
|
45
47
|
end
|
|
@@ -55,17 +57,24 @@ module Archsight
|
|
|
55
57
|
# @return [Array<Array(Page|String, Result|nil)>]
|
|
56
58
|
def select(names)
|
|
57
59
|
all = @database.instances_by_kind("Page")
|
|
58
|
-
return all.values.select { |p| link(p) }.sort_by(&:name).map { |p| [p, nil] } if names.empty?
|
|
60
|
+
return all.values.select { |p| link(p) && tagged?(p) }.sort_by(&:name).map { |p| [p, nil] } if names.empty?
|
|
59
61
|
|
|
60
62
|
names.map do |name|
|
|
61
63
|
page = all[name]
|
|
62
64
|
if page.nil? then [name, Result.new(page: name, status: :failed, message: "no such page")]
|
|
63
65
|
elsif link(page).nil? then [page, Result.new(page: name, status: :skipped, message: "no `confluence:` link in the frontmatter")]
|
|
66
|
+
elsif !tagged?(page) then [page, Result.new(page: name, status: :skipped, message: "has none of the tags #{@tags.join(", ")}")]
|
|
64
67
|
else [page, nil]
|
|
65
68
|
end
|
|
66
69
|
end
|
|
67
70
|
end
|
|
68
71
|
|
|
72
|
+
def tagged?(page)
|
|
73
|
+
return true if @tags.empty?
|
|
74
|
+
|
|
75
|
+
page.annotations["page/tags"].to_s.split(",").map { |t| t.strip.downcase }.intersect?(@tags)
|
|
76
|
+
end
|
|
77
|
+
|
|
69
78
|
def link(page)
|
|
70
79
|
value = page.annotations["page/confluence"].to_s.strip
|
|
71
80
|
value.empty? ? nil : value
|