Appearance
Chart Engine
Business Context
This feature provides the core engine for generating charts over any measured data segment tracked in the system (energy/water consumption, power, indoor environment quality, outdoor temperature, CO2 emissions), across any supported time granularity, filtered by building or organisational attributes, and expressed in physical units or currency.
It replaces the legacy E-manazer serializers (timeline, comparison, specific-consumption, prediction) with a single, consistent data contract that all other Visualization epics (chart types, overlays, dashboards, reports) build on.
The customer is every E-Manazer user with access to buildings and gauges — facility managers, energy managers, and administrators — who need to inspect, compare, and export energy and related operational data.
Business-Level Definition
Provide one generic, query-time chart-data API capable of returning any measured time series for any selection of gauges or buildings, at any supported granularity, in physical units or currency, so that chart UI, dashboard tiles, and reports can all be built on the same data contract instead of bespoke per-view logic.
Requirements Definition
Chart logic has previously been spread across multiple report code paths, each with its own unit-conversion logic and no currency conversion at all. Consolidating this into one engine removes that duplication, provides a single aggregation layer for all Visualization capabilities, and is a prerequisite for the chart-type, overlay, dashboard, and report features that depend on it.
Users generating charts and reports also need to narrow displayed data by organisational/administrative building attributes, not only by selecting a specific gauge or building:
- who owns the building
- who manages it, directly or through delegated management
- which campus it belongs to
- its UČEH code
- its PENB class
These attributes are not always tied to an organisation record in the system — an owner or manager may be a third party with no such record — so filtering must work without a foreign-key join, matching the stored IČO or text value directly on the building. Without this, narrowing a chart to "every building under manager X" or "every building with PENB class C or worse" means manually walking and selecting individual buildings or gauges, which does not scale to a portfolio of dozens or hundreds of buildings.
Acceptance Criteria
- Given a gauge and a requested granularity, the API returns data aggregated at that level with no client-visible interpolation or resampling.
- Given a segment that is not a resampled physical measurement (baseline, calculated consumption), the API returns it in its own documented shape (anchor value or native monthly points) rather than resampled to the primary series' density.
- Given a building-attribute filter (owner, manager, delegated manager, campus, UČEH, PENB class), returned series are limited to gauges under matching buildings.
- Given a request for a segment with a known missing dependency (cost, price, savings, SEU-based filtering), the API returns an explicit "unavailable — pending [dependency]" response rather than an empty or zero result.
- Unit and currency conversion is handled by one centralized conversion service, not duplicated per segment or chart type.
- Given no attribute filter is set, the chart/report result matches today's behaviour with no attribute filters applied — no additional condition is silently added.
- Given a filter on manager IČO = "44994575", the result contains only gauges of buildings whose stored manager IČO matches this value exactly, after normalizing the input to a zero-padded 8-digit string — no partial matches.
- Given a filter on campus = a specific campus-level building-tree node, the result contains gauges of every building in that node's subtree, not just buildings carrying some flat "campus" value.
- Given a filter on PENB class IN [C, D], the result contains only gauges of buildings whose energy profile PENB class is C or D; buildings without a profile (null class) are excluded.
- Given both an attribute filter (e.g. owner IČO) and an explicit gauge selection, both apply together (AND) — the selected gauge only appears if it also satisfies the attribute filter.
- Given a filter on UČEH code, this is only enabled once the UČEH code is confirmed available through the building API contract used by this feature — it is a blocking dependency, not an implicit assumption.
- Given a selection of five gauges, the response carries five series and a total, so a table built from it shows five rows and one total row.
- Given a selection that contains both a parent gauge and a sub-gauge beneath it, each contributes according to its own
outputMode— the sub-gauge's own setting, not its position under a parent, decides whether it enters the total — and the response states the difference against the plain sum of the rows. - Given a building, a sector or a campus node as the subject of a series, the value comes from the building's system total for that medium — never from a sum that also adds the building's own gauges.
- Given
sortKey=total, series are ordered by their total over the whole requested period, after authorisation and aggregation, so a subject the caller may not see never influences the order. - Given a series with intervals missing, the response carries its coverage and the missing intervals are gaps — never zeros, never interpolated values.
- Given a unit that differs from the stored one, the response carries values already converted by the engine, and a conversion that cannot be performed is returned as unavailable rather than as the unconverted number.
- Given a 15-minute granularity and a period longer than the cap, the response covers the most recent allowed window of that period and says so, rather than rejecting the request or silently returning the whole range.
- Given day, month or year granularity, bucket boundaries follow the Europe/Prague calendar, including the days on which the clock changes.
Loom Link
N/A — not available in the source material.
Technical Context
User Stories / Use Cases
- As a facility manager, I want to see a building's electricity balance (grid consumption, self-generation, export) in one chart, so I understand its full energy picture.
- As an energy manager, I want to compare current consumption against a building's baseline and against its calculated/predicted consumption, so I can evaluate progress since EnMS introduction.
- As an energy manager, I want to overlay multiple years of consumption on a shared calendar axis, so I can spot seasonal patterns.
- As an administrator, I want to generate charts filtered by building ownership, management, delegated management, campus, UČEH code, or PENB rating, so I can analyze portfolios by groupings beyond sector.
- As any chart consumer, I want data returned in my preferred physical unit and currency, so I don't have to convert it manually.
UI/UX Design
N/A — this feature has no user-facing interface. User-facing configuration (selecting chart content, choosing a visual type, composing views) belongs to the Chart Types, Dashboard Tiles & Custom Views feature, which is a layer on top of this engine.
The only frontend-adjacent work in this epic is a mock data layer used to let that feature build against this contract before the backend exists (see API Analysis) — that is infrastructure, not UI/UX design.
Functional Requirements
FR1 — Segment resolution: segments are resolved against gauge/consumption measurement dimensions (type, direction, tariff, source, format) rather than a fixed hardcoded list. Consumption, power, temperature, humidity, and CO2 (via gauge.medium = kvp) are already supported by the existing schema with no changes needed. The format dimension (cumulative vs. delta) is part of this resolution and is already defined on the gauge reading entity.
FR2 — Granularity: year, month, day, hour, 15-minute. The 15-minute level is the stored consumption series; every coarser level is the sum of the same base, served from the summaries the Consumption Aggregation feature maintains. No interpolation, no resampling and no availability-dependent fallback: the engine never splits coarser data to fake a finer granularity, which is what the legacy reports do when remote readings are missing.
- Day, month and year buckets follow the Europe/Prague calendar, so a day with a clock change has 23 or 25 hours and a month is the local month. Hour and 15-minute buckets are the stored intervals themselves.
- No length or series cap on the 15-minute level. The period asked for is the period served; nothing is silently shortened, and no request is refused for being wide. What a realistic portfolio can actually carry is measured against real data rather than guessed at now, and the limits that measurement justifies are added as their own change (see the deferred list).
- Buckets are clipped to the requested period. A period that does not start and end on a bucket boundary returns partial edge buckets built from the quarter-hours inside the period, each carrying its effective
fromandto. Nothing is prorated and nothing outside the period is included, so the same period read here and through the Consumption Aggregation total gives the same figure. - Sums are reproducible: the same period read at two granularities gives the same total, because both come from the same base.
FR3 — Source mode: which reading source stands behind a value is decided by the Consumption Aggregation feature, not here. The engine asks for the effective series — the per-interval winner of the gauge's source priority — or for one named source series, and passes the answer through unchanged. It never re-resolves priority at query time, because a second implementation of that rule is a second set of numbers.
- Default: effective consumption.
- Named sources (manual, remote, invoice, calculated) can be requested side by side, each as its own series, never added together.
- Which stored rows are summed is stated, not inferred from the medium. A consumption row is identified by kind, measured quantity, flow direction, tariff band, source and — for invoice-derived values — the invoice. The engine sums rows of kind
consumptionfor the medium's quantity, over all tariff bands (a tariff is a split of the same consumption, not a separate one), for the direction the subject's own rule defines — never import and export added together because they share a medium. Cost rows are not part of this slice.
FR4 — Electricity balance view: a single call returns grid-import, self-generation, and export as separate series for a building, built from existing gauge .direction and the auto-generated building-total virtual gauge — no new data structures required.
FR5 — Overlays: two non-resampled overlays, always returned separately from the primary series:
- Baseline — a single anchor value (per medium and reference year) from the baseline entity, not repeated into a flat line, available at any granularity.
- Calculated consumption — native monthly points from the calculated-consumption entity, only offered when the requested chart granularity is month or year.
FR6 — Year-over-year comparison: returned as N series (one per year) against a shared calendar category axis (month, or day-of-year), not as series of absolute timestamps — a distinct response mode from the standard timeline.
FR7 — Attribute-based filtering: narrows the set of buildings/gauges a chart or report works with, by building ownership, management, delegated management, campus, UČEH code, or PENB class.
- Combines with (AND), and never replaces, an explicit building/gauge selection.
- Value shapes differ by attribute: campus and PENB class are enumerable/bounded (matched against existing values, similar to today's sector/building/type/gauge filter); owner, manager, delegated-manager IČO, and UČEH code are free-text/code values with no fixed enumeration.
- An unset filter means no restriction on that attribute at all — consistent with how an empty gauge/building selection today means "don't filter on this," not "select nothing."
- IČO filters match an exact, zero-padded 8-digit value after normalizing the input — never a substring or fuzzy match.
- Campus is a value of the building tree's level type, not a standalone field: the filter selects one or more campus-level nodes and includes every building/gauge in their subtree.
- Filters support selecting more than one value at once (e.g. several managers, several campuses) by default, consistent with today's report/graph filter convention.
- UČEH filtering is blocked until the UČEH code is exposed through the building API contract and a frontend schema — today it exists only in the backend domain and database.
FR8 — Unit and currency conversion: the unit is a parameter of the query and the conversion happens in the engine, through the one centralized conversion service. One unit per request, shared by every medium in it: the response carries values, totals and sort values already in that unit, so no consumer converts anything and a chart, its table and any export cannot drift apart.
- The default unit is the user's preferred display unit; the request carries it explicitly so a saved chart reopens in the unit it was saved with.
- Values are computed in decimal arithmetic and converted once, before any sum: convert, then add, then round. The response carries values at a stated number of decimal places, the same for every number in it, and the unit beside them.
- The offered units follow the medium: kWh, MWh and GJ for energy, m³ and litres for water. Where several media are requested together, the offered units are those their shared quantity family allows.
- A conversion that cannot be performed — a volume to energy without a calorific value, or any currency while no pricing source exists — is returned as unavailable with its reason. Returning the unconverted number under the new unit label is the legacy defect this rule exists to prevent.
FR9 — Subject, medium and breakdown: a query states a scope (what may enter the result) and a breakdown (what forms a series). They are separate parameters: a campus as the scope with buildings as the breakdown gives one series per building; the same campus with no breakdown gives one series for the campus. The medium is the second axis of that set: several media may be asked for at once and each one gives its own series per subject. Media are never merged: breakdown=none collapses the subjects, never the media, so three media asked for together come back as three series even when they share a unit. Adding them into one figure is what the total is for, and that is a number beside the chart, not a column in it.
A series is identified by its subject — type (
gauge,building,sector,campusNode,client), id and label — and by its medium, not by a gauge id, so every level is expressible in the same contract.A building, sector or campus value is built from the building's system total for that medium (the auto-generated building-total virtual gauge), never from that total plus the gauges behind it. Sector and campus values are sums of building values, which is what keeps signs, sub-gauges and self-generation correct without the engine re-deriving them.
Every response carries a total alongside the series: the value the selection adds up to, both over the whole period and per bucket. The per-bucket figure is there so that no consumer ever has to add points up itself — whether it shows it, and where, is the consumer's business. What each series contributes to that total is decided by how its subject accounts, not by what it is called:
- A gauge contributes with the sign its
outputModeanddirectiongive it in the building total formula:include+ import adds,include+ export subtracts,subtract+ import subtracts,subtract+ export adds. A gauge whose mode isexcludeorreportOnlycontributes nothing — it is drawn as its own series when it was asked for, because the user wanted to see it, but it never moves the total. - A building, sector or campus series is already a system total, so it contributes as it stands. A gauge that sits inside a selected building contributes nothing of its own, because that building's value already contains it.
- A sub-gauge is not a containment case. It contributes exactly as its own
outputModesays, whether or not its parent gauge is selected:reportOnly(the default for a new sub-gauge — Nezapočítávat) draws it without touching the total,subtract(Odečíst) takes it out of the parent's figure,include(Přičíst) adds it. A sub-gauge set toincludeunder an included parent is therefore counted twice — because that is what the user asked for. The engine never overrides that setting; the meter form defaults it and warns about it (see the decision below). - Overlap is judged within a medium — a gas sub-meter does not overlap an electricity meter.
Each series states its own verdict as
contribution—add,subtractornone— so a consumer can explain the total row instead of guessing why the column does not add up. Every row whose contribution isnoneis named intotal.excludedSubjectswith the reason (excludedByOutputMode,containedInSubject) and, for containment, the subject whose value already carries it. The plain row sum is stated beside the total, so the difference is visible rather than mysterious. A consumer that adds the rows itself would double-count.Decision (28 Sep 2026) — the user's setting wins over automatic containment. The legacy system never deduplicates a sub-meter:
dataInResults(+1 / 0 / −1) is the only rule, andDailyStatusCollectorreturns a gauge exactly as it is. An earlier draft of this contract silently overrode anincludesub-gauge with acontainedInParentGaugeexclusion, which would let a user set Přičíst on a meter and then watch a chart ignore it — a wrong number that renders cleanly. The rules are therefore:- The only per-gauge rule is
outputMode×direction. There is nocontainedInParentGaugereason. - The sub-gauge hierarchy is used by the meter form, not by the engine: a new sub-gauge defaults to
reportOnly, and choosingincludewhile the parent gauge isincludein the same building shows a double-counting warning (feature Založení měřidla, field Započtení do spotřeby objektu). containedInSubjectstays: a building, sector or campus value is a system total with no user setting to respect, so a gauge inside a selected building, or a building reachable twice through a campus, still enters once.- The building system total and this engine share one rule. The backend building-total formula (
building-total-formula.rules.ts,EvaluateBuildingTotalUseCase) must applyoutputModethe same way, so thatscopeType=buildingand the signed sum of the building's gauges always agree (worked case U9). A building total that counts every active gauge regardless ofoutputModeis a defect to fix, not a second semantics to choose between.
- A gauge contributes with the sign its
With several media in one answer, the total is their sum in the shared unit, split per medium beside it. Which is why the media in one request must share a quantity family: energy media (electricity, gas, heat) belong on one axis and add up to a meaningful figure; water is a volume and does not, so asking for it together with energy is refused rather than answered with a number nobody should read. A screen showing both asks twice and renders two charts.
FR10 — Ordering: the order of series is part of the answer, not a client-side afterthought.
sortKeyis the subject label or the series total over the requested period;sortDirectionis ascending or descending; ties break on label, then medium, then subject id, so the same query always returns the same order.- Ordering never touches the points inside a series: a time axis stays chronological regardless of
sortKey. sortValueis returned per series, so a consumer can show what the ranking was based on.- Every series carries a stable
idthat does not change with its label, the period, the granularity or the unit. It is the last tie-break and the key a consumer uses to remember which series a person hid. - Keeping only the first N series is not part of this feature yet: a ranking that returns a subset needs its own answer to what the total then describes, and that question is open. Until then every matching series is returned.
FR11 — Coverage, provisional values and explicit states: the response distinguishes a real zero, a missing value and an unsupported request.
- Coverage is counted in 15-minute intervals, whatever granularity is displayed — the quarter-hours in which every contributing gauge had a value, against the quarter-hours in the period. This is the definition the Consumption Aggregation feature uses for period totals, so a chart and an invoice check describe the same data the same way. Counting buckets instead would let a month holding one quarter-hour report full coverage.
- Coverage is reported per series, per bucket and for the total. A bucket missing part of its base keeps the sum of what is known and says how much that is; a bucket with nothing usable is
null. A displayed value is therefore never silently partial. - Values still awaiting recalculation are marked provisional, so a chart can say its numbers may move.
- Every selected subject and medium keeps its identity in the answer, including when there is nothing to draw. A selected gauge with no readings in the period returns a series of nulls, not an absence; a subject the medium does not apply to, a building with no system total for it, a conversion that could not be performed and a capability that does not exist yet each return their own named state against that subject and medium. A silent omission is indistinguishable from "we have no such thing", which is the one thing a chart must never say by accident.
- An unsupported or not-yet-available combination returns an unavailable marker with its reason, never an empty series and never zeros.
Explicitly deferred / blocked — not functional requirements, but exclusions the API must surface explicitly as "unavailable" rather than a silent empty or zero result:
Ranking to the first N series (Top N) — the capability is wanted, but what the selection total then describes has to be answered first.
Query limits for the 15-minute level — set from measurement against a realistic portfolio, not from a guess, together with the error the engine raises when one is exceeded.
Metric capabilities: "a coarser bucket is the sum of its base" is true of consumption and false of temperature, humidity and power, which need a time-weighted mean, a minimum and a maximum. Adding a metric means stating its aggregation, not inheriting this one.
Re-invoicing across buildings. The legacy system lets a sub-meter hang under a parent gauge in a different building and books the energy in both places — added where it is measured, subtracted where it is billed. A building total here is computed per building over that building's own gauges, so nothing accounts for that transfer, and this engine does not invent it: such a sub-gauge is charted and totalled as its own consumption. Whether the transfer is modelled at all is a question for the Consumption & Cost Calculation Core, not for a chart.
Real/predicted expenses and unit-price curves — no pricing data source exists yet; depends on the external market data and cost/tariff optimisation epics.
Cost-by-commodity-component breakdown — depends on the cost/tariff optimisation epic's price-decision output.
Savings charts — data source not yet confirmed. [MISSING — needs clarification]
SEU-based filtering — depends on the EnMS/ISO 50001 epic (not started).
Histogram chart type — [MISSING — needs clarification]; no confirmed precedent, and confirmed as a genuinely new concept with no detailed specification yet.
"Project tag" attribute filtering — [MISSING — needs clarification] whether this maps to an existing free-text classification field or requires a new one.
Internationalization & Localization
Labels, units and number formats are rendered by the consumer, not by this engine: the response carries subject labels as stored, unit codes, and ISO timestamps with the Europe/Prague offset. Bucket boundaries follow the local calendar (FR2), which is the one localisation decision that cannot be left to the consumer, because it changes the values themselves.
Non-Functional Requirements
- Query limits. None in this slice: the period asked for is the period served (FR2). The first realistic portfolio is measured at the 15-minute level over long periods, and the limits that measurement justifies — a point budget, a series budget, or both — are added then, validated before the query runs rather than after it returns.
- Performance. Timeline queries are measured against a realistic portfolio over a full year at every granularity, including 15 minutes, because that measurement is what the limits above will be derived from. Filtering, grouping and ordering dimensions are indexed; a materialized ranking is introduced only if measurement shows the plain query cannot meet the target.
- Determinism. Secondary ordering (
total DESC, label ASC, medium ASC, subjectId ASC) is part of the contract, so repeated identical requests return identical order. - Observability. Each query records its granularity, the aggregation level it read, the number of series and points returned, and the ordering applied — never a re-derived source-priority decision, which belongs upstream.
- Transactional Operations: none — this is a read-only feature.
Dependency / sequencing. The stored consumption series and its summaries are owned by the Consumption & Cost Calculation Core feature (consumption store, continuous aggregates, effective-consumption read path). This engine consumes them and adds no aggregation of its own. Until they carry real data, the engine is built and tested against the agreed shape of that store filled with a hand-computable fixture set (see Test Data), so the contract, the ordering, the coverage semantics and the rendering can all be verified before the upstream data lands.
Performance
Covered under Non-Functional Requirements above: the caps that bound a request, the two query shapes measured separately, and the indexing and determinism rules that keep repeated queries comparable.
Transactional Operations
N/A — read-only.
Processes & Related Systems
- Consumption & Cost Calculation Core owns the stored series, the summaries and the effective-consumption rule; this engine reads them and re-derives none of it.
- Chart Types, Dashboard Tiles & Custom Views renders the response and owns every presentation decision.
- Report Builder and Dashboard Management consume the same endpoint through that feature, so a chart, a report block and a dashboard tile show the same numbers.
- Invoice Management reads period totals from Consumption Aggregation rather than from here; the two read the same stored values, so a total beside a chart matches it.
Diagrams & Models
A chart query, from the request to the rendered series:
sequenceDiagram
actor U as User
participant S as Chart screen
participant E as Chart Engine
participant A as Authorisation
participant C as Consumption store
U->>S: view, scope, medium, period, granularity, unit, ordering
S->>E: GET /v1/charts/series
E->>A: resolve scope and attribute filters to permitted subjects
A-->>E: subject set
E->>C: read the matching aggregation level for those subjects
C-->>E: values, gaps, recalculation state
E->>E: sum to granularity, convert unit, total, order, apply limit, count coverage
E-->>S: series + total + coverage + unavailable entries
S-->>U: chart and value table from one response
A request whose window the caps shorten (FR2):
stateDiagram-v2
[*] --> Validated: parameters parse
Validated --> Rejected: range invalid, media of mixed quantities, unit not shared
Validated --> Served: the period asked for, clipped to bucket boundaries
Served --> [*]
Rejected --> [*]
API Analysis
API Analysis — GET /v1/charts/series.
Ten worked request/response pairs against the fixture set below, with every figure computed by hand so that it can be used as a test — the shape of the answer, the signed selection total, several media on one axis, water as its own answer, a clock-change day, a clipped custom period, the named states and the value table's header: api-a-cases.md. The screen's mock fixtures, the epic's test corpus and the reference the backend is written against are all that one document.
Domain Model (ER diagram) & Data Attribute Table
This feature introduces no entities and writes nothing. It reads:
- consumption — the stored 15-minute series and its summaries (owned by Consumption & Cost Calculation Core)
- gauge — subject identity, medium, unit, purpose, sign in outputs
- building — the tree that carries sector, campus subtree and the attribute filters of FR7
- buildingEnergyProfile — PENB class for FR7
- buildingEnergyBaseline and buildingCalculatedConsumption — the overlays of FR5
erDiagram
building ||--o{ gauge : "has"
building ||--o| buildingEnergyProfile : "rated by"
building ||--o{ buildingEnergyBaseline : "baseline per medium"
building ||--o{ buildingCalculatedConsumption : "calculated per usage"
gauge ||--o{ consumption : "produces"
building ||--o{ building : "tree (campus subtree)"
Data
The shape the engine reads, as a representative seed of the consumption store — one gauge, one hour of electricity, one interval missing:
sql
INSERT INTO consumption (tenant_id, gauge_id, slot_at, kind, type, direction, tariff, source, value, unit)
VALUES
('00000000-0000-0000-0000-0000000000aa', '…e1', '2026-03-02T00:00:00+01:00', 'consumption', 'energy', 'import', 'total', 'remote', 2.100000, 'kWh'),
('00000000-0000-0000-0000-0000000000aa', '…e1', '2026-03-02T00:15:00+01:00', 'consumption', 'energy', 'import', 'total', 'remote', 2.400000, 'kWh'),
('00000000-0000-0000-0000-0000000000aa', '…e1', '2026-03-02T00:45:00+01:00', 'consumption', 'energy', 'import', 'total', 'remote', 2.300000, 'kWh');The hour above totals 6.800000 kWh with coverage 3 of 4 — the interval at 00:30 is absent, not zero, which is the distinction every consumer of this contract depends on.
Test Data
The E2E tenant seed named in the project overlay carries this feature's fixtures. Search it for the chart-engine block: the gauge labelled CHART-E1 (a full month of 15-minute electricity, including a clock-change day and a run of missing intervals), the building labelled CHART-B1 (a reportOnly sub-gauge that only displays, self-generation and an export gauge, so its system total differs from the sum of its gauges), the sector CHART-S1 and the campus node CHART-C1 above several such buildings, and one gauge per remaining medium — gas stored in kWh, heat readable in GJ, water in m³ — which is where the accepted and the refused unit conversion come from.
Logging
.info: chart query executed (segment, granularity, filter summary, resulting series count).debug: resolved aggregation level for the request, the subject and point counts returned, the ordering applied, and whether edge buckets were clipped.warn: request for a deferred/blocked segment (cost, price, savings, SEU filter)
Monitoring
N/A — not covered in source material beyond the logging entries above.
Caching
N/A — not covered in source material.
Backward Compatibility and Migration
No existing API to remain compatible with — this is new capability. It reads only from entities that already carry any migrated historical data (see Domain Model above); no separate migration step is required at this layer.
Legal Context
N/A — the engine reads operational measurements of the tenant's own assets and produces no personal data; retention of the underlying series is decided where that series is stored.
Cybersecurity Considerations
- Tenant isolation is enforced by the storage layer; on top of it the permission code
tenant.charts.readgates the endpoint and the subject set is resolved before aggregation and ordering, so no total, order or coverage figure can be computed from data the caller may not see. - Object-level access (a user limited to assigned buildings) is not yet implemented platform-wide; the engine resolves the subject set in one place so that restriction applies there without changing any other rule.
- Query logs carry identifiers, counts and timings — never measured values.
Risk Assessment
Business Risks
| Risk | Business impact | Mitigation |
|---|---|---|
| A consumer adds a building total and the gauges behind it | Consumption overstated in a report a customer acts on; the number is defensible nowhere | The engine returns the selection total and names the rows excluded from it (FR9) |
| Missing data read as zero | A gap reads as a saving; a customer is told they improved when they were not measured | Gaps stay null, coverage is reported in quarter-hours, a clipped edge bucket carries its real boundaries (FR2, FR11) |
| A conversion returned unconverted under the new unit label | Figures wrong by orders of magnitude in a customer-facing chart — the defect the previous system shipped | Refused conversions return unavailable with a reason (FR8) |
| Charts, tables and totals disagreeing across screens | Trust in the whole product, not just one chart | One endpoint, one response, one conversion point; period totals read the same store |
Technical Risks
| Risk | Consequence | Mitigation |
|---|---|---|
| A second implementation of source priority | Two sets of numbers for the same question, drifting apart | Source resolution stays in the upstream feature (FR3) |
| Fine granularity over a wide period or many subjects | Slow queries, an unusable screen, load on the shared store | Accepted knowingly in this slice: the period asked for is the period served. What a real portfolio carries is measured first, and the limits that measurement justifies are added as their own change (deferred list) |
| Ordering applied before authorisation | An order computed from rows the caller may not see | Subject set resolved first, in one place (Cybersecurity) |
| Upstream store not yet carrying data | The path cannot be exercised | Built against the agreed store shape with the fixture set above |
| Non-deterministic order on ties | Two identical requests disagree; a paged or limited result flickers | Documented secondary ordering |
Auditing, Reporting & Measurement
Read-only, so nothing is audited as a change. What is measured is the query itself — granularity, aggregation level read, subject and point counts, ordering and limit, duration — which is what tells whether the caps hold and where the engine is slow.
Originally migrated from Confluence page "Chart Engine" (id 688226328). Revised against the EM2 report code, the EM3 codebase and the decisions recorded for the first consumption-chart slice. Items still flagged [MISSING — needs clarification] belong to metrics outside that slice and remain open.