Computed annotations are dynamic values calculated from related resources at database load time. Unlike regular annotations that are stored in YAML files, computed annotations are derived from the data of related resources in the resource graph.
Computed annotations allow you to:
- Aggregate data from related resources (sum, count, average, etc.)
- Collect information across the resource tree (unique languages, all tags, etc.)
- Compute derived metrics (risk scores, costs, coverage percentages)
They are computed after all resources are loaded and relations are resolved, making the full resource graph available for computation.
Computed annotations are defined in resource classes using the computed_annotation DSL:
class ApplicationComponent < Base
computed_annotation 'computed/artifact_count',
title: 'Artifact Count',
description: 'Number of related technology artifacts' do
count(outgoing_transitive(:TechnologyArtifact))
end
endComputed annotations support all the same options as regular annotations:
| Option | Type | Default | Description |
|---|---|---|---|
key |
String | required | The annotation key (first argument) |
description |
String | nil | Human-readable description |
title |
String | nil | Display title for UI |
filter |
Symbol | nil | Filter type (:word, :list) |
format |
Symbol | nil | Rendering format (:markdown, :tag_word, :tag_list) |
enum |
Array | nil | Allowed values |
sidebar |
Boolean | false | Show in sidebar |
type |
Class | nil | Type for value coercion (Integer, Float, String) |
summary |
Boolean | false | Return the value with every search hit of this kind (at most 3 per kind) |
&block |
Block | required | The computation logic |
summary: true (also available on regular annotation definitions) marks the value as a summary attribute: the
search API returns it with every hit of the kind (highlights) and the result page shows it next to the name.
A kind can have at most 3 summary annotations; a fourth raises an ArgumentError when the kind is defined.
- Nil values: Not stored (annotation key won't exist)
- Empty arrays: Not stored (annotation key won't exist)
- Arrays: Converted to comma-separated strings for storage
- Other values: Stored as-is (with optional type coercion)
# Count related resources
computed_annotation 'computed/artifact_count',
title: 'Artifact Count',
description: 'Number of related technology artifacts' do
count(outgoing_transitive(:TechnologyArtifact))
end
# Sum numeric values with type coercion
computed_annotation 'computed/total_cost',
title: 'Total Cost',
description: 'Total estimated cost',
type: Integer do
sum(outgoing_transitive(:TechnologyArtifact), 'scc/estimatedCost')
end
# Find primary language (by LOC)
computed_annotation 'computed/primary_language',
title: 'Primary Language',
description: 'Programming language with most lines of code',
filter: :word do
artifacts = outgoing_transitive(:TechnologyArtifact)
loc_by_language = Hash.new(0)
artifacts.each do |artifact|
artifact.annotations.each do |key, value|
if key =~ %r{^scc/language/(.+)/loc$}
loc_by_language[Regexp.last_match(1)] += value.to_i
end
end
end
loc_by_language.max_by { |_, loc| loc }&.first
end
# Collect unique values (stored as comma-separated string)
computed_annotation 'computed/languages',
title: 'Languages',
description: 'All programming languages used',
filter: :list do
collect(outgoing_transitive(:TechnologyArtifact), 'scc/languages')
endInside the computation block, you have access to relation traversal methods:
| Method | Query Equivalent | Description |
|---|---|---|
outgoing(Kind) |
-> Kind |
Direct outgoing relations to specified kind |
outgoing_transitive(Kind) |
~> Kind |
All transitively reachable resources of kind |
incoming(Kind) |
<- Kind |
Direct incoming relations from specified kind |
incoming_transitive(Kind) |
<~ Kind |
All resources that transitively reference this one |
All relation methods accept either:
- Symbol: Simple kind filter (e.g.,
:TechnologyArtifact) - String: Full query selector using the query language (e.g.,
'TechnologyArtifact: activity/status == "active"')
# Direct relations only
outgoing(:TechnologyArtifact) # Resources directly related
# Transitive relations (follows relation chains)
outgoing_transitive(:TechnologyArtifact) # All reachable artifacts
# Incoming relations
incoming(:ApplicationService) # Services that reference this component
# With depth limit
outgoing_transitive(:TechnologyArtifact, max_depth: 5)
# Query selector - filter by annotation value
outgoing_transitive('TechnologyArtifact: activity/status == "active"')
# Query selector - filter by numeric comparison
outgoing('TechnologyArtifact: scc/estimatedCost > 10000')
# Query selector - filter by name pattern
outgoing_transitive('TechnologyArtifact: name =~ "repo-go"')
# Query selector - kind filter only
incoming_transitive('ApplicationService:')The following aggregation functions are available:
| Function | Description | Returns |
|---|---|---|
sum(instances, key) |
Sum of numeric annotation values | Float or nil |
avg(instances, key) |
Average of numeric values | Float or nil |
min(instances, key) |
Minimum numeric value | Float or nil |
max(instances, key) |
Maximum numeric value | Float or nil |
| Function | Description | Returns |
|---|---|---|
count(instances) |
Count of instances | Integer |
count(instances, key) |
Count of non-nil annotation values | Integer |
| Function | Description | Returns |
|---|---|---|
collect(instances, key) |
Unique sorted values | Array |
first(instances, key) |
First non-nil value | Any or nil |
most_common(instances, key) |
Most frequent value (mode) | Any or nil |
# Sum estimated costs
sum(outgoing_transitive(:TechnologyArtifact), 'scc/estimatedCost')
# Count artifacts with specific status
count(outgoing_transitive(:TechnologyArtifact), 'activity/status')
# Average lines of code
avg(outgoing_transitive(:TechnologyArtifact), 'scc/loc')
# Get all unique languages
collect(outgoing_transitive(:TechnologyArtifact), 'scc/languages')
# Find primary language
most_common(outgoing_transitive(:TechnologyArtifact), 'scc/languages')Inside the computation block, you can access:
computed_annotation 'computed/risk_adjusted_cost' do
base_cost = sum(outgoing_transitive(:TechnologyArtifact), 'scc/estimatedCost')
risk_factor = annotation('risk/factor')&.to_f || 1.0
base_cost * risk_factor if base_cost
endcomputed_annotation 'computed/cost_per_artifact' do
total = computed('computed/total_cost')
count = computed('computed/artifact_count')
total / count if total && count && count > 0
end- Database loads all YAML resources
- Relations are verified and resolved
- Computed annotations are calculated for all instances
- Values are cached for performance
Computed annotations that depend on other computed annotations are resolved automatically through lazy evaluation with cycle detection.
Computed annotations are written to the instance's annotation hash, making them queryable like regular annotations:
# Using the query language
db.query('ApplicationComponent: computed/artifact_count > 10')
# Direct access
instance.annotations['computed/artifact_count']
# Via the dedicated method
instance.computed_annotation_value('computed/artifact_count')Use the computed/ prefix for computed annotation keys to distinguish them from stored annotations:
computed_annotation 'computed/total_cost' do
# ...
end- Computed values are cached after first calculation
- Avoid deep transitive traversals when possible (use
max_depth) - Consider the number of related resources when designing aggregations
- Aggregation functions handle nil values gracefully
- Empty result sets return nil for most aggregations
- Circular dependencies are detected and raise an error
class ApplicationComponent < Base
# Count related artifacts
computed_annotation 'computed/artifact_count',
title: 'Artifact Count',
description: 'Number of related technology artifacts' do
count(outgoing_transitive(:TechnologyArtifact))
end
# Sum costs with type coercion
computed_annotation 'computed/total_cost',
title: 'Total Cost',
description: 'Total estimated cost from related artifacts',
type: Integer do
sum(outgoing_transitive(:TechnologyArtifact), 'scc/estimatedCost')
end
# Find primary language by LOC
computed_annotation 'computed/primary_language',
title: 'Primary Language',
description: 'Programming language with most lines of code',
filter: :word do
artifacts = outgoing_transitive(:TechnologyArtifact)
loc_by_language = Hash.new(0)
artifacts.each do |artifact|
artifact.annotations.each do |key, value|
if key =~ %r{^scc/language/(.+)/loc$}
loc_by_language[Regexp.last_match(1)] += value.to_i
end
end
end
loc_by_language.max_by { |_, loc| loc }&.first
end
# Collect all languages (stored as comma-separated string)
computed_annotation 'computed/languages',
title: 'Languages',
description: 'All programming languages used across related artifacts',
filter: :list do
collect(outgoing_transitive(:TechnologyArtifact), 'scc/languages')
end
# Derived computation using other computed values
computed_annotation 'computed/avg_cost_per_artifact',
title: 'Avg Cost per Artifact',
description: 'Average cost per artifact' do
total = computed('computed/total_cost')
count = computed('computed/artifact_count')
(total.to_f / count).round(2) if total && count && count > 0
end
end