archsight 0.1.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 (122) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/CONTRIBUTING.md +186 -0
  5. data/Dockerfile +39 -0
  6. data/LICENSE.txt +201 -0
  7. data/README.md +170 -0
  8. data/SECURITY.md +27 -0
  9. data/exe/archsight +9 -0
  10. data/lib/archsight/annotations/aggregators.rb +109 -0
  11. data/lib/archsight/annotations/annotation.rb +168 -0
  12. data/lib/archsight/annotations/architecture_annotations.rb +59 -0
  13. data/lib/archsight/annotations/backup_annotations.rb +21 -0
  14. data/lib/archsight/annotations/computed.rb +264 -0
  15. data/lib/archsight/annotations/email_recipient.rb +35 -0
  16. data/lib/archsight/annotations/generated_annotations.rb +17 -0
  17. data/lib/archsight/annotations/git_annotations.rb +21 -0
  18. data/lib/archsight/annotations/relation_resolver.rb +160 -0
  19. data/lib/archsight/cli.rb +120 -0
  20. data/lib/archsight/configuration.rb +36 -0
  21. data/lib/archsight/database.rb +183 -0
  22. data/lib/archsight/documentation.rb +171 -0
  23. data/lib/archsight/graph.rb +113 -0
  24. data/lib/archsight/helpers.rb +210 -0
  25. data/lib/archsight/linter.rb +77 -0
  26. data/lib/archsight/mcp/analyze_resource_tool.rb +222 -0
  27. data/lib/archsight/mcp/base.rb +48 -0
  28. data/lib/archsight/mcp/query_tool.rb +113 -0
  29. data/lib/archsight/mcp/resource_doc_tool.rb +87 -0
  30. data/lib/archsight/mcp.rb +6 -0
  31. data/lib/archsight/query/ast.rb +279 -0
  32. data/lib/archsight/query/errors.rb +39 -0
  33. data/lib/archsight/query/evaluator.rb +707 -0
  34. data/lib/archsight/query/lexer.rb +289 -0
  35. data/lib/archsight/query/parser.rb +506 -0
  36. data/lib/archsight/query.rb +68 -0
  37. data/lib/archsight/renderer.rb +134 -0
  38. data/lib/archsight/resources/application_component.rb +346 -0
  39. data/lib/archsight/resources/application_interface.rb +54 -0
  40. data/lib/archsight/resources/application_service.rb +222 -0
  41. data/lib/archsight/resources/base.rb +300 -0
  42. data/lib/archsight/resources/business_actor.rb +195 -0
  43. data/lib/archsight/resources/business_constraint.rb +32 -0
  44. data/lib/archsight/resources/business_process.rb +37 -0
  45. data/lib/archsight/resources/business_product.rb +206 -0
  46. data/lib/archsight/resources/business_requirement.rb +56 -0
  47. data/lib/archsight/resources/compliance_evidence.rb +42 -0
  48. data/lib/archsight/resources/data_object.rb +49 -0
  49. data/lib/archsight/resources/motivation_goal.rb +37 -0
  50. data/lib/archsight/resources/motivation_outcome.rb +33 -0
  51. data/lib/archsight/resources/motivation_stakeholder.rb +38 -0
  52. data/lib/archsight/resources/strategy_capability.rb +38 -0
  53. data/lib/archsight/resources/technology_artifact.rb +154 -0
  54. data/lib/archsight/resources/technology_interface.rb +34 -0
  55. data/lib/archsight/resources/technology_node.rb +42 -0
  56. data/lib/archsight/resources/technology_service.rb +35 -0
  57. data/lib/archsight/resources/technology_system_software.rb +37 -0
  58. data/lib/archsight/resources/view.rb +51 -0
  59. data/lib/archsight/resources.rb +49 -0
  60. data/lib/archsight/template.rb +49 -0
  61. data/lib/archsight/version.rb +5 -0
  62. data/lib/archsight/web/application.rb +290 -0
  63. data/lib/archsight/web/doc/archimate.md +215 -0
  64. data/lib/archsight/web/doc/computed_annotations.md +316 -0
  65. data/lib/archsight/web/doc/icons.md +303 -0
  66. data/lib/archsight/web/doc/index.md.erb +74 -0
  67. data/lib/archsight/web/doc/modeling.md +200 -0
  68. data/lib/archsight/web/doc/search.md +227 -0
  69. data/lib/archsight/web/doc/togaf.md +255 -0
  70. data/lib/archsight/web/doc/tool.md +90 -0
  71. data/lib/archsight/web/public/css/artifact.css +985 -0
  72. data/lib/archsight/web/public/css/base.css +201 -0
  73. data/lib/archsight/web/public/css/graph.css +106 -0
  74. data/lib/archsight/web/public/css/highlight.min.css +10 -0
  75. data/lib/archsight/web/public/css/iconoir.css +22 -0
  76. data/lib/archsight/web/public/css/instance.css +329 -0
  77. data/lib/archsight/web/public/css/layout.css +421 -0
  78. data/lib/archsight/web/public/css/mermaid-layers.css +188 -0
  79. data/lib/archsight/web/public/css/pico.min.css +4 -0
  80. data/lib/archsight/web/public/favicon.ico +0 -0
  81. data/lib/archsight/web/public/img/archimate.png +0 -0
  82. data/lib/archsight/web/public/img/togaf-high-level.png +0 -0
  83. data/lib/archsight/web/public/js/graph-zoom.js +18 -0
  84. data/lib/archsight/web/public/js/highlight.min.js +3899 -0
  85. data/lib/archsight/web/public/js/htmx.min.js +1 -0
  86. data/lib/archsight/web/public/js/mermaid-init.js +88 -0
  87. data/lib/archsight/web/public/js/mermaid.min.js +2811 -0
  88. data/lib/archsight/web/public/js/sparkline.js +42 -0
  89. data/lib/archsight/web/public/js/svg-pan-zoom.min.js +3 -0
  90. data/lib/archsight/web/public/js/svg-zoom-controls.js +93 -0
  91. data/lib/archsight/web/views/index.haml +12 -0
  92. data/lib/archsight/web/views/partials/artifact/_activity.haml +55 -0
  93. data/lib/archsight/web/views/partials/artifact/_agentic.haml +25 -0
  94. data/lib/archsight/web/views/partials/artifact/_deployment.haml +29 -0
  95. data/lib/archsight/web/views/partials/artifact/_git_info.haml +16 -0
  96. data/lib/archsight/web/views/partials/artifact/_language_stats.haml +53 -0
  97. data/lib/archsight/web/views/partials/artifact/_links.haml +24 -0
  98. data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +26 -0
  99. data/lib/archsight/web/views/partials/artifact/_repositories.haml +55 -0
  100. data/lib/archsight/web/views/partials/artifact/_team.haml +83 -0
  101. data/lib/archsight/web/views/partials/artifact/_workflow.haml +69 -0
  102. data/lib/archsight/web/views/partials/components/_activity.haml +37 -0
  103. data/lib/archsight/web/views/partials/components/_git.haml +17 -0
  104. data/lib/archsight/web/views/partials/components/_jira.haml +18 -0
  105. data/lib/archsight/web/views/partials/components/_languages.haml +29 -0
  106. data/lib/archsight/web/views/partials/components/_owner.haml +15 -0
  107. data/lib/archsight/web/views/partials/components/_repositories.haml +37 -0
  108. data/lib/archsight/web/views/partials/components/_status.haml +23 -0
  109. data/lib/archsight/web/views/partials/instance/_detail.haml +99 -0
  110. data/lib/archsight/web/views/partials/instance/_graph.haml +6 -0
  111. data/lib/archsight/web/views/partials/instance/_list.haml +84 -0
  112. data/lib/archsight/web/views/partials/instance/_relations.haml +43 -0
  113. data/lib/archsight/web/views/partials/instance/_requirements.haml +41 -0
  114. data/lib/archsight/web/views/partials/instance/_view_detail.haml +57 -0
  115. data/lib/archsight/web/views/partials/layout/_content.haml +40 -0
  116. data/lib/archsight/web/views/partials/layout/_error.haml +22 -0
  117. data/lib/archsight/web/views/partials/layout/_head.haml +24 -0
  118. data/lib/archsight/web/views/partials/layout/_navigation.haml +20 -0
  119. data/lib/archsight/web/views/partials/layout/_sidebar.haml +27 -0
  120. data/lib/archsight/web/views/search.haml +53 -0
  121. data/lib/archsight.rb +17 -0
  122. metadata +311 -0
