# What Axmo can do with a model's data — the capability table Written by production (AGL9-20, from the AGL9-19 Part 0 census, re-measured). **Each row names one test that pins it by value** (`tests/capabilities/capabilities.test.ts`); `check:ai-pack` fails when a row names no such test or a test names no row. **The rows are part of the format version**: a changed row moves the version, and every guide file declaring the old one then fails the guide gate, naming the row — that is trigger T2. Scenario names in brackets are the test fixtures in `tests/fixtures/capabilities/`. "Row N" in a report line counts data rows from 1, without the header. | Row | Question | Works | Does not | Gap | Test | |---|---|---|---|---|---| | 1a | Several loaders into one series | Loaders with disjoint coordinates fill one series, and its roll-ups combine them (P1 10 + P2 20 = 30). | — | None. | `[row 1a]` | | 1b | Duplicate rows within one loader | `duplicateHandling`, set per target, default `FIRST`: of rows 10 and 30, `FIRST` 10, `LAST` 30, `AVERAGE` 20, `SUM` 40. `BLOCK` warns (`SERIES_LOADER_DUPLICATE_COORD_CONFLICT`), and the cell and its totals become error values. | `FIRST`, `LAST`, `AVERAGE` and `SUM` collapse duplicates with no report line. | A source with duplicate rows by mistake is collapsed silently under every policy but `BLOCK`. | `[row 1b]` | | 1b2 | Two loaders writing one cell | Always reported (`SERIES_LOADER_CROSS_LOADER_CONFLICT`), whatever `duplicateHandling` says; the cell becomes an error value. | No precedence between loaders. The conflict fires when both values are equal too, because one cell has one loader; the line states the values (*Both supplied 10*, or *"L1" supplied 10 and "L2" supplied 99*). | Sources cannot be layered, a corrections table over a base table. | `[row 1b2]` | | 1c | Actuals and budget from two tables | One series with a Version dimension, each loader fixing Version (`fixedCoordinates`): Actual 30, Budget 30, All_Version 60. Two separate series work too. | A loader that leaves a dimension of its target unmapped is a compile error (`SL_DIM_MAPPING_MISMATCH`). A series named as an element (`Actual`) is refused. | Series and element names share one namespace. | `[row 1c]` | | 2a | Several generators into one dimension | They merge as one batch, case-insensitively, without duplicates; `parentColumn` builds a layer under another generator's elements, whatever the generators' names (`Catalogue` placing under `Groups` works). One parent's elements keep the order of their first appearance: spec-time first, then by first row, generators in name order. **An element's layer** is its `layerColumn` cell where one is given, any layer strictly below its parent's (a gap allowed: `X1` at Item under All_Product); otherwise, with no column or an empty cell, its parent's layer + 1, and the top layer for an element with no parent. | A generator into a time (sequential) dimension is a compile error (`DG_TARGET_SEQUENTIAL`), and the generator does not run: a sequential dimension's elements come from its parameters only. A layer cell at or above its parent's layer is refused and its row skipped (`DG_LAYER_PARENT_MISMATCH`); two rows giving one element different layers keep the first (`DG_LAYER_REDEFINITION`). A generator never creates a layer. | A time dimension cannot be extended from data; change its parameters (`start`, `end`). | `[row 2a]` | | 2b | How labels match element names | Trimmed and case-insensitive, inner spaces kept, for cell labels and column headers alike (`' P2 '`, `' 2026-02 '`, `ACTUAL`); a number matches by its digits (2025 and `'2026'` both match). | No format conversion: `2026-1`, `Jan 2026`, `Jan-26`, a date serial, `2026-03-01`, `FY2026` do not match, and the row is skipped with a warning. | A source whose period labels differ from the element names must be reshaped first. A date-typed period column in Excel never matches. | `[row 2b]` | | 2c | Mapping a source column or label to a dimension | Any column maps to any dimension (`dimensionMappings: {Time: {column: PERIOD}}`); a dimension can be fixed for a whole loader. | No label-to-element mapping (no alias, lookup or transform). A dimension cannot be read from two columns (Year + Month). | Recoding happens outside Axmo, or in Excel formula columns the input range reads. | `[row 2c]` | | 2d | A row whose label matches no element | Skipped, one warning per distinct label (`SERIES_LOADER_UNKNOWN_DIM_ELEMENT`); empty dimension cells, non-numeric values and missing columns are reported too. | When every row fails, the source loads nothing and the import still exits `0`, with warnings only. | Read the warnings, not only the exit code. | `[row 2d]` | | 3 | Table shapes | Long (dimension columns and one value column). Measures as columns, into several series, with one loader. **Months across columns with one loader**: the data source's `columnLayout: dimension`, `columnsDimension: Time` and a band (`bandStartMode` / `bandEndMode` with their columns), the loader mapping Time to `_HeaderDimElement` and reading `_Value` (P1 1, 2, 3; quarter 6). Rows at a quarter or a year with a split-down rule (`EQUAL` 90 → 20 a month). Rows at mixed levels under the default `splitDown: NONE` stay where they are loaded and reach every total (P1 at `2026-Q1` 30 and P2 at `2026` 60: `All_Product / 2026` 90, `All_Product / 2026-Q1` 30, months empty), at any depth (over `All_Restaurants › Nord (Milano, Torino) › Sud (Napoli)`, Milano at `2026-Q4` 30, Torino at `2026-11` 5 and `2026-12` 6, Napoli at `2026` 12: `All_Restaurants / 2026-Q4` 41, `All_Restaurants / 2026` 53). | Months across columns in one loader **without** `columnLayout` is a compile error. Vector and scalar loaders load nothing (`SL_LOAD_MODE_NOT_FED`). A parent row and its child row in one load collide (`SERIES_DATACELL_COLLISION`). | Vector and scalar sources are unusable. | `[row 3]` | | 4 | Dimensions, and time element names | Static trees and generated elements. `axmo_time` names: `2026`, `2026-H1`, `2026-Q1`, `2026-01`, `2026-01-30`, under `All_Time`; optional `BEFORE` / `AFTER`. | No other naming (no `Jan 2026`, no fiscal years). A layer may not carry its dimension's name. | Period labels in a source must be exactly those forms. | `[row 4]` | | 5 | Combinations that never get data | An empty input cell stays empty; a loaded 0 is 0; `ISEMPTY` tells them apart. | Calculated series count empty as zero, so they are dense: 120 of 120 cells, the whole Budget version included. | Reports over sparse data show long runs of zeros. | `[row 5]` | | 6 | Export depths and re-import | `definition`: dimensions and series. `bindings`: and the data sources, loaders and output ranges. `complete`: and the cells of each data source's range. | Generated elements are not in a `definition` export (`All_Product: null`). A re-import replaces input the user changed after the export; before it does, the Import dialog names how many input cells differ from the file, and in which input ranges, and the user chooses **Import anyway** or **Cancel** (`axmo:check` has no such workbook, so it never shows this). | A definition export of a model with generated dimensions arrives with empty dimensions; re-importing an older file still replaces newer input when the user chooses **Import anyway**. | `[row 6]` | | 7 | A total rolled up by `FORMULA` | The formula (the series' own, or `rollUpFormula`) is evaluated at the total from the total's own inputs, whatever its members hold: `IF(not (ISEMPTY(Marketing)), Marketing / Sales)`, with Marketing loaded only at `Milano / 2026-Q4` (30) and Sales 220 there, is empty in every month and 0.136 at the quarter and its totals. A total is reached where a member holds a value, or where an input of the formula bound on every dimension of the series does. Over several dimensions with different methods, each total is produced in the series' roll-up order. | An input bound on fewer dimensions (a rate by Time only) reaches no total by itself. Every other method leaves a total whose members hold nothing empty. | A guarded formula whose only input is bound on fewer dimensions stays empty at its totals. | `[row 7]` |