Metrics Engine¶
Layer: backend
Status — Wave 0 (2026-08-30): catalog, engine and the two endpoints are live. No existing caller has moved yet:
/info,/admin/model-stats,/admin/export/full-model/info, the activity heatmap and/metrics/batchstill compute (or shim) their own numbers until Wave 1; the detail panel and tree chips until Wave 3; EIDOS Dash until Wave 4.
Why an engine¶
Before Wave 0, metrics were computed per feature, on the fly, with divergent
definitions: connections meant one thing in the tree chip and another in the
detail panel; "changes" had two audit folds (chip vs heatmap); sub-items,
descendants and parts were four counters over the same hierarchy; and EIDOS
Dash re-implemented ~25 numbers from the export archive. Users could see
numbers that disagreed, and nobody could look up what a number meant.
The Metrics Engine makes the definition the unit of ownership. A metric exists only if it is in the catalog; the engine computes only what the catalog declares; this page's table is generated from the catalog at build time.
Facts, not scores¶
The engine owns facts: counts and ratios of structural things that can be
verified against disk. Weighted or composite scores (completeness,
readiness, health, maturity) are dashboard policy and stay in EIDOS Dash — but
must be defined purely over catalog ids. Instance-administration figures
(users, snapshots, tier) are not metrics and remain in /admin/model-stats.
Scope, source, merge¶
- scope
node— computed for a list of node ids (compute_node);model— computed for one domain and merged across domains. - source — the only thing a compute function may read:
graph(effective graph via the loader),audit(committed events viaaudit_service, read at most once per window — contract 13),iis(iis.storage). No direct disk or SQL (contract 01.01). - merge — how per-domain values become the model value:
sum,max,mean(weighted by the declaredinputs),dict_sum, ornone(primary only). An unreachable secondary yieldsdomains.<name>: nullandpartial: true; a merged number is never presented as complete when a contribution is missing.
Freshness¶
Model-scope results are memoised per (graph_version, date, days, ids).
graph_version is the mutation store's cross-worker sentinel counter — every
commit bumps it, so the cache dies by itself. There is no separate
invalidation mechanism to keep in sync.
IIS-sourced metrics (interfaces_*) are the one exception: the IIS store has
its own write path outside the graph sentinel, so they are computed fresh on
every request rather than served from the memo cache.
Response shape¶
GET /metrics/model returns {scope, graph_version, computed_at, days, partial,
metrics: {id: {value, unit, inputs?, findings?, domains}}, errors}. inputs
carries denominators so a consumer can show the arithmetic; findings (capped
at 200) lists the offending ids for quality facts. Formula text is in the
catalog, not in the response.
Catalog¶
Relation-based node metrics (connections, type, location, unresolved and the *_label / *_uuid companions) read a node's effective outgoing relations — own plus inherited — from the canonical resolver domain/effective_relations.py, the same engine the detail panel renders from. Inheritance is decided there by the rulebook (relationInheritance, most-specific-wins, tier-gated) or a relation's inherit_source flag; the metric catalog itself never walks ancestors. GET /metrics/batch injects the live rulebook and tier gate into the engine (rulebook=, tier_can=), so a tree chip and the panel header always show the same location and type.
| id | scope | unit | source | merge | definition |
|---|---|---|---|---|---|
subitems |
node | count | graph | — | Direct children of the node (children_of[id]). |
connections |
node | count | graph | — | Effective (own + rulebook-inherited) outgoing internal relations deduped by (predicate, target) plus incoming relations; cross-domain references are excluded (see type/location). |
type |
node | count | graph | — | Effective outgoing cross-domain Type references (% aspect → 'type' domain): own, plus inherited where the rulebook (or the relation's inherit_source flag) says so; deduped on external_ref. |
location |
node | count | graph | — | Effective outgoing Location references (+ aspect): own, plus inherited where the rulebook (or the relation's inherit_source flag) says so — the detail panel's number; deduped on external_ref. |
documents |
node | count | graph | — | Engineering entries whose value matches ^(https?|file):// (case-insensitive). |
specifications |
node | count | graph | — | Engineering entries whose value is NOT such a URL. |
unresolved |
node | count | graph | — | Effective outgoing relations that cannot bind: an internal unresolved_ref that key_derivation.resolve_ref cannot bind, or a cross-domain ref whose domain has no public URL. |
changes |
node | count | audit | — | Committed audit events for the node in the last days days; Create/DeleteRelation are attributed to payload.sourceId. |
location_label |
node | label | graph | — | external_ref of the first effective location, '' if none (BL-FE-129). |
location_uuid |
node | label | graph | — | UUID ('to') of the first effective location node, '' if none (BL-FE-129). |
type_label |
node | label | graph | — | external_ref of the first effective type relation, '' if none (BL-FE-162). |
type_uuid |
node | label | graph | — | UUID ('to') of the first effective type node, '' if none (BL-FE-162). |
descendants |
node | count | graph | — | Size of the recursive subtree below the node, excluding the node itself. |
changes_subtree |
node | count | audit | — | changes summed over the node and every descendant (the activity heatmap number). |
nodes_total |
model | count | graph | sum | Number of effective nodes (base + overlay − tombstoned) in the domain. |
nodes_by_class |
model | dict | graph | dict_sum | Effective nodes grouped by class. |
nodes_by_product_type |
model | dict | graph | dict_sum | Nodes grouped by their productType field (Functional System / Technical System / Component); nodes without one are excluded, never fabricated. |
roots |
model | count | graph | sum | Nodes with no parent. |
tree_edges |
model | count | graph | sum | Sum of direct children over all nodes (= Σ subitems). |
relations_explicit |
model | count | graph | sum | Effective explicit relations (base + overlay − tombstoned). |
relations_inherited |
model | count | graph | sum | Relations materialised by inheritance: each inherit_source relation counts once per descendant of its source node; tombstoned sources excluded. |
relations_by_predicate |
model | dict | graph | dict_sum | relations_total grouped by predicate. |
relations_cross_domain |
model | count | graph | sum | Relations whose target lies in another domain (external_domain set). |
relations_total |
model | count | graph | sum | relations_explicit + relations_inherited. |
overlay_nodes |
model | count | graph | sum | Nodes added by the mutation overlay. |
overlay_relations |
model | count | graph | sum | Relations added by the mutation overlay. |
tombstones_nodes |
model | count | graph | sum | Soft-deleted nodes (node tombstones). |
tombstones_relations |
model | count | graph | sum | Soft-deleted relations (relation tombstones). |
hierarchy_depth_max |
model | count | graph | max | Maximum root-to-node depth (root = 0). |
hierarchy_depth_avg |
model | ratio | graph | mean | Mean root-to-leaf depth over all leaves (nodes without children). |
max_container |
model | id_count | graph | max | Node with the most direct children. |
max_relations_node |
model | id_count | graph | max | Node with the most effective relations (in + out). |
orphan_nodes |
model | id_count | graph | sum | Isolated nodes: no parent, no children and no effective relations. |
unresolved_refs |
model | id_count | graph | sum | Nodes with ≥1 unresolved outgoing relation (node unresolved > 0). |
broken_relations |
model | id_count | graph | sum | Relations whose from is not an effective node, or whose to is set, internal, and not an effective node. |
duplicate_designations |
model | id_count | graph | sum | Designations (external_ref) shared by ≥2 effective nodes in the domain. |
predicate_coverage |
model | dict | graph | mean | {predicate: share of product nodes with ≥1 outgoing relation of that predicate} for every predicate in the relation catalog; a product node is a node whose productType is not null. |
changes_total |
model | count | audit | sum | Committed audit events in the requested days window. |
changes_7d |
model | count | audit | sum | Committed audit events in the last 7 days. |
changes_30d |
model | count | audit | sum | Committed audit events in the last 30 days. |
changes_by_day |
model | dict | audit | dict_sum | Committed audit events per calendar day, last 30 days. |
active_users_30d |
model | count | audit | sum | Distinct actors with ≥1 committed audit event in the last 30 days. |
interfaces_total |
model | count | iis | none | Detected interfaces (IIS store total). |
interfaces_by_status |
model | dict | iis | none | Detected interfaces grouped by status. |
interfaces_by_criticality |
model | dict | iis | none | Detected interfaces grouped by criticality class. |