@@ -0,0 +1,69 @@
1
+ - instance = db.instance_by_kind(@kind, @instance)
2
+ - return unless instance
3
+ - platforms = (instance.annotations['workflow/platforms'] || 'none').split(',').map(&:strip)
4
+ - types = (instance.annotations['workflow/types'] || 'none').split(',').map(&:strip)
5
+
6
+ - if platforms != ['none'] || types != ['none']
7
+ - has_generic_test = types.include?('test')
8
+ %tr
9
+ %th{scope: 'row'} CI/CD Platforms
10
+ %td
11
+ .workflow-platforms
12
+ %a.workflow-platform-item{**filter_link_attrs('workflow/platforms', 'github-actions')}
13
+ %input{type: 'checkbox', disabled: true, checked: platforms.include?('github-actions')}
14
+ %label GitHub Actions
15
+ %a.workflow-platform-item{**filter_link_attrs('workflow/platforms', 'gitlab-ci')}
16
+ %input{type: 'checkbox', disabled: true, checked: platforms.include?('gitlab-ci')}
17
+ %label GitLab CI
18
+ %a.workflow-platform-item{**filter_link_attrs('workflow/platforms', 'makefile')}
19
+ %input{type: 'checkbox', disabled: true, checked: platforms.include?('makefile')}
20
+ %label Makefile
21
+ %tr
22
+ %td.info-section-cell{colspan: 2}
23
+ %details{open: true}
24
+ %summary
25
+ %strong Workflow Types
26
+ .workflow-types
27
+ %div
28
+ .workflow-category
29
+ %strong Build & Deploy
30
+ %ul
31
+ %li
32
+ %input{type: 'checkbox', disabled: true, checked: types.include?('build')}
33
+ %a{**filter_link_attrs('workflow/types', 'build')} Build
34
+ %li
35
+ %input{type: 'checkbox', disabled: true, checked: types.include?('deploy')}
36
+ %a{**filter_link_attrs('workflow/types', 'deploy')} Deploy
37
+ .workflow-category
38
+ %strong Testing
39
+ %ul
40
+ %li
41
+ %input{type: 'checkbox', disabled: true, checked: types.include?('test')}
42
+ %a{**filter_link_attrs('workflow/types', 'test')} Test (generic)
43
+ %li{class: has_generic_test ? 'grayed-out' : ''}
44
+ %input{type: 'checkbox', disabled: true, checked: types.include?('unit-test')}
45
+ %a{**filter_link_attrs('workflow/types', 'unit-test')} Unit Tests
46
+ %li{class: has_generic_test ? 'grayed-out' : ''}
47
+ %input{type: 'checkbox', disabled: true, checked: types.include?('integration-test')}
48
+ %a{**filter_link_attrs('workflow/types', 'integration-test')} Integration Tests
49
+ %li{class: has_generic_test ? 'grayed-out' : ''}
50
+ %input{type: 'checkbox', disabled: true, checked: types.include?('smoke-test')}
51
+ %a{**filter_link_attrs('workflow/types', 'smoke-test')} Smoke Tests
52
+ .workflow-category
53
+ %strong Quality & Security
54
+ %ul
55
+ %li
56
+ %input{type: 'checkbox', disabled: true, checked: types.include?('lint')}
57
+ %a{**filter_link_attrs('workflow/types', 'lint')} Linting
58
+ %li
59
+ %input{type: 'checkbox', disabled: true, checked: types.include?('security-scan')}
60
+ %a{**filter_link_attrs('workflow/types', 'security-scan')} Security Scan
61
+ .workflow-category
62
+ %strong Automation
63
+ %ul
64
+ %li
65
+ %input{type: 'checkbox', disabled: true, checked: types.include?('dependency-update')}
66
+ %a{**filter_link_attrs("workflow/types", "dependency-update")} Dependency Updates
67
+ %li
68
+ %input{ type: "checkbox", disabled: true, checked: types.include?("ticket-creation") }
69
+ %a{ **filter_link_attrs("workflow/types", "ticket-creation") } Ticket Creation
@@ -0,0 +1,37 @@
1
+ -# Activity component - commits sparkline + trend (last 12 months)
2
+ - annotations = instance.annotations
3
+ - commits = annotations['activity/commits']
4
+ - status = annotations['activity/status']
5
+ - bus_factor = annotations['activity/busFactor']
6
+ - contributors_6m = annotations['activity/contributors/6m']
7
+ - if commits || status
8
+ - commit_values = commits ? commits.split(',').map(&:to_i).last(12) : []
9
+ - total_commits = commit_values.sum
10
+ -# Calculate trend: compare last 3 months vs previous 3 months
11
+ - recent_3m = commit_values.last(3).sum
12
+ - previous_3m = commit_values[-6, 3]&.sum || 0
13
+ - if previous_3m > 0
14
+ - trend_pct = ((recent_3m - previous_3m).to_f / previous_3m * 100).round
15
+ - else
16
+ - trend_pct = recent_3m > 0 ? 100 : 0
17
+ - trend_class = trend_pct > 10 ? 'positive' : (trend_pct < -10 ? 'negative' : 'neutral')
18
+ - tooltip_parts = []
19
+ - tooltip_parts << "#{total_commits} commits (12m)" if total_commits > 0
20
+ - tooltip_parts << "trend: #{recent_3m} vs #{previous_3m} (3m)"
21
+ - tooltip_parts << "#{contributors_6m} contributors" if contributors_6m
22
+ - tooltip_parts << "status: #{status}" if status
23
+ - tooltip_parts << "bus factor: #{bus_factor}" if bus_factor
24
+ - tooltip = tooltip_parts.join(', ')
25
+ .component-activity{title: tooltip}
26
+ - if commits && !commits.empty?
27
+ .activity-sparkline.sparkline-sm{'data-values' => commit_values.join(',')}
28
+ .component-trend{class: "trend-#{trend_class}"}
29
+ - if trend_pct.positive?
30
+ %i.iconoir-arrow-up
31
+ - elsif trend_pct.negative?
32
+ %i.iconoir-arrow-down
33
+ - else
34
+ %i.iconoir-minus
35
+ %span= "#{trend_pct.abs}%"
36
+ - else
37
+ %span.empty-value —
@@ -0,0 +1,17 @@
1
+ -# Git component - repository link
2
+ - annotations = instance.annotations
3
+ - git_url = annotations['repository/git']
4
+ - visibility = annotations['repository/visibility']
5
+ - if git_url
6
+ - display_name = git_url.sub(%r{^https?://}, '').sub(/\.git$/, '')
7
+ - short_name = display_name.split('/').last(2).join('/')
8
+ - tooltip = git_url
9
+ - tooltip += " (#{visibility})" if visibility
10
+ %a.component-link{href: git_url, target: "_blank", title: tooltip}
11
+ - if git_url.include?("gitlab")
12
+ %i.iconoir-git-fork
13
+ - else
14
+ %i.iconoir-github
15
+ = short_name
16
+ - else
17
+ %span.empty-value —
@@ -0,0 +1,18 @@
1
+ -# Jira component - compact for tables
2
+ - annotations = instance.annotations
3
+ - jira_key = annotations['team/jira']
4
+ - jira_url = annotations['jira/projectUrl']
5
+ - jira_created = annotations['jira/issues/created']
6
+ - jira_resolved = annotations['jira/issues/resolved']
7
+ - if jira_key
8
+ .component-jira
9
+ %a.badge.badge-sm{href: jira_url, target: '_blank'}= jira_key
10
+ - if jira_created && jira_resolved
11
+ - created_avg = jira_created.split(',').map(&:to_i).sum / 6.0
12
+ - resolved_avg = jira_resolved.split(',').map(&:to_i).sum / 6.0
13
+ - backlog_change = (created_avg - resolved_avg).round(1)
14
+ - trend_class = backlog_change < -1 ? 'positive' : (backlog_change > 1 ? 'negative' : 'neutral')
15
+ .component-trend{class: "trend-#{trend_class}", title: "#{created_avg.round(1)} created/mo, #{resolved_avg.round(1)} resolved/mo"}
16
+ = "#{"+" if backlog_change.positive?}#{backlog_change}"
17
+ - else
18
+ %span.empty-value —
@@ -0,0 +1,29 @@
1
+ -# Languages component - bar visualization with tooltip details
2
+ - annotations = instance.annotations
3
+ - languages = annotations['scc/languages']
4
+ - if languages && !languages.empty?
5
+ - lang_data = []
6
+ - languages.split(',').each do |lang|
7
+ - loc_value = annotations["scc/language/#{lang.strip}/loc"]
8
+ - lang_data << { name: lang.strip, loc: loc_value ? loc_value.to_i : 0 }
9
+ - lang_data = lang_data.sort_by { |l| -l[:loc] }
10
+ - total_loc = lang_data.sum { |l| l[:loc] }
11
+ - if total_loc > 0
12
+ -# Build tooltip with language breakdown
13
+ - tooltip_parts = ["#{number_with_delimiter(total_loc)} loc"]
14
+ - lang_data.first(5).each { |l| tooltip_parts << "#{l[:name]}: #{number_with_delimiter(l[:loc])} (#{(l[:loc].to_f / total_loc * 100).round(1)}%)" }
15
+ - tooltip_parts << "..." if lang_data.size > 5
16
+ - tooltip = tooltip_parts.join(", ")
17
+ .component-language{title: tooltip}
18
+ .component-lang-bar
19
+ - lang_data.first(5).each_with_index do |lang, idx|
20
+ - pct = (lang[:loc].to_f / total_loc * 100).round(1)
21
+ - if pct > 0
22
+ .lang-bar-segment{class: "lang-#{idx}", style: "width: #{pct}%"}
23
+ - other_pct = 100 - lang_data.first(5).sum { |l| (l[:loc].to_f / total_loc * 100).round(1) }
24
+ - if other_pct.positive?
25
+ .lang-bar-segment.lang-other{ style: "width: #{other_pct}%" }
26
+ - else
27
+ %span.empty-value —
28
+ - else
29
+ %span.empty-value —
@@ -0,0 +1,15 @@
1
+ -# Owner component - team/owner link
2
+ - annotations = instance.annotations
3
+ - owner = annotations['team/owner']
4
+ - if owner
5
+ - owner_instance = @database.find("BusinessActor", owner)
6
+ .component-owner{title: owner}
7
+ - if owner_instance
8
+ %a.component-link{ href: "/artifact/BusinessActor/#{owner}" }
9
+ %i.iconoir-community
10
+ = owner
11
+ - else
12
+ %i.iconoir-community
13
+ = owner
14
+ - else
15
+ %span.empty-value —
@@ -0,0 +1,37 @@
1
+ -# Repositories component - count + mini bar with all statuses
2
+ - annotations = instance.annotations
3
+ - total = annotations['repository/artifacts/total']&.to_i
4
+ - active = annotations['repository/artifacts/active']&.to_i || 0
5
+ - abandoned = annotations['repository/artifacts/abandoned']&.to_i || 0
6
+ - archived = annotations['repository/artifacts/archived']&.to_i || 0
7
+ - high_bus_factor = annotations['repository/artifacts/highBusFactor']&.to_i || 0
8
+ - if total && total > 0
9
+ - active_healthy = active - high_bus_factor
10
+ - other = total - active - abandoned - archived
11
+ - tooltip_parts = []
12
+ - tooltip_parts << "#{active_healthy} active" if active_healthy > 0
13
+ - tooltip_parts << "#{high_bus_factor} high bus factor" if high_bus_factor > 0
14
+ - tooltip_parts << "#{archived} archived" if archived > 0
15
+ - tooltip_parts << "#{other} other" if other > 0
16
+ - tooltip_parts << "#{abandoned} abandoned" if abandoned > 0
17
+ - tooltip = "#{total} repos: #{tooltip_parts.join(', ')}"
18
+ .component-repos{title: tooltip}
19
+ %span.component-count= total
20
+ .component-bar-mini
21
+ - if active_healthy > 0
22
+ - pct = (active_healthy.to_f / total * 100).round
23
+ .bar-segment.status-active{style: "width: #{pct}%"}
24
+ - if high_bus_factor > 0
25
+ - pct = (high_bus_factor.to_f / total * 100).round
26
+ .bar-segment.status-high-bus-factor{style: "width: #{pct}%"}
27
+ - if archived > 0
28
+ - pct = (archived.to_f / total * 100).round
29
+ .bar-segment.status-archived{style: "width: #{pct}%"}
30
+ - if other > 0
31
+ - pct = (other.to_f / total * 100).round
32
+ .bar-segment.status-other{style: "width: #{pct}%"}
33
+ - if abandoned.positive?
34
+ - pct = (abandoned.to_f / total * 100).round
35
+ .bar-segment.status-abandoned{ style: "width: #{pct}%" }
36
+ - else
37
+ %span.empty-value —
@@ -0,0 +1,23 @@
1
+ -# Status component - status + bus factor
2
+ - annotations = instance.annotations
3
+ - status = annotations['activity/status']
4
+ - bus_factor = annotations['activity/busFactor']
5
+ - contributors_6m = annotations['activity/contributors/6m']
6
+ - if status || bus_factor
7
+ - bus_factor_desc = case bus_factor
8
+ - when 'high' then 'High risk: >75% commits from one contributor'
9
+ - when 'medium' then 'Medium risk: 50-75% commits from one contributor'
10
+ - when 'low' then 'Low risk: No single contributor dominates'
11
+ - else nil
12
+ - tooltip_parts = []
13
+ - tooltip_parts << "Status: #{status}" if status
14
+ - tooltip_parts << bus_factor_desc if bus_factor_desc
15
+ - tooltip_parts << "#{contributors_6m} contributors (6m)" if contributors_6m
16
+ - tooltip = tooltip_parts.join(' | ')
17
+ .component-status{title: tooltip}
18
+ - if status
19
+ %span.component-badge{class: "status-#{status}"}= status
20
+ - if bus_factor && bus_factor != "low"
21
+ %span.component-badge{ class: "bus-factor-#{bus_factor}" }= "bus:#{bus_factor}"
22
+ - else
23
+ %span.empty-value —
@@ -0,0 +1,99 @@
1
+ - instance = db.instance_by_kind(@kind, @instance)
2
+ - unless instance
3
+ %article
4
+ %header
5
+ %h2 Not Found
6
+ %p
7
+ Instance
8
+ %strong= @instance
9
+ of kind
10
+ %strong= @kind
11
+ was not found.
12
+ - else
13
+ %article
14
+ %header
15
+ %h2
16
+ %i{class: "iconoir-#{instance.class.icon} icon-#{instance.class.layer}"}
17
+ .instance-title-text
18
+ %span.instance-name= @instance
19
+ %span.instance-kind-subtitle= @kind
20
+ - if instance.annotations['generated/script']
21
+ .generated-badge
22
+ %span.generated-script
23
+ Generated by
24
+ = instance.annotations['generated/script']
25
+ - if instance.annotations['generated/at']
26
+ %span.generated-time= time_ago(instance.annotations['generated/at'])
27
+ - if instance.has_relations?
28
+ .graph-container
29
+ #graphviz.canvas
30
+ != graphviz_svg(create_graph_one(db, @kind, @instance), 'graphviz')
31
+ - if instance.annotations['architecture/description']
32
+ - git_url = instance.annotations['repository/git']
33
+ - if instance.has_relations?
34
+ %footer
35
+ != markdown(instance.annotations['architecture/description'], git_url: git_url)
36
+ - else
37
+ != markdown(instance.annotations['architecture/description'], git_url: git_url)
38
+ != haml :'partials/instance/_requirements', locals: { instance: instance }
39
+ %article.documentation
40
+ %header
41
+ %h2 Details
42
+ %table
43
+ %thead
44
+ %tr
45
+ %th{scope: 'col'} Name
46
+ %th{scope: 'col'} Value
47
+ %tbody
48
+ != haml :'partials/artifact/_git_info'
49
+ != haml :'partials/artifact/_language_stats'
50
+ != haml :'partials/artifact/_project_estimate'
51
+ != haml :'partials/artifact/_repositories'
52
+ != haml :'partials/artifact/_activity'
53
+ != haml :'partials/artifact/_team'
54
+ != haml :'partials/artifact/_deployment'
55
+ != haml :'partials/artifact/_workflow'
56
+ != haml :'partials/artifact/_agentic'
57
+ != haml :'partials/artifact/_links'
58
+ - instance.annotations.each do |k, v|
59
+ - next if k == 'scc/languages' || k =~ /^scc\/language\/.+\/loc$/
60
+ - next if k =~ /^scc\/estimated(Cost|ScheduleMonths|People)$/
61
+ - next if k =~ /^activity\/(commits|contributors(\/(6m|total))?|status|busFactor|createdAt)$/
62
+ - next if k == 'architecture/description'
63
+ - next if k =~ /^repository\/(artifacts|git|visibility|)$/
64
+ - next if k.start_with?('repository/artifacts/')
65
+ - next if k == 'workflow/platforms'
66
+ - next if k == 'workflow/types'
67
+ - next if k == 'agentic/tools'
68
+ - next if k.start_with?('link/')
69
+ - next if k.start_with?('team/')
70
+ - next if k.start_with?('jira/')
71
+ - next if k.start_with?('generated/')
72
+ - annotation_def = Archsight::Resources[@kind].annotation_matching(k)
73
+ %tr
74
+ %th{scope: 'row', title: annotation_def&.description}= annotation_def&.title || k.split('/').last.capitalize
75
+ %td
76
+ - format = Archsight::Resources[@kind].annotation_format(k)
77
+ - case format
78
+ - when :markdown
79
+ != markdown(v, git_url: git_url)
80
+ - when :tag_list
81
+ .instance-badges
82
+ - v.split(',').map(&:strip).each do |val|
83
+ - badge_class = case k
84
+ - when 'repository/artifacts' then "deployment-#{val}"
85
+ - when 'workflow/platforms' then "workflow-#{val}"
86
+ - when 'workflow/types' then "workflow-#{val}"
87
+ - when 'agentic/tools' then "agentic-#{val}"
88
+ - else ''
89
+ %a.badge.badge-info{class: badge_class, **filter_link_attrs(k, val)}
90
+ = val
91
+ - when :tag_word
92
+ %a.badge.badge-info{**filter_link_attrs(k, v)}
93
+ = v
94
+ - else
95
+ - if v.is_a?(String) && v.start_with?(%r{https?://})
96
+ %a{ href: v, target: "_blank" }= v
97
+ - else
98
+ = v
99
+ != haml :"partials/instance/_relations", locals: { instance: instance }
@@ -0,0 +1,6 @@
1
+ %article
2
+ %header
3
+ %h2 Architecture Overview
4
+ .graph-container.graph-expand
5
+ #graphviz.fullcanvas
6
+ != graphviz_svg(create_graph_all(db), "graphviz")
@@ -0,0 +1,84 @@
1
+ -# Parameters:
2
+ -# instances: Array of resource instances to display
3
+ -# omit_kind: Boolean - hide kind column (default: false)
4
+ -# fields: Array of annotation keys to display as columns (optional, overrides list_annotations)
5
+ - omit_kind ||= false
6
+ - fields ||= nil
7
+ - if instances.nil? || instances.empty?
8
+ %p.empty-state
9
+ %i No resources found
10
+ - else
11
+ - sample_class = instances.first.class
12
+ -# Determine which annotations to show as columns
13
+ - if fields
14
+ -# Use provided fields - convert to annotation-like objects
15
+ -# Always create wrapper with the actual field key to ensure correct value lookups
16
+ -# (pattern annotations like scc/language/*/loc have the pattern as key, not the actual field)
17
+ - field_annotation = Struct.new(:key, :title, :description, :type)
18
+ - list_annotations = fields.map do |field|
19
+ - if field.start_with?('@')
20
+ -# Component field - renders a partial from partials/components/
21
+ - component_name = field[1..-1]
22
+ - field_annotation.new(field, component_name.capitalize, "Component: #{component_name}", :component)
23
+ - else
24
+ - annotation_def = sample_class.annotation_matching(field)
25
+ - segments = field.split('/')
26
+ -# Generate title: for paths like scc/language/C/loc, use "C Loc" (language name + last segment)
27
+ - if segments.length >= 2
28
+ - title = "#{segments[-2]} #{segments[-1]}".split(/(?=[A-Z])/).map(&:capitalize).join(' ')
29
+ - else
30
+ - title = segments.last.split(/(?=[A-Z])/).map(&:capitalize).join(' ')
31
+ - field_annotation.new(field, title, annotation_def&.description, annotation_def&.type)
32
+ - list_annotations.compact!
33
+ - elsif omit_kind
34
+ -# Auto-detect from list: true annotations
35
+ - list_annotations = sample_class.list_annotations
36
+ - if list_annotations.empty?
37
+ - tags_annotation = sample_class.annotation_matching('architecture/tags')
38
+ - list_annotations = [tags_annotation] if tags_annotation
39
+ - else
40
+ - list_annotations = []
41
+ - if list_annotations.any?
42
+ %table.resource-list-table
43
+ %thead
44
+ %tr
45
+ %th.col-name Name
46
+ - unless omit_kind
47
+ %th.col-kind Kind
48
+ - list_annotations.each do |annotation|
49
+ %th.col-annotation{title: annotation.description}= annotation.title
50
+ %tbody
51
+ - instances.each do |instance|
52
+ %tr.resource-list-row
53
+ %td.col-name
54
+ %a.instance-name{href: "/kinds/#{instance.klass}/instances/#{instance.name}"}
55
+ %i{class: "iconoir-#{instance.class.icon} icon-#{instance.class.layer}"}
56
+ = instance.name
57
+ - unless omit_kind
58
+ %td.col-kind
59
+ %span.instance-kind= instance.klass
60
+ - list_annotations.each do |annotation|
61
+ %td.col-annotation
62
+ - if annotation.type == :component
63
+ - component_name = annotation.key[1..-1]
64
+ != haml :"partials/components/_#{component_name}", locals: { instance: instance }
65
+ - else
66
+ - value = instance.annotations[annotation.key] || instance.computed_annotation_value(annotation.key)
67
+ - if value
68
+ - if annotation.type == Time
69
+ - time_value = value.is_a?(Time) ? value : Time.parse(value.to_s)
70
+ %span{title: time_value.strftime("%Y-%m-%d %H:%M:%S")}= time_ago(value)
71
+ - else
72
+ = value
73
+ - else
74
+ %span.empty-value —
75
+ - else
76
+ %ul.search-instance-list
77
+ - instances.each do |instance|
78
+ %li.search-instance-item
79
+ .instance-main
80
+ %a.instance-name{href: "/kinds/#{instance.klass}/instances/#{instance.name}"}
81
+ %i{ class: "iconoir-#{instance.class.icon} icon-#{instance.class.layer}" }
82
+ = instance.name
83
+ - unless omit_kind
84
+ %span.instance-kind= instance.klass
@@ -0,0 +1,43 @@
1
+ - return unless instance.has_relations? || instance.references.any?
2
+ %article.relations-section
3
+ %header
4
+ %h2 Relations
5
+ .relations-grid
6
+ .relations-column.relations-outgoing
7
+ %h3
8
+ Outgoing
9
+ %i.iconoir-arrow-right
10
+ - if instance.has_relations?
11
+ - instance.relations_grouped.each do |verb, kinds|
12
+ - kinds.each do |kind, instances|
13
+ - kind_class = Archsight::Resources[kind]
14
+ .relation-group
15
+ .relation-kind
16
+ %i{class: "iconoir-#{kind_class&.icon || 'cube'} icon-#{kind_class&.layer || 'technology'}"}
17
+ %span= kind
18
+ .relation-verb= verb
19
+ .instance-badges
20
+ - instances.sort_by(&:name).each do |item|
21
+ %a.badge.badge-info{href: "/kinds/#{item.klass}/instances/#{item.name}"}
22
+ = item.name
23
+ - else
24
+ .relations-empty None
25
+ .relations-column.relations-incoming
26
+ %h3
27
+ %i.iconoir-arrow-right
28
+ Incoming
29
+ - if instance.references.any?
30
+ - instance.references_grouped.each do |kind, verbs|
31
+ - kind_class = Archsight::Resources[kind]
32
+ - verbs.each do |verb, instances|
33
+ .relation-group
34
+ .relation-kind
35
+ %i{class: "iconoir-#{kind_class&.icon || 'cube'} icon-#{kind_class&.layer || 'technology'}"}
36
+ %span= kind
37
+ .relation-verb= verb || "references"
38
+ .instance-badges
39
+ - instances.sort_by(&:name).each do |item|
40
+ %a.badge.badge-info{ href: "/kinds/#{item.klass}/instances/#{item.name}" }
41
+ = item.name
42
+ - else
43
+ .relations-empty None
@@ -0,0 +1,41 @@
1
+ - return unless instance
2
+ - realized = instance.relations(:realizes, :businessRequirements).map { |r| [r, 'implemented'] }
3
+ - partial = instance.relations(:partiallyRealizes, :businessRequirements).map { |r| [r, 'partial'] }
4
+ - planned = instance.relations(:plans, :businessRequirements).map { |r| [r, 'planned'] }
5
+ - all_requirements = realized + partial + planned
6
+ - if all_requirements.any?
7
+ %article.requirements-section
8
+ %header
9
+ %h2 Business Requirements
10
+ %table.requirements-table
11
+ %thead
12
+ %tr
13
+ %th
14
+ %th Name
15
+ %th Priority
16
+ %th Story
17
+ %tbody
18
+ - all_requirements.each do |req, status|
19
+ - status_icon = case status
20
+ - when 'implemented' then 'check-circle'
21
+ - when 'partial' then 'half-moon'
22
+ - when 'planned' then 'calendar'
23
+ - else 'circle'
24
+ - status_title = status.capitalize
25
+ %tr
26
+ %td.requirement-status
27
+ %i.requirement-status-icon{class: "iconoir-#{status_icon} status-#{status}", title: status_title}
28
+ %td
29
+ %a{href: "/kinds/BusinessRequirement/instances/#{req.name}"}= req.name
30
+ %td
31
+ - priority = req.annotations["requirement/priority"]
32
+ - if priority
33
+ %a.badge.badge-info{**search_link_attrs("BusinessRequirement: <- \"#{instance.name}\" & requirement/priority == \"#{priority}\"")}= priority
34
+ - else
35
+ %span.view-empty-value -
36
+ %td.requirement-story
37
+ - story = req.annotations["requirement/story"]
38
+ - if story
39
+ != markdown(story)
40
+ - else
41
+ %span.view-empty-value -
@@ -0,0 +1,57 @@
1
+ - view_instance = db.instance_by_kind(@kind, @instance)
2
+ - return unless view_instance
3
+ - view_query = view_instance.annotations['view/query']
4
+ - view_description = view_instance.annotations['architecture/description']
5
+ - view_fields_raw = view_instance.annotations['view/fields'] || ''
6
+ - view_fields = view_fields_raw.split(',').map(&:strip).reject(&:empty?)
7
+ - view_type = view_instance.annotations['view/type'] || 'list:name+kind'
8
+ - show_kind = view_type == 'list:name+kind'
9
+ - view_sort_raw = view_instance.annotations['view/sort'] || ''
10
+ - view_sort_fields = view_sort_raw.split(',').map(&:strip).reject(&:empty?)
11
+
12
+ %article.view-header
13
+ %header
14
+ %h2
15
+ %i{class: "iconoir-#{view_instance.class.icon} icon-#{view_instance.class.layer}"}
16
+ = @instance
17
+ - if view_description
18
+ .view-description
19
+ != markdown(view_description)
20
+ - if view_query
21
+ .view-query-display
22
+ %p.query-item
23
+ %span.label Query:
24
+ %code.query-value= view_query
25
+
26
+ - if view_query
27
+ - begin
28
+ - start_time = Time.now
29
+ - parsed_query = Archsight::Query.parse(view_query)
30
+ - results = parsed_query.filter(db)
31
+ - results = sort_instances(results, view_sort_fields)
32
+ - search_time_ms = ((Time.now - start_time) * 1000).round(2)
33
+ -# Hide kind if view_type is not 'list:name+kind' OR if query filters by a specific kind (redundant)
34
+ - show_kind = show_kind && parsed_query.kind_filter.nil?
35
+
36
+ %article.view-results
37
+ %header
38
+ %h3 Results
39
+ %span.view-result-meta
40
+ = results.size
41
+ = results.size == 1 ? "item" : "items"
42
+ in #{search_time_ms} ms
43
+
44
+ != haml :"partials/instance/_list", locals: { instances: results, omit_kind: !show_kind, fields: view_fields.any? ? view_fields : nil }
45
+
46
+ - rescue Archsight::Query::QueryError => e
47
+ .search-error
48
+ .search-error-header
49
+ %i.iconoir-warning-triangle
50
+ Query Error
51
+ .search-error-message= e.message
52
+ .search-error-query
53
+ Query:
54
+ %code= view_query
55
+ - else
56
+ %p.view-empty-state
57
+ %em No query defined
@@ -0,0 +1,40 @@
1
+ .content
2
+ - if @doc_content
3
+ %article
4
+ != @doc_content
5
+ - elsif @instances
6
+ != haml :search
7
+ - elsif @kind && @instance
8
+ - if !Archsight::Resources[@kind]
9
+ %article
10
+ %header
11
+ %h2 Kind Not Found
12
+ %p
13
+ Kind
14
+ %strong= @kind
15
+ does not exist.
16
+ - elsif @kind == "View"
17
+ != haml :'partials/instance/_view_detail'
18
+ - else
19
+ != haml :'partials/instance/_detail'
20
+ - elsif @kind && !@instance
21
+ - if !Archsight::Resources[@kind]
22
+ %article
23
+ %header
24
+ %h2 Kind Not Found
25
+ %p
26
+ Kind
27
+ %strong= @kind
28
+ does not exist.
29
+ - else
30
+ - kind_snake = @kind.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
31
+ %article
32
+ %header
33
+ %h2
34
+ %i{class: "iconoir-#{Archsight::Resources[@kind].icon} icon-#{Archsight::Resources[@kind].layer}"}
35
+ = @kind
36
+ %a.kind-help{href: "/doc/resources/#{kind_snake}", title: "Documentation for #{@kind}"}
37
+ %i.iconoir-help-circle
38
+ != haml :"partials/instance/_list", locals: { instances: db.instances_by_kind(@kind).values.sort_by(&:name), omit_kind: true }
39
+ - elsif !@kind
40
+ != haml :"partials/instance/_graph"
@@ -0,0 +1,22 @@
1
+ %header.container
2
+ %article.error-header
3
+ %h3
4
+ %i.iconoir-warning-triangle
5
+ Error
6
+ %p.error-message= @error.message
7
+ %p.error-location
8
+ %i.iconoir-page
9
+ = relative_error_path(@error.ref.path)
10
+ %span.error-line-badge Line #{@error.ref.line_no}
11
+ %a.reload-btn{href: "/reload?redirect=#{params["redirect"]}"}
12
+ %i.iconoir-refresh
13
+ Reload
14
+
15
+ %main.container
16
+ .error-code-block
17
+ %pre
18
+ %code
19
+ - error_context_lines(@error.ref.path, @error.ref.line_no).each do |line|
20
+ - line_class = line[:selected] ? "error-line" : "code-line"
21
+ - escaped_content = ERB::Util.html_escape(line[:content])
22
+ != "<span class='#{line_class}'><span class='line-number'>#{line[:line_no].to_s.rjust(4)}</span>#{escaped_content}</span>\n"