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.
Files changed (131) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/docs/computed_annotations.md +7 -5
  4. data/docs/licenses.md +1 -2
  5. data/docs/modeling.md +5 -1
  6. data/docs/pages.md +63 -6
  7. data/docs/search.md +17 -0
  8. data/lib/archsight/annotations/annotation.rb +5 -4
  9. data/lib/archsight/annotations/computed.rb +5 -1
  10. data/lib/archsight/annotations/relation_resolver.rb +96 -79
  11. data/lib/archsight/cli.rb +3 -2
  12. data/lib/archsight/database.rb +2 -1
  13. data/lib/archsight/documentation.rb +2 -1
  14. data/lib/archsight/export/confluence/exporter.rb +11 -2
  15. data/lib/archsight/export/confluence/storage.rb +99 -8
  16. data/lib/archsight/export/confluence/tables.rb +78 -0
  17. data/lib/archsight/helpers/fenced_blocks.rb +61 -0
  18. data/lib/archsight/helpers/requirements_blocks.rb +90 -0
  19. data/lib/archsight/helpers/view_blocks.rb +108 -0
  20. data/lib/archsight/helpers/wiki_links.rb +39 -5
  21. data/lib/archsight/helpers.rb +3 -0
  22. data/lib/archsight/import/handlers/go_grapher.rb +4 -1
  23. data/lib/archsight/import/handlers/go_module_parser.rb +4 -1
  24. data/lib/archsight/linter.rb +25 -3
  25. data/lib/archsight/mcp/base.rb +38 -0
  26. data/lib/archsight/requirements.rb +70 -0
  27. data/lib/archsight/resources/analysis.rb +2 -1
  28. data/lib/archsight/resources/application_component.rb +5 -3
  29. data/lib/archsight/resources/application_interface.rb +4 -2
  30. data/lib/archsight/resources/application_service.rb +4 -3
  31. data/lib/archsight/resources/base.rb +27 -12
  32. data/lib/archsight/resources/business_actor.rb +5 -3
  33. data/lib/archsight/resources/business_product.rb +4 -3
  34. data/lib/archsight/resources/business_requirement.rb +4 -3
  35. data/lib/archsight/resources/compliance_evidence.rb +4 -2
  36. data/lib/archsight/resources/data_object.rb +2 -1
  37. data/lib/archsight/resources/import.rb +5 -2
  38. data/lib/archsight/resources/page.rb +4 -2
  39. data/lib/archsight/resources/technology_artifact.rb +4 -3
  40. data/lib/archsight/resources/technology_node.rb +1 -1
  41. data/lib/archsight/resources/technology_service.rb +4 -0
  42. data/lib/archsight/resources/technology_system_software.rb +4 -0
  43. data/lib/archsight/resources/view.rb +2 -1
  44. data/lib/archsight/version.rb +1 -1
  45. data/lib/archsight/view_table.rb +102 -0
  46. data/lib/archsight/web/api/json_helpers.rb +7 -5
  47. data/lib/archsight/web/api/openapi/spec.yaml +135 -2
  48. data/lib/archsight/web/api/requirements_helpers.rb +26 -0
  49. data/lib/archsight/web/api/routes.rb +19 -0
  50. data/lib/archsight/web/application.rb +6 -1
  51. data/lib/archsight/web/public/vue/ApiDocsPage-D-cPRZCT.js +1 -0
  52. data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +1 -0
  53. data/lib/archsight/web/public/vue/{DocPage-uaT8CdFm.js → DocPage-DK6vNDbF.js} +1 -1
  54. data/lib/archsight/web/public/vue/EditorPage-BoJpQaVw.js +35 -0
  55. data/lib/archsight/web/public/vue/EditorPage-CbG4mc9T.css +1 -0
  56. data/lib/archsight/web/public/vue/ErrorPage-Ck9izEUS.css +1 -0
  57. data/lib/archsight/web/public/vue/ErrorPage-PGyjdtEf.js +2 -0
  58. data/lib/archsight/web/public/vue/GraphView-BLiKR4zP.js +1 -0
  59. data/lib/archsight/web/public/vue/GraphView-BvWAbUAl.css +1 -0
  60. data/lib/archsight/web/public/vue/HomePage-C0lR8i2C.js +2 -0
  61. data/lib/archsight/web/public/vue/InstanceRouter-60Tt3ZNM.css +1 -0
  62. data/lib/archsight/web/public/vue/InstanceRouter-D3W2jJHV.js +1 -0
  63. data/lib/archsight/web/public/vue/KindList-BlsaRBNO.js +1 -0
  64. data/lib/archsight/web/public/vue/PageView-9MgHtrgl.js +1 -0
  65. data/lib/archsight/web/public/vue/QueryError-3EqjpbyV.css +1 -0
  66. data/lib/archsight/web/public/vue/QueryError-D1FL1xgA.js +1 -0
  67. data/lib/archsight/web/public/vue/ResourceList-B0FLaFR3.css +1 -0
  68. data/lib/archsight/web/public/vue/ResourceList-vkgFyeOY.js +2 -0
  69. data/lib/archsight/web/public/vue/SearchResults-Cl3O_OEr.js +1 -0
  70. data/lib/archsight/web/public/vue/SearchResults-DiW5XVYW.css +1 -0
  71. data/lib/archsight/web/public/vue/WikiPage-C-8SG67a.css +1 -0
  72. data/lib/archsight/web/public/vue/WikiPage-CeCQBTDS.js +13 -0
  73. data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BfA1TPxQ.js +1 -0
  74. data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-BmgdUKVD.js +1 -0
  75. data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-DbXDvAWF.js +1 -0
  76. data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-BkVhrKS1.js +1 -0
  77. data/lib/archsight/web/public/vue/index-D7m61Ahx.js +3 -0
  78. data/lib/archsight/web/public/vue/index-Dbx3MXWG.css +1 -0
  79. data/lib/archsight/web/public/vue/info-A6RAGUB7-BIlCVuUY.js +1 -0
  80. data/lib/archsight/web/public/vue/mermaid-BjEi5URd.js +3334 -0
  81. data/lib/archsight/web/public/vue/packet-AYTQ26CC-eoQTjY3j.js +1 -0
  82. data/lib/archsight/web/public/vue/pie-WAS4IAKB-DEOotGhh.js +1 -0
  83. data/lib/archsight/web/public/vue/radar-RG4KPBEZ-C2yucuUN.js +1 -0
  84. data/lib/archsight/web/public/vue/railroad-74A4TZTK-CzONK5VY.js +1 -0
  85. data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-9zRPsDsx.js +1 -0
  86. data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-BJ4dkz9l.js +1 -0
  87. data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DFy_rqAj.js +1 -0
  88. data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-2cq5CyD2.js +1 -0
  89. data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DK-e9Ez4.js +1 -0
  90. data/lib/archsight/web/public/vue/{useGraphviz-C71SdG-N.js → useGraphviz-DweKV7Kg.js} +1 -1
  91. data/lib/archsight/web/public/vue/wardley-WFR3VGLG-BQwqUqfB.js +1 -0
  92. data/lib/archsight/web/public/vue.html +3 -3
  93. data/lib/archsight.rb +2 -0
  94. metadata +49 -40
  95. data/lib/archsight/web/public/vue/ApiDocsPage-C0y953v0.css +0 -1
  96. data/lib/archsight/web/public/vue/ApiDocsPage-C_4tAWis.js +0 -1
  97. data/lib/archsight/web/public/vue/EditorPage-C557BJC-.js +0 -35
  98. data/lib/archsight/web/public/vue/EditorPage-df5N-p21.css +0 -1
  99. data/lib/archsight/web/public/vue/ErrorPage-Vdebmife.js +0 -2
  100. data/lib/archsight/web/public/vue/ErrorPage-uMDnfY5_.css +0 -1
  101. data/lib/archsight/web/public/vue/GraphView-BduUql2N.js +0 -1
  102. data/lib/archsight/web/public/vue/GraphView-Cj2V2stN.css +0 -1
  103. data/lib/archsight/web/public/vue/HomePage-BHUTg8Ap.js +0 -2
  104. data/lib/archsight/web/public/vue/InstanceRouter-1lSigA58.js +0 -1
  105. data/lib/archsight/web/public/vue/InstanceRouter-Di7f3Rya.css +0 -1
  106. data/lib/archsight/web/public/vue/KindList-BDZs0j6d.js +0 -1
  107. data/lib/archsight/web/public/vue/PageView-BYzJDwof.js +0 -1
  108. data/lib/archsight/web/public/vue/ResourceList-CnbhU9wm.js +0 -1
  109. data/lib/archsight/web/public/vue/ResourceList-xyBwu7fh.css +0 -1
  110. data/lib/archsight/web/public/vue/SearchResults-CNhf6VOx.js +0 -1
  111. data/lib/archsight/web/public/vue/SearchResults-DOHzqAy3.css +0 -1
  112. data/lib/archsight/web/public/vue/WikiPage-DbmWkM7W.js +0 -13
  113. data/lib/archsight/web/public/vue/WikiPage-GV67QmNJ.css +0 -1
  114. data/lib/archsight/web/public/vue/architecture-TIHT7OUA-Bf-MWFmn.js +0 -1
  115. data/lib/archsight/web/public/vue/cynefin-VYW2F7L2-CkKt8qqs.js +0 -1
  116. data/lib/archsight/web/public/vue/eventmodeling-45OFAUF4-NwSTYPHj.js +0 -1
  117. data/lib/archsight/web/public/vue/gitGraph-TEB2WS4Q-DLRegrRG.js +0 -1
  118. data/lib/archsight/web/public/vue/index-CyVWObLU.js +0 -3
  119. data/lib/archsight/web/public/vue/index-DtKeHT3S.css +0 -1
  120. data/lib/archsight/web/public/vue/info-DKCQHKI2-EV39NzoN.js +0 -1
  121. data/lib/archsight/web/public/vue/mermaid-BMkGfnhm.js +0 -3279
  122. data/lib/archsight/web/public/vue/packet-7NZHBO7P-USljV3MZ.js +0 -1
  123. data/lib/archsight/web/public/vue/pie-RZYD4A2V-DChciBW2.js +0 -1
  124. data/lib/archsight/web/public/vue/radar-I7S5WNFK-CgGJdW1t.js +0 -1
  125. data/lib/archsight/web/public/vue/railroad-3IZDKUUU-Dy43vF5c.js +0 -1
  126. data/lib/archsight/web/public/vue/railroad-abnf-AHOZXSZD-BhsTH-sE.js +0 -1
  127. data/lib/archsight/web/public/vue/railroad-ebnf-EBAXGLYW-BBTd_u0u.js +0 -1
  128. data/lib/archsight/web/public/vue/railroad-peg-LSFZ7HO6-BiNTEzf0.js +0 -1
  129. data/lib/archsight/web/public/vue/treeView-QDETBFTQ-BfnCcf-q.js +0 -1
  130. data/lib/archsight/web/public/vue/treemap-6X3UGDF4-BsAORhvk.js +0 -1
  131. 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: 47bed92ae99f141a261c0dc19a47095a566bd60b9c6a9201dbcd2f6298e1a35f
4
- data.tar.gz: 5ff9e1d7d7f02162192a95d86a3a89194b1e450006f4766191da9206f7de55bf
3
+ metadata.gz: 336946fbac9548285ad368125db862c2f1926d99f2f40c1d40ba4155ebe8ca45
4
+ data.tar.gz: 63ff3e1a973432f98b5ffb66ac9c518a204c0f87d90ee495615cf60bba293cd3
5
5
  SHA512:
6
- metadata.gz: dd5b2c1db012e147af1f123c530184b1c4a6eab3b5e7e1e1dbacb658c6e70080b461975d659d68faeca43118989d52d63d134a91fee79767966f06797e352b3c
7
- data.tar.gz: f50137692088c6f3a24e799eb7433206e5124c5d83805b94e4d1ea1992537b71528f742538fe6362f4c56c547ffa88cca3d5719bfa153fb09181b893046f4de5
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 (`![](../img/a.png)`, `![](../../fop/flow.drawio)`); 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
- | `list` | Boolean | false | Whether values are lists |
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; embedded views and analyses (`![[View/..]]`) become a note, their content
262
- is live and exists in Archsight only. A page with a broken image or a diagram that does not render is not exported and
263
- reported as failed. The Confluence title is kept.
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 <version> from
280
- <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
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, :list, :editor
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
- @list = options.fetch(:list, false)
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
- def list_display?
55
- @list == true
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
- def initialize(instance, database)
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
- @query_cache = {}
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
- visited = Set.new
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
- visited = Set.new
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
- # Simple kind check
93
- instance_kind == filter.to_s
172
+ @cache.kind_name(instance.class) == filter.to_s
94
173
  else
95
- # Query selector - parse and evaluate
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)
@@ -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
- inst.name || raise("metadata name of #{kind} not present")
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
- rows << "| `#{a.key}` | #{a.description || "-"} | #{values} |"
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