diff --git a/specs/Formula-Columns-Specification.md b/specs/Formula-Columns-Specification.md
new file mode 100644
index 00000000000..61f2f3a6bf3
--- /dev/null
+++ b/specs/Formula-Columns-Specification.md
@@ -0,0 +1,1342 @@
+# Formula (Calculated) Columns Specification
+
+### Contents
+
+1. [Overview](#overview)
+2. [User Stories](#user-stories)
+3. [Functionality](#functionality)
+
+ 3.1. [End-User Experience](#end-user-xp)
+
+ 3.2. [Developer Experience](#dev-xp)
+
+ 3.3. [Expression Language](#expression-language)
+
+ 3.4. [Architecture](#architecture)
+
+ 3.5. [Feature Integration](#feature-integration)
+
+ 3.6. [Globalization/Localization](#globalization)
+
+ 3.7. [Keyboard Navigation](#keyboard)
+
+ 3.8. [API](#api)
+4. [Test Scenarios](#test-scenarios)
+
+ 4.1. [Automation](#automation)
+
+ 4.2. [Manual](#manual)
+5. [Accessibility](#accessibility)
+6. [Assumptions and Limitations](#assumptions-and-limitations)
+7. [References](#references)
+
+### Owned by
+
+**Team Name**
+
+**Developer Name**
+
+### Requires approval from
+
+- [ ] Peer Developer Name | Date:
+- [ ] Platform Architect Name | Date:
+
+### Signed off by
+
+- [ ] Product Owner Name | Date:
+- [ ] Platform Architect Name | Date:
+
+## Revision History
+
+| Version | User | Date | Notes |
+|--------:|------|------|-------|
+| 0.1 | | | Initial draft — engine, UI, API surface, test plan, resolved design questions |
+
+## 1. Overview
+
+A **formula column** (also called a *calculated column*) is a grid column whose value is **derived from an
+expression evaluated against the row** instead of being read from a stored field. The derived value is produced
+*before* the data pipeline runs, so the column behaves like a stored field for every other grid feature —
+sorting, filtering, grouping, summaries, search, clipboard and export.
+
+Crucially, formula columns are **authorable at run time by the end user**: the grid ships a toolbar action that
+opens a formula editor dialog, in which the user types `=[Price] * [Quantity]`, names the column, picks a result
+data type, previews the result and commits. The new column is then indistinguishable from a developer-declared
+one.
+
+Today the grid has no concept of a derived value. Every value path resolves a column against the record by field
+path — `resolveNestedPath(record, columnFieldPath(field))` — in
+[`filtering-strategy.ts:270`](../projects/igniteui-angular/core/src/data-operations/filtering-strategy.ts#L270),
+[`grid-sorting-strategy.ts:103`](../projects/igniteui-angular/core/src/data-operations/grid-sorting-strategy.ts#L103),
+[`merge-strategy.ts:120`](../projects/igniteui-angular/core/src/data-operations/merge-strategy.ts#L120),
+[`tree-grid-filtering-strategy.ts:34`](../projects/igniteui-angular/core/src/data-operations/tree-grid-filtering-strategy.ts#L34)
+and [`pipes.ts:336`](../projects/igniteui-angular/grids/core/src/common/pipes.ts#L336).
+Derived columns are therefore faked in one of two lossy ways:
+
+1. **`formatter`** ([`column.component.ts:735`](../projects/igniteui-angular/grids/core/src/columns/column.component.ts#L735))
+ changes only what is *rendered*. Sorting, filtering, grouping, summaries, search and export still see the
+ underlying (usually `undefined`) field value. It is declared `/* blazorOnlyScript */`, so it does not cross
+ over to the Web Components / Blazor wrappers.
+2. **Pre-computing the value into the data source.** This works, but it forces the app to re-derive on every
+ edit, breaks down with remote data and batch editing, bloats the row model, and makes the set of derived
+ columns something only a *developer* can change, at build time.
+
+The documented escape hatch — an unbound column plus custom strategies — requires a matched set of
+`sortStrategy`, `filters`, `groupingComparer`, `mergingComparer` and `summaries` per column just to make one
+derived column behave like a real one, and it cannot be driven from a dialog at all.
+
+### Objectives
+
+The feature includes the following:
+
+- A safe, serializable, `eval`-free expression language with cross-column references and a function library.
+- A single **value-resolution hook** so that sorting, filtering, grouping, merging, summaries, search, clipboard
+ and export see the computed value with no per-feature strategy code.
+- A dependency graph with incremental recalculation on cell edit, row edit, transactions and `data`
+ reassignment.
+- Read-only cells by default — `editable` on a formula column means "edit the *formula*", not the value.
+- Contained, Excel-shaped error values (`#DIV/0!`, `#VALUE!`, …) that never throw through the pipeline.
+- A **formula editor dialog** with inline validation, autocomplete, a function list and a live preview.
+- A **toolbar entry point** mirroring `igx-grid-toolbar-advanced-filtering`, plus edit/delete from the column's
+ Excel-style-filtering menu.
+- Round-tripping of user-created columns through `IgxGridStateDirective`.
+- Full keyboard, screen reader, theming and localization parity with the rest of the grid UI.
+
+### Acceptance criteria
+
+> **Must-have before we can consider the feature a sprint candidate**
+
+1. A column can declare a formula instead of (or in addition to) a `field`, and the result is what the whole
+ pipeline sees — not just the renderer.
+2. Formulas may reference other columns, including other formula columns, by header or by field name.
+3. Formula columns are sortable, filterable, groupable and summarizable with **no extra strategy code**.
+4. The result has a `dataType` (inferred, overridable) so date/number/currency `pipeArgs`, editors and filtering
+ operands work unchanged.
+5. Cells are read-only by default; editing a dependency recalculates dependents through cell edit, row edit,
+ batch editing/transactions and `data` reassignment.
+6. Evaluation errors are contained to the offending cell; circular dependencies are detected at definition time.
+7. Formula definitions — including ones created at run time by the user — round-trip through
+ `IgxGridStateDirective`.
+8. Excel and CSV export contain the computed values.
+9. The primary authoring form is a **string**, so it marshals to the Web Components and Blazor wrappers.
+10. The grid exposes a toolbar action that opens the editor and adds a calculated column on commit; existing
+ formula columns can be edited and deleted without losing grid state.
+11. The editor validates as the user types and blocks commit with a message pointing at the offending token.
+12. The whole feature can be switched off, or restricted to a set of columns, by the developer.
+13. No `eval` and no `new Function` anywhere in the evaluation path.
+
+### Scope of the MVP
+
+The MVP is deliberately end-to-end — engine *and* UI — because a developer-only API does not solve the problem
+that motivates the request. It ships on **`igx-grid` only**, with values-only export and a deliberately small
+function library.
+
+**Explicitly out of scope for the MVP:**
+
+- Cross-row / cross-sheet references (`SUM(A1:A10)`, references to *other* rows). The MVP is strictly
+ **row-scoped**.
+- Column-level aggregates inside row formulas (`[Price] / SUM([Price])`) — see [Q4](#q4).
+- Circular reference *resolution* beyond detection and clear error reporting.
+- Server-side formula evaluation for remote data — see [R3](#r3).
+- Export of live `=` worksheet formulas to Excel (values only in the MVP).
+- Tree Grid aggregate references, Hierarchical Grid per-island formulas, Pivot calculated measures, Grid Lite —
+ see [Q5](#q5).
+
+### Delivery phases
+
+| Phase | Content |
+|-------|---------|
+| **MVP (v1)** | Engine, full pipeline participation, recalculation on edit, state persistence, formula editor UI with toolbar entry point, validation, autocomplete, error presentation, theming, a11y and localization. `igx-grid` only. |
+| **v1.next** | Real `=` worksheet formula export from `IgxExcelExporterService` (AST → A1/R1C1 translation), expanded function library, richer editor affordances (signature help, formula history/reuse), conditional formatting over formula columns. |
+| **v2** | Tree Grid (formulas over hierarchical records, including references to aggregated children), Hierarchical Grid (per-island formulas), Pivot Grid calculated *measures* (a related but distinct feature — likely its own issue), Grid Lite scope decision. |
+
+## 2. User Stories
+
+**End-user stories:**
+
+- Story 1: As an end user, I want to create a new column from an expression over existing columns, so that I can
+ see derived data without asking a developer to change the application.
+- Story 2: As an end user, I want to reference columns by the header text I can see, so that I do not have to
+ know the underlying field names.
+- Story 3: As an end user, I want the editor to tell me *while I type* that a column or function name is wrong
+ and where, so that I can fix it before committing.
+- Story 4: As an end user, I want to pick from a list of available columns and functions, so that I do not have
+ to memorise names or spelling.
+- Story 5: As an end user, I want to preview the result over the first few rows before creating the column, so
+ that I can confirm the formula does what I meant.
+- Story 6: As an end user, I want to give the new column a header, a result data type and a display format, so
+ that it reads like the rest of the grid.
+- Story 7: As an end user, I want to sort, filter, group and summarize a calculated column exactly like any
+ other column.
+- Story 8: As an end user, I want the calculated values to update when I edit a cell they depend on, so that the
+ grid stays consistent.
+- Story 9: As an end user, I want a clear error in the cell (and an indicator on the header) when a formula
+ cannot be evaluated for a row, so that I can tell a bad row from a blank one.
+- Story 10: As an end user, I want to edit or delete a calculated column I created, without losing my sorting,
+ filtering and grouping.
+- Story 11: As an end user, I want my calculated columns to still be there after I reload the app.
+- Story 12: As an end user, I want to copy calculated cells to the clipboard and export them to Excel/CSV and
+ get the computed values.
+- Story 13: As an end user, I want the editor to be fully operable from the keyboard and announced by my screen
+ reader.
+
+**Developer stories:**
+
+- Story 1: As a developer, I want to declare a formula column in markup with a string expression, so that it
+ works in the Angular, Web Components and Blazor wrappers alike.
+- Story 2: As a developer, I want a TypeScript escape hatch for formulas that the expression language cannot
+ express.
+- Story 3: As a developer, I want formula columns to participate in sorting, filtering, grouping and summaries
+ without writing a strategy per column.
+- Story 4: As a developer, I want to override the inferred result data type and supply `pipeArgs`, so that
+ currency/date/percent formatting works unchanged.
+- Story 5: As a developer, I want to add, edit and remove formula columns programmatically, so that I can build
+ my own authoring UI.
+- Story 6: As a developer, I want to validate an expression standalone and get a structured, positioned error,
+ so that I can drive my own editor.
+- Story 7: As a developer, I want to register custom functions and have them show up in the editor's function
+ list and autocomplete with no extra work.
+- Story 8: As a developer, I want to switch end-user authoring off entirely, or restrict which columns can be
+ referenced, so that apps that must not let users derive new data can comply.
+- Story 9: As a developer, I want formula definitions to be part of the grid state so that I can persist and
+ restore them.
+- Story 10: As a developer, I want to be notified when a column is added, edited or removed and when a formula
+ errors, so that I can log and react.
+- Story 11: As a developer, I want to be sure the evaluator never uses `eval`/`new Function`, so that the grid
+ works under a strict Content Security Policy.
+- Story 12: As a developer, I want apps that never use a formula column to pay for neither the parser nor the
+ editor UI.
+
+## 3. Functionality
+
+### 3.1. End-User Experience
+
+#### Creating a calculated column
+
+The entry point is a toolbar action, placed alongside the existing ones and behaving the same way:
+
+```html
+
+
+
+
+
+
+
+```
+
+Activating it opens the **formula editor dialog** in a modal overlay, following the anatomy of the advanced
+filtering dialog ([`advanced-filtering-dialog.component.ts`](../projects/igniteui-angular/grids/core/src/filtering/advanced-filtering/advanced-filtering-dialog.component.ts)):
+a draggable header, a body, and an apply/cancel footer.
+
+```text
+┌─ Add calculated column ────────────────────────────────────────────── [drag] ─┐
+│ │
+│ Column name Result type Format │
+│ ┌─────────────────┐ ┌──────────────────┐ ┌──────────────────────────────┐ │
+│ │ Total │ │ Currency ▾ │ │ 1.2-2, EUR │ │
+│ └─────────────────┘ └──────────────────┘ └──────────────────────────────┘ │
+│ │
+│ Formula │
+│ ┌─────────────────────────────────────────────────────────────────────────┐ │
+│ │ = [Price] * [Quantity] * (1 - [Discount]) │ │
+│ └─────────────────────────────────────────────────────────────────────────┘ │
+│ ⚠ Unknown column 'Qty' at position 10. │
+│ │
+│ ┌── Columns ──────────────┐ ┌── Functions ──────────────────────────────┐ │
+│ │ Price number │ │ ▾ Math ABS ROUND ROUNDUP ROUNDDOWN … │ │
+│ │ Quantity number │ │ ▾ Logic IF IFS AND OR NOT ISBLANK … │ │
+│ │ Discount percent │ │ ▾ Text CONCAT LEFT RIGHT MID LEN … │ │
+│ │ Cost currency │ │ ▾ Date TODAY NOW YEAR MONTH DAY … │ │
+│ └─────────────────────────┘ │ IF(condition, value_if_true, value_if_… ) │ │
+│ └───────────────────────────────────────────┘ │
+│ Preview │
+│ ┌───────────┬────────────┬────────────┬───────────────────────────────────┐ │
+│ │ Price │ Quantity │ Discount │ Total │ │
+│ │ 18.00 │ 12 │ 0.10 │ €194.40 │ │
+│ │ 19.00 │ 40 │ 0.00 │ €760.00 │ │
+│ │ 10.00 │ 0 │ 0.05 │ €0.00 │ │
+│ └───────────┴────────────┴────────────┴───────────────────────────────────┘ │
+│ │
+│ [ Cancel ] [ Add column ] │
+└───────────────────────────────────────────────────────────────────────────────┘
+```
+
+Behaviour:
+
+- The **formula input** is validated on every keystroke (debounced). The validation message names the error and
+ points at the offending token and position. **Apply is disabled while the expression is invalid or empty.**
+- The **Columns** list shows every referenceable column by header, with its data type. Double-click, `Enter`, or
+ drag inserts `[Header]` at the caret.
+- The **Functions** list is generated from the public function registry and grouped by category. Selecting a
+ function shows its signature and description; double-click/`Enter` inserts `NAME()` and places the caret
+ inside the parentheses.
+- **Autocomplete** is offered inline: typing `[` opens the column list filtered as the user types; typing two or
+ more identifier characters at a position where a function is legal opens the function list. Implemented with
+ [`IgxAutocompleteDirective`](../projects/igniteui-angular/drop-down/src/drop-down/autocomplete/autocomplete.directive.ts).
+- The **result type** defaults to the type inferred from the expression and can be overridden. The **format**
+ editor is type-aware and produces `pipeArgs`.
+- The **preview** evaluates the formula over the first `previewRowCount` (default `5`) rows of
+ `filteredSortedData` and renders the referenced columns plus the result, formatted with the chosen type and
+ format. Preview cells that error render the error value.
+- **Apply** creates the column and appends it after the last visible column; **Cancel** discards.
+
+#### Editing and deleting
+
+For a column that already has a formula, the Excel-style filtering menu gains two entries — *Edit formula* and
+*Delete column* — rendered only when end-user authoring is enabled and an editor host is present. *Edit formula*
+opens the same dialog pre-filled and in "edit" mode (the apply button reads *Save*). *Delete column* is only
+offered for **user-created** columns; a developer-declared formula column can be edited but not deleted from the
+UI.
+
+Editing or deleting a formula column **must not reset unrelated grid state**: sorting, filtering, grouping,
+pinning, hiding, row selection and expansion are preserved. Sorting/filtering/grouping expressions that
+reference a *deleted* column are dropped, consistent with how `updateColumns` recreates the filtering trees
+([`grid-base.directive.ts` → `updateColumns`](../projects/igniteui-angular/grids/grid/src/grid-base.directive.ts)).
+
+#### Cells and errors
+
+- Formula cells render like any other cell of their `dataType`, using the column's `pipeArgs`.
+- Formula cells are **read-only**: they are skipped by cell/row edit entry, cannot be entered with `Enter`/`F2`,
+ and are excluded from the row-edit form. They remain navigable, selectable, copyable and searchable.
+- A cell whose evaluation failed renders a themed error state showing the Excel-shaped error value
+ (`#DIV/0!`, `#VALUE!`, `#REF!`, `#NAME?`, `#NUM!`, `#CIRCULAR!`) with a tooltip carrying the message. The
+ cell is decorated with `aria-invalid="true"` and an `aria-describedby` pointing at a visually hidden
+ description.
+- When **any** row in the column errored, the header shows an error indicator with an accessible label and a
+ tooltip stating how many rows failed.
+- `formulaErrorTemplate` on the column overrides the error cell rendering.
+
+### 3.2. Developer Experience
+
+#### Declarative
+
+```html
+
+
+
+
+```
+
+```typescript
+public statusFormula = `IF([Stock] = 0, 'Out of stock', IF([Stock] < 10, 'Low', 'OK'))`;
+```
+
+A formula column does not need a `field`. When `field` is omitted the grid generates a stable synthetic key from
+the header (`formula_total`, deduplicated with a numeric suffix) and assigns it, so that every existing
+field-keyed mechanism — selection, state, export, summaries cache — keeps working unchanged.
+
+When both `field` and `formula` are set, the formula wins for value resolution and the stored field value is
+ignored; this is the supported way to *shadow* an existing field.
+
+#### Column definition objects
+
+```typescript
+grid.columns = [
+ { field: 'Price', dataType: 'number' },
+ { field: 'Quantity', dataType: 'number' },
+ {
+ header: 'Total',
+ // string form: serializable, works in WC/Blazor, round-trips through grid state,
+ // and is the same shape the UI produces
+ formula: '[Price] * [Quantity]',
+ dataType: 'currency'
+ },
+ {
+ header: 'Margin %',
+ // escape hatch: full TS, Angular-only, /* blazorOnlyScript */, not authorable from the UI
+ formulaFn: (rowData: any): FormulaValue =>
+ (rowData.Total - rowData.Cost) / rowData.Total,
+ dependsOn: ['Total', 'Cost'], // explicit deps, since they cannot be parsed
+ dataType: 'percent'
+ }
+];
+```
+
+#### Runtime authoring — what the UI calls under the hood
+
+```typescript
+grid.addFormulaColumn({ header: 'Total', formula: '[Price] * [Quantity]', dataType: 'currency' });
+
+// recalculates dependents
+grid.getColumnByName('formula_total').formula = '[Price] * [Quantity] * 1.2';
+
+grid.removeFormulaColumn('formula_total');
+```
+
+#### Validation, usable standalone by an app building its own editor
+
+```typescript
+const result = grid.formulaEngine.validate('[Price] * [Qty]');
+// {
+// valid: false,
+// errors: [{ code: 'UNKNOWN_COLUMN', message: "Unknown column 'Qty'.", token: 'Qty', position: 10, length: 3 }],
+// dataType: undefined,
+// references: ['Price']
+// }
+```
+
+#### Custom functions
+
+```typescript
+grid.formulaEngine.registerFunction({
+ name: 'VATINCL',
+ category: 'Math',
+ minArgs: 1,
+ maxArgs: 2,
+ returnType: 'number',
+ description: 'Adds VAT to a net amount. VATINCL(amount, [rate=0.2])',
+ evaluate: (args) => Number(args[0]) * (1 + (args[1] === undefined ? 0.2 : Number(args[1])))
+});
+```
+
+Registering a function surfaces it in the editor's function list, its autocomplete and its signature help with
+no extra work — the UI is generated from the registry.
+
+#### Turning the feature off / restricting it
+
+```html
+
+
+
+
+
+```
+
+`allowFormulaColumns` defaults to `false`. It gates only **end-user authoring** — the toolbar action renders
+disabled and the Excel-style menu entries are hidden. Developer-declared `formula` columns are unaffected, so
+turning the switch on is never required to use the engine.
+
+#### Events
+
+```typescript
+grid.formulaColumnAdded.subscribe((e: IFormulaColumnEventArgs) => { /* ... */ });
+grid.formulaColumnEdited.subscribe((e: IFormulaColumnEventArgs) => { /* ... */ });
+grid.formulaColumnRemoved.subscribe((e: IFormulaColumnEventArgs) => { /* ... */ });
+grid.formulaError.subscribe((e: IFormulaErrorEventArgs) => { /* ... */ });
+```
+
+### 3.3. Expression Language
+
+The language is **row-scoped**: an expression sees exactly one record and produces exactly one value. It is
+locale-independent in its stored form (see [Q3](#q3)).
+
+#### Grammar
+
+```ebnf
+expression = logical_or ;
+logical_or = logical_and , { "OR" , logical_and } ;
+logical_and = comparison , { "AND" , comparison } ;
+comparison = concat , { ( "=" | "<>" | "<" | "<=" | ">" | ">=" ) , concat } ;
+concat = additive , { "&" , additive } ;
+additive = multiplicative , { ( "+" | "-" ) , multiplicative } ;
+multiplicative = power , { ( "*" | "/" | "%" ) , power } ;
+power = unary , { "^" , unary } ; (* right associative *)
+unary = [ "-" | "+" | "NOT" ] , primary ;
+primary = number | string | boolean | "NULL"
+ | reference | function_call | "(" , expression , ")" ;
+reference = "[" , { ? any char except "]" ? } , "]" ;
+function_call = identifier , "(" , [ expression , { "," , expression } ] , ")" ;
+```
+
+A leading `=` is accepted and stripped, so users may type either `[Price] * 2` or `=[Price] * 2`. The canonical
+stored form has no leading `=`.
+
+#### Operator precedence
+
+From lowest to highest binding:
+
+| Precedence | Operators | Associativity | Notes |
+|-----------:|-----------|---------------|-------|
+| 1 | `OR` | left | Logical or |
+| 2 | `AND` | left | Logical and |
+| 3 | `=` `<>` `<` `<=` `>` `>=` | left | Comparison; `=` is equality, not assignment |
+| 4 | `&` | left | String concatenation |
+| 5 | `+` `-` | left | Addition / subtraction |
+| 6 | `*` `/` `%` | left | `%` is modulo |
+| 7 | `^` | **right** | Exponentiation |
+| 8 | unary `-` `+` `NOT` | right | |
+| 9 | `(` `)`, function call, reference | — | Primary |
+
+#### References
+
+`[Column Header]` or `[field.nested.path]`. Resolution order when parsing:
+
+1. Exact, case-insensitive match on a column `header`.
+2. Exact, case-sensitive match on a column `field`, including dotted nested paths.
+
+References are **canonicalized to `field` when stored**, so renaming a header never breaks a saved formula
+(see [Q1](#q1)). The editor renders the canonical form back as headers for display.
+
+A reference to a column that does not exist yields `#REF!` at *definition* time, reported as an
+`UNKNOWN_COLUMN` validation error before commit.
+
+#### Literals
+
+| Literal | Syntax | Example |
+|---------|--------|---------|
+| Number | Decimal, `.` as decimal separator, optional exponent | `12`, `3.5`, `1.2e3` |
+| String | Single-quoted; `''` escapes a quote | `'Low'`, `'it''s'` |
+| Boolean | `TRUE` / `FALSE`, case-insensitive | `TRUE` |
+| Null | `NULL`, case-insensitive | `NULL` |
+
+#### Type coercion
+
+| Situation | Rule |
+|-----------|------|
+| Arithmetic on a non-numeric string | Numeric-looking strings coerce; otherwise `#VALUE!` |
+| Arithmetic on `null` / `undefined` / `''` | Treated as `0` |
+| `&` on any value | Coerced to string; `null` becomes `''` |
+| Comparison of mixed types | The operands are coerced to a common type first: number vs. numeric string → number; boolean vs. number → number (`TRUE` = 1); anything vs. non-numeric string → string. `NULL` equals only `NULL`, and is less than every other value. Types that cannot be brought to a common type yield `#VALUE!` |
+| `Date` in arithmetic | Coerced to epoch milliseconds |
+| Division by zero | `#DIV/0!` |
+| Any operand already an error | Propagates unchanged (except inside `IFERROR`/`ISERROR`) |
+
+#### Result data type inference
+
+The parser infers a `GridColumnDataType` from the AST root: arithmetic and math functions → `number`;
+comparison, logical functions and `ISBLANK`/`ISERROR` → `boolean`; `&` and text functions → `string`;
+`TODAY`/`NOW`/`EDATE` → `date`. `IF`/`IFS` infer the common type of their branches, falling back to `string`.
+An explicit `dataType` on the column always wins.
+
+#### Function library (MVP)
+
+| Category | Functions |
+|----------|-----------|
+| Math | `ABS` `ROUND` `ROUNDUP` `ROUNDDOWN` `CEILING` `FLOOR` `MIN` `MAX` `SUM` `POWER` `SQRT` `MOD` |
+| Logic | `IF` `IFS` `AND` `OR` `NOT` `ISBLANK` `ISERROR` `IFERROR` |
+| Text | `CONCAT` `LEFT` `RIGHT` `MID` `LEN` `UPPER` `LOWER` `TRIM` `SUBSTITUTE` `TEXT` |
+| Date | `TODAY` `NOW` `YEAR` `MONTH` `DAY` `DATEDIF` `EDATE` |
+
+`MIN`, `MAX` and `SUM` are **row-scoped variadic** functions over their arguments (`SUM([A], [B], [C])`), *not*
+column aggregates. Function names are always English in the stored expression (see [Q6](#q6)).
+
+#### Error values
+
+| Value | Raised when |
+|-------|-------------|
+| `#DIV/0!` | Division or modulo by zero |
+| `#VALUE!` | An operand cannot be coerced to the required type |
+| `#REF!` | A referenced column no longer exists at evaluation time |
+| `#NAME?` | An unknown function name survived to evaluation |
+| `#NUM!` | A numeric result is not finite (overflow, `SQRT` of a negative number) |
+| `#CIRCULAR!` | The column participates in a dependency cycle |
+
+Errors are values, not exceptions. They flow through the pipeline: Excel-style filtering lists them as a
+distinct value, summaries skip them, and export writes the error string.
+
+Ordering deserves a note. `DefaultSortingStrategy.compareValues`
+([sorting-strategy.ts](../projects/igniteui-angular/core/src/data-operations/sorting-strategy.ts#L77))
+today handles only the nullish case explicitly and otherwise falls through to the JavaScript relational
+operators, which return `false` in both directions for an object operand — an error value would compare
+*equal* to everything and land in an arbitrary position. `compareValues` therefore gains one more guard, ahead
+of the existing nullish checks, that orders `FormulaError` after every non-error value in ascending order.
+That is the only change the sorting strategy needs, and it is covered by a dedicated regression test.
+
+### 3.4. Architecture
+
+#### Package layout
+
+| Artifact | Location | Entry point | Rationale |
+|----------|----------|-------------|-----------|
+| Tokenizer, parser, AST, evaluator, function registry, error types | `projects/igniteui-angular/core/src/data-operations/formula/` | `igniteui-angular/core` | Zero Angular dependencies; unit-testable in isolation; reusable by Grid Lite / Pivot later. |
+| Value resolver + memoization + dependency graph | `projects/igniteui-angular/core/src/data-operations/formula/` | `igniteui-angular/core` | Sits on the data-operations hot path; must be reachable from the strategies. |
+| `formula` / `formulaFn` / `dependsOn` / `isFormulaColumn` on the column | `grids/core/src/columns/column.component.ts`, `core/src/data-operations/grid-types.ts` | existing | |
+| `IgxFormulaEditorComponent`, `IgxGridToolbarFormulaColumnComponent`, formula column actions | `projects/igniteui-angular/grids/formula-editor/` | **new**: `igniteui-angular/grids/formula-editor` | Opt-in, keeps overlay/drop-down/dialog cost out of `grids/core` (see [Q7](#q7)). |
+
+The new entry point follows the existing convention exactly — a directory with an empty `ng-package.json`, an
+`index.ts` re-exporting `./src/public_api`, and a `src/public_api.ts` — the same shape as
+`projects/igniteui-angular/query-builder/` and `projects/igniteui-angular/grids/core/`. No `angular.json` or
+`tsconfig.json` change is needed; the `"igniteui-angular/*"` path mapping already covers it.
+
+`grids/core` must have **no static import** of the editor. The editor entry point provides an
+`IGX_FORMULA_EDITOR_HOST` injection token; `grids/core` renders the Excel-style menu entries and the toolbar
+action only when that token resolves, so an app that never imports the editor pays for neither the dialog nor
+its dependencies.
+
+#### Evaluation pipeline
+
+```text
+formula string
+ │
+ ├── tokenize() ──► Token[] { kind, value, position, length }
+ │ errors: UNTERMINATED_STRING, UNTERMINATED_REFERENCE, UNEXPECTED_CHARACTER
+ │
+ ├── parse() ──► FormulaNode (AST)
+ │ errors: UNEXPECTED_TOKEN, UNEXPECTED_END, UNBALANCED_PARENS, MAX_DEPTH_EXCEEDED
+ │
+ ├── bind() ──► resolves references to column fields, functions to registry entries,
+ │ infers the result data type
+ │ errors: UNKNOWN_COLUMN, UNKNOWN_FUNCTION, ARITY_MISMATCH, CIRCULAR_REFERENCE
+ │
+ └── evaluate(record) ──► value | FormulaError
+```
+
+`tokenize` and `parse` are a hand-written scanner and a precedence-climbing (Pratt) parser; `evaluate` is a
+tree-walking interpreter over the bound AST. **There is no `eval` and no `new Function` anywhere in this path**
+(see [R2](#r2)).
+
+#### Structured errors
+
+The error contract is the *first* thing to land, because the editor's inline validation depends on it and the
+engine and UI work streams cannot proceed in parallel until it is stable (see [R5](#r5)).
+
+```typescript
+export interface IFormulaParseError {
+ code: FormulaErrorCode; // 'UNKNOWN_COLUMN' | 'UNKNOWN_FUNCTION' | 'ARITY_MISMATCH' | 'UNEXPECTED_TOKEN' | ...
+ message: string; // localized, from the grid resource strings
+ token?: string; // the offending source text
+ position: number; // 0-based index into the expression string
+ length: number; // length of the offending token
+}
+
+export interface IFormulaValidationResult {
+ valid: boolean;
+ errors: IFormulaParseError[];
+ references: string[]; // canonical column fields referenced, in source order
+ dataType?: GridColumnDataType; // inferred result type
+}
+```
+
+#### The value-resolution hook
+
+This is the single most important change and the one that makes sorting, filtering, grouping and merging work
+for free. Every existing `resolveNestedPath(record, columnFieldPath(field))` call site routes through one
+resolver that first asks whether the field belongs to a formula column:
+
+```typescript
+// core/src/data-operations/formula/formula-value-resolver.ts
+export function resolveColumnValue(record: unknown, field: string, grid?: GridTypeBase): any {
+ const column = grid?.getColumnByName(field);
+ if (column?.isFormulaColumn) {
+ return grid.formulaEngine.evaluate(column, record);
+ }
+ return resolveNestedPath(record, columnFieldPath(field));
+}
+```
+
+Call sites, and how each is reached:
+
+| Feature | Call site | Change |
+|---------|-----------|--------|
+| Filtering | [`filtering-strategy.ts:270`](../projects/igniteui-angular/core/src/data-operations/filtering-strategy.ts#L270) `FilteringStrategy.getFieldValue` | Already receives `grid`; swap `resolveNestedPath` for `resolveColumnValue`. |
+| Excel-style filter value list | [`filtering-strategy.ts` `getFilterItems`](../projects/igniteui-angular/core/src/data-operations/filtering-strategy.ts) | Same swap; the unique-values list then enumerates computed values. |
+| Tree Grid filtering | [`tree-grid-filtering-strategy.ts:34`](../projects/igniteui-angular/core/src/data-operations/tree-grid-filtering-strategy.ts#L34) | Same swap (resolves against `record.data`). |
+| Sorting | [`grid-sorting-strategy.ts:103`](../projects/igniteui-angular/core/src/data-operations/grid-sorting-strategy.ts#L103) `IgxSorting.getFieldValue` | Extend the signature with `grid?` and thread it through. See the compatibility note below. |
+| Grouping | `IgxGrouping` extends `IgxSorting`; group key at `grid-sorting-strategy.ts` (`getFieldValue(group[0], …)`) | Inherited for free once `IgxSorting` is updated. |
+| Merging | [`merge-strategy.ts:120`](../projects/igniteui-angular/core/src/data-operations/merge-strategy.ts#L120) | Same swap. |
+| Rendering | [`pipes.ts:336`](../projects/igniteui-angular/grids/core/src/common/pipes.ts#L336) `IgxGridDataMapperPipe` | Formula columns always take the resolver branch (they are treated as "nested path" columns for mapping purposes). |
+| Cell value | [`grid-public-cell.ts:132`](../projects/igniteui-angular/grids/core/src/grid-public-cell.ts#L132) | Same swap, so `cell.value` is the computed value. |
+| Summaries | [`grid-summary.service.ts` `calculateSummaries`](../projects/igniteui-angular/grids/core/src/summaries/grid-summary.service.ts) — `data.map(r => resolveNestedPath(r, columnPathParts[idx]))` | Same swap. `IgxSummaryOperand.operate(data, allData, fieldName, groupRecord)` then works unchanged, including group-level summaries. |
+| Search / highlight | `rebuildMatchCache()` in [`grid-base.directive.ts`](../projects/igniteui-angular/grids/grid/src/grid-base.directive.ts) | Same swap; gated by `column.searchable` as today. |
+| Clipboard / `getSelectedData` | `extractDataFromSelection()` in [`grid-base.directive.ts:7531`](../projects/igniteui-angular/grids/grid/src/grid-base.directive.ts#L7531) | Same swap. |
+| Export (Excel/CSV) | [`base-export-service.ts:460`](../projects/igniteui-angular/grids/core/src/services/exporter-common/base-export-service.ts#L460) `exportRow` | Same swap; computed values are exported, `formatter` still applied where configured. |
+| Validation | [`grid-validation.service.ts:61`](../projects/igniteui-angular/grids/core/src/grid-validation.service.ts#L61) `addFormControl` | **Skip** formula columns — no form control is created, so field validators never run against a derived value. |
+
+**Compatibility note for sorting.** `IgxSorting.sortData` currently passes `this.getFieldValue` as an unbound
+callback and `DefaultSortingStrategy.sort` re-binds it with `valueResolver.bind(this)`. To thread the grid
+through, `sortData` passes an arrow closure that captures `grid` and forwards to `this.getFieldValue(obj, key,
+isDate, isTime, grid)`. `Function.prototype.bind` on an arrow function is a no-op, so existing custom
+`ISortingStrategy` implementations that call `valueResolver.bind(this)` keep working; the only observable change
+is that `this` inside `getFieldValue` becomes the sorting *strategy owner* rather than the
+`DefaultSortingStrategy` instance, which is what the overrides (e.g. `IgxDataRecordSorting.getFieldValue`)
+already assume. A regression test covers a custom strategy that binds the resolver.
+
+#### Dependency graph and invalidation
+
+At definition time the engine builds a directed graph over column fields from each formula's parsed references
+(or from `dependsOn` for `formulaFn`). The graph is used for three things:
+
+1. **Cycle detection.** A depth-first search on insert reports `CIRCULAR_REFERENCE` before the column is
+ committed. The offending column is not added (or, for an edit, the previous formula is kept) and the editor
+ surfaces the cycle path (`Total → Margin → Total`).
+2. **Evaluation order.** Formula-on-formula references are evaluated in topological order, so `[Margin]`
+ referencing `[Total]` sees `Total`'s computed value.
+3. **Incremental invalidation.** On `update_cell`
+ ([`api.service.ts:148`](../projects/igniteui-angular/grids/core/src/api.service.ts#L148)) and `update_row`,
+ the engine invalidates only *that record* × the transitive dependents of the edited field. On transaction
+ commit/undo/redo, on `data` reassignment and on `pipeTrigger` bumps caused by a formula edit, the affected
+ scope is widened accordingly.
+
+| Trigger | Invalidation scope |
+|---------|--------------------|
+| `update_cell(field)` | one record × transitive dependents of `field` |
+| `update_row` | one record × all formula columns |
+| Transaction commit / undo / redo / clear | records touched by the transaction × their dependents |
+| `data` setter, `IterableDiffer` change | whole cache |
+| `column.formula` setter | that column and its transitive dependents, all records (formula version bump) |
+| Column added / removed | dependents of the affected field; removal marks dependents `#REF!` |
+
+#### Memoization
+
+Evaluation must never run over the whole data set on every change-detection cycle. The cache is:
+
+```typescript
+WeakMap