# Tesora Compute API — full reference for AI agents

A free, unauthenticated, rate-limited REST service over the Tesora spreadsheet
compute engine (`@tesora/compute`). It evaluates pure Excel-formula
calculations: a workbook of cells and formulas, driven by inputs, producing
named outputs. It has NO database and NO org/auth; the only persistent state is
optional "saved operations". The same engine powers the Tesora platform's
governed compute route and the Excel add-in.

Everything you can compute in Excel formulas works, PLUS a large actuarial
library: `CHAINLADDER.*`, `ACTUAR.*`, `CASDATASETS.*`, `ACTUAFLOW.*`.

---

## Endpoints

| Method | Path | Purpose |
|---|---|---|
| GET  | `/` | HTML documentation landing page (human-facing); agents should read `/llms.txt` instead. |
| GET  | `/tutorial` | Human-facing guided walkthrough (a live notebook over the compute routes); agents should read this document instead. |
| GET  | `/healthz` | Liveness -> `{"status":"ok"}`. |
| GET  | `/llms.txt` | Concise agent guide. |
| GET  | `/llms-full.txt` | This document. |
| GET  | `/docs` | Interactive Swagger UI. |
| GET  | `/openapi.json` | OpenAPI 3 description of this surface. |
| GET  | `/v1/functions` | The function catalog. Filters: `?category=`, `?nameContains=`. |
| POST | `/v1/evaluate` | Evaluate ONE formula -> a single value. |
| POST | `/v1/run` | Run a whole workbook -> named outputs. |
| POST | `/v1/run-tsra` | Compile and run a `.tsra` program -> named outputs. |
| POST | `/v1/feedback` | Report a bug, limitation, docs gap, or idea about THIS API. |
| GET  | `/v1/operations` | List saved operations. |
| POST | `/v1/operations` | Create a saved operation. |
| GET  | `/v1/operations/:slug` | Fetch one saved operation. |
| PUT  | `/v1/operations/:slug` | Update a saved operation. |
| DELETE | `/v1/operations/:slug` | Delete a saved operation. |
| POST | `/v1/operations/:slug/run` | Run a saved operation by slug. |
| POST | `/v1/files` | Upload a small CSV/Parquet (base64 inline, <=10 MB decoded) to the ephemeral store (24-hour TTL). |
| POST | `/v1/files/upload-urls` | Mint a single-use streaming upload URL for a larger file (up to 512 MB). |
| PUT  | `/v1/files/content` | Stream the raw bytes under the minted token (header `x-compute-upload-token`). |
| GET  | `/v1/files` | List your live ephemeral files. |
| GET  | `/v1/files/:fileId` | One ephemeral file's metadata. |
| DELETE | `/v1/files/:fileId` | Delete an ephemeral file early. |

---

## POST /v1/evaluate — one formula

Body: `{ "formula": string, "functions"?: {...}, "cache"?: boolean }`

- `formula`: an Excel formula. A leading `=` is optional (a bare expression is promoted).
- `functions`: optional inline custom functions callable as `=NAME(args)`. A lone formula has no cells to bind data into, so `/v1/evaluate` takes no `data`/`inputs` — use `/v1/run` for a data-driven calculation.
- `cache`: optional, default `true`. See "Caching" below. Set `false` to force a fresh computation.
- Response: `{ "value": <result> }`. A scalar formula returns a bare value; a
  dynamic-array (spilling) formula returns a 2D array (row-major), so an Excel
  add-in can spill it across the grid.

Examples:

```
{"formula":"=1+2*3"}                 -> {"value":7}
{"formula":"SQRT(144)+MAX(3,9,2)"}   -> {"value":21}
{"formula":"=PMT(0.05/12,360,-300000)"} -> {"value":1610.46...}
{"formula":"=SEQUENCE(2,3)"}         -> {"value":[[1,2,3],[4,5,6]]}
{"formula":"=CASDATASETS.LIST()"}    -> {"value":[["dataset","title"],["Davis",...],...]}
```

---

## POST /v1/run — a workbook

Body fields (mirrors the platform's `awb_run_computation` step):

| Field | Type | Meaning |
|---|---|---|
| `template` | object (required) | The workbook: `{ "sheets": [{ "name": string, "data": Cell[][] }] }`. `data` is row-major; a cell is a number, string, boolean, null, or a formula string starting with `=`. |
| `outputs` | object | Output name -> cell reference, e.g. `{ "premium": "Calc!C1" }`. |
| `outputSchema` | array | Optional declared output types, e.g. `[{ "name": "premium", "schemaType": "PRIMITIVE", "schemaItemType": "NUMBER" }]`. Only `PRIMITIVE` entries are applied: they coerce a scalar output to the declared type; `LIST` outputs are left as-is. |
| `inputs` | object | Cell reference -> a `trigger.data.*` binding, e.g. `{ "Calc!A1": "trigger.data.base" }`. The bound value is overlaid onto that cell before calc. |
| `data` | object | The values the `inputs` bindings read, e.g. `{ "base": 100 }`. |
| `functions` | object | Inline custom functions callable from formulas (see below). |
| `simulation` | object | Monte Carlo spec (see below). |
| `failOnFormulaErrors` | boolean | Omitted or `true` (the default): any formula error reaching an output fails the run with a 400 whose `error` names the offending cell, its Excel code, and the fix (wrap in IFERROR or set `failOnFormulaErrors: false`). Explicit `false`: the run succeeds, each errored output holds its bare Excel error code, and the response also gains an `output.formulaErrors` map (cell ref -> `{ error, message?, formula? }`) of the formula errors the engine surfaced while computing the run. The exact set of cells covered varies by request (in some cases just the declared outputs, in others every erroring cell in the workbook), so treat it as a best-effort diagnostic rather than a guaranteed minimal or exhaustive set. `error` is the bare Excel code (e.g. `#DIV/0!`), `message` a plain explanation of that code, and `formula` the cell's formula echoed back from your template. The engine's own error prose is never included, so no platform internals leak. |
| `cache` | boolean | Default `true`. See "Caching" below. Set `false` to force a fresh computation that neither reads nor writes the cache. |

Response: `{ "output": { <name>: <value>, ... } }`, plus `output.formulaErrors`
when `failOnFormulaErrors: false` surfaced any formula errors (a per-cell
diagnostic; see "Errors" below). No engine metadata is exposed.
Simulation statistics (when requested) appear under `output`.

Worked example — `premium = base * factor`:

```
POST /v1/run
{
  "template": { "sheets": [{ "name": "Calc", "data": [[0, 0, "=A1*B1"]] }] },
  "inputs":   { "Calc!A1": "trigger.data.base", "Calc!B1": "trigger.data.factor" },
  "outputs":  { "premium": "Calc!C1" },
  "data":     { "base": 100, "factor": 1.5 },
  "failOnFormulaErrors": true
}
-> { "output": { "premium": 150 } }
```

You can also compute entirely in-sheet (no `inputs`/`data`): put literals and
formulas directly in `template` and read `outputs`.

### Inline custom functions

Pass a `functions` map so a formula can call `=NAME(args)`. Each entry is a
mini-workbook whose cells receive the arguments and expose a result:

```
{
  "formula": "=DOUBLE(21)",
  "functions": {
    "DOUBLE": {
      "name": "DOUBLE",
      "description": "Doubles its numeric argument",
      "template": { "sheets": [{ "name": "F", "data": [[0], ["=A1*2"]] }] },
      "params": [{ "name": "x", "cell": "F!A1", "schemaItemType": "NUMBER" }],
      "returns": { "result": { "cell": "F!A2" } },
      "primaryReturn": "result"
    }
  }
}
-> {"value":42}
```

### Monte Carlo simulation

Add a `simulation` spec to `/v1/run`. `iterations` is a STRING; `outputs` maps a
name to a cell plus the stats to collect (`"mean"`, `"stdev"`, `"min"`, `"max"`,
`"median"`, or a percentile written as `"p95"`/`"p99.5"` or the equivalent
`"percentile:0.95"` fraction). An unrecognized stat name fails the run rather than
being silently dropped. An optional `convergence` object
(`{ "tolerance": 0.03, "minIterations": 500, "checkInterval": 100 }`) enables early
termination; `simulationConverged` is `true`/`false` only when convergence was
requested, and `null` when it was not (the check never ran). Note that the
convergence test watches the MEAN settle, not the tail: a `converged` verdict does
not certify that a deep-tail percentile (e.g. `"p99.5"`, the 1-in-200 level) is
stable. Far-tail estimates typically need more iterations than the body of the
distribution, so sanity-check deep-tail figures against `iterations` regardless of
the convergence verdict.

An optional `seed` (also a STRING, like `iterations`) makes the run reproducible:
the same seed over the same workbook draws the same samples. Omit it and the engine
derives one. Either way the seed it used comes back as `simulationSeed` AND in the
`provenance` stamp beside `requestHash`, so an unpinned run can still be replayed
from its receipt alone by passing that value in as `seed` — the property an audit
needs to re-derive a capital number. Reproducibility holds within an execution tier,
and `simulationVectorized` reports which one ran, so record the pair.

The seed drives the actuarial sampler family — `DIST.SAMPLE` and the
`FREQ.*.SAMPLE` functions — and the jstat generator most distributions draw
through. Two things it does NOT reach, and a model drawing through either is not
reproducible even with a seed pinned and stamped:

- Excel's own `RAND` and `RANDBETWEEN`, which are HyperFormula built-ins over
  `Math.random()` and sit outside the seeded stream entirely.
- Any library sampler, including `CHAINLADDER.BOOTSTRAPODPSAMPLE`. A library call
  runs in the actuarial runtime, which draws from its own generator, so its
  reproducibility comes from its own explicit `random_state` argument and nowhere
  else. Pass one; the workbook seed will not do it for you.

So for anything whose numbers you intend to publish: use a sampler from the seeded
family, or pass `random_state` to the library function, and record which.

Every requested
output's stats block always carries `mean`, `stdev`, `min`, `max`, `percentiles`
(populated only for requested percentile tokens, keyed by the spelling you asked
for), and `samples`; `median` is added only when requested:

```
{
  "template": { "sheets": [{ "name": "Sim", "data": [["=NORM.INV(RAND(),100,15)"]] }] },
  "outputs": { "score": "Sim!A1" },
  "simulation": { "iterations": "500", "seed": "20260728", "outputs": { "score": { "cell": "Sim!A1", "stats": ["mean","stdev"] } } },
  "failOnFormulaErrors": true
}
-> { "output": { "score": <last draw>, "simulation": { "score": { "mean": ..., "stdev": ..., "min": ..., "max": ..., "percentiles": {}, "samples": 500 }, "visualizations": { ... } }, "simulationIterations": 500, "simulationConverged": null, "simulationRuntimeMs": 42, "simulationSeed": 20260728, "simulationVectorized": true } }
```

---

## POST /v1/run-tsra — a .tsra program

TSRA (Traceable Spreadsheet Reproducible Artifact) is a small declarative language
that compiles to the same workbook `/v1/run` runs. Write named inputs and formulas
instead of a grid of `Sheet!A1` cells.

Body: `{ "source"?: string, "files"?: { <name>: string }, "data"?: {...}, "simulation"?: {...}, "failOnFormulaErrors"?: boolean, "compileOnly"?: boolean }`

- Upload one document as `source`, or a bundle as `files` (filename -> source).
  Files merge into one program, so any `input`/`let`/`output`/`func` in one file
  is visible to every other file, not just `func`s — a shared library file works.
- `data` supplies the declared inputs, by input name.
- `simulation` is the same Monte Carlo spec `/v1/run` takes, with one difference that
  matters: its `outputs[*].cell` names a DECLARATION, not a cell. You never see which
  row a `let` compiled to, so `{ "draw": { "cell": "draw", "stats": ["mean"] } }`
  points at the name you wrote. A name that was never declared fails with the list of
  declared outputs. An A1 reference is still accepted if you have inspected a
  `compileOnly` result.
- `compileOnly: true` returns `{ "compiled": {...} }` (the workbook) instead of running.
- Response is `{ "output": {...} }`, the same shape as `/v1/run`.

Statements:

- `input name: type` — a value from `data`. `type` is `number`, `string`, or `boolean`. An optional `= literal` sets a default the request can override.
- `let name = <formula>` — a named computed cell. The formula is Excel syntax and may reference any name.
- `output name` — mark a result. `output name = <formula>` defines and returns it in one line. Several outputs may be listed in one statement, comma-separated: `output a = 1, b = 2`.
- `assert <condition>, "message"` — a condition the run must not return without. If it does not hold, the run FAILS with `400 { "error": "assertion failed", "details": ["<your message> (assert <condition>)"] }` and no `output` and no `provenance`, so a script that ignores the body cannot mistake a refusal for a result. Use it for the invariants you would otherwise check by eye: `assert ABS(residual_sum) < 1e-9, "residuals do not sum to zero"`. Three things to know. The condition must be `TRUE` or `FALSE`; anything else fails too, naming what it produced, because `assert some_number` would otherwise look satisfied while asserting nothing. Every failing assertion is reported, in source order, so fixing one does not cost a round trip to find the next. And under a `simulation` it is checked on EVERY draw, so one violating iteration fails the run — strict on purpose, which also makes it the wrong tool for a condition that is only usually true. Top-level only; it is rejected inside a `func`, where a failure would become an ordinary cell error and lose your message.
- `func NAME(p: type, ...) -> type { ... }` — a reusable subfunction callable as `=NAME(args)`. The body uses `let`, `output`, and a bare `return <formula>`. A parameter may carry an inline default (`p: type = <literal>`), making it optional at the call site. Use `-> (a: type, b: type)` with `output a = ...` lines for several named returns.

Comments are `// line` and `/* block */`; whitespace is free.

Worked example — `premium = base * factor`:

```
POST /v1/run-tsra
{
  "source": "input base: number\ninput factor: number\nlet premium = base * factor\noutput premium",
  "data": { "base": 100, "factor": 1.5 }
}
-> { "output": { "premium": 150 } }
```

Worked example — a subfunction in its own file:

```
POST /v1/run-tsra
{
  "files": {
    "lib.tsra":  "func DOUBLE(x: number) -> number { return x * 2 }",
    "main.tsra": "output answer = DOUBLE(21)"
  }
}
-> { "output": { "answer": 42 } }
```

---

## Actuarial library functions

Namespaces `CHAINLADDER.*`, `ACTUAR.*`, `CASDATASETS.*`, `ACTUAFLOW.*` are backed
by a real Python + R runtime. Call them like any formula. Discover the exact
names via `GET /v1/functions?nameContains=CHAINLADDER` (etc.).

Patterns:

- Scalars are direct: `=ACTUAR.MPARETO(1, 3, 2000)` -> `1000`.
- Datasets: `=CASDATASETS.NROW("danishuni")` -> `2167`; `=CASDATASETS.LIST()` spills dataset names.
- Pipelines pass HANDLES between calls. Chain-ladder Mack IBNR total:
  `=CHAINLADDER.TOTAL(CHAINLADDER.GET(CHAINLADDER.FIT(CHAINLADDER.MACKCHAINLADDER(), CHAINLADDER.LOAD_SAMPLE("raa")), "ibnr_"))` -> `52135.23`.
- GLM pricing (actuaflow): fit, then REDUCE the spilled prediction column with a
  native aggregate:
  `=SUM(ACTUAFLOW.PREDICT(ACTUAFLOW.FIT(ACTUAFLOW.FREQUENCYMODEL(), "claims ~ age", ACTUAFLOW.DATAFRAME(A1:C13), "exposure")))`.
  A frequency model is claims per unit of exposure, so name the exposure column as
  `FIT`'s 4th argument: it enters as a log-offset, so the linear predictor targets
  the claim rate (claims per exposure) rather than a raw count. `PREDICT` then
  carries that offset back in, so the predicted column is expected counts
  (rate × exposure) and `SUM` over it is the portfolio's expected-claims total
  (it reconciles to observed claims on the training set). Here `A1:C13` is claims,
  age, exposure; drop the 4th argument only for a bare count model with no
  exposure basis.
  A library function that returns a spilled column (e.g. `ACTUAFLOW.PREDICT`)
  must be consumed by `SUM`, `COUNT`, or `INDEX` (or spilled inside a real
  workbook) — do not read its individual spilled cells as separate outputs.

If the actuarial runtime is not configured for a deployment, these calls fail
loudly (`formula error: #NA`) rather than returning wrong numbers.

### Open source attribution

Tesora Compute is built on open source work by others. The actuarial
function libraries it exposes are:

- **chainladder-python** (`CHAINLADDER.*`), by John Bogaardt and
  contributors, maintained by the Casualty Actuarial Society
  Open-Source Projects Working Group. Mozilla Public License 2.0.
  https://github.com/casact/chainladder-python

- **ActuaFlow** (`ACTUAFLOW.*`), by Michael Watson.
  Copyright (c) 2026-present Michael Watson.
  Mozilla Public License 2.0.
  https://github.com/WattyAI/actuaflow
  Cite as: Watson, M. (2025). ActuaFlow: Modern Actuarial Pricing
  Library. https://github.com/WattyAI/actuaflow

- **actuar** (`ACTUAR.*`), by Vincent Goulet and contributors.
  GNU General Public License, version 2 or later.
  https://cran.r-project.org/package=actuar

- **CASdatasets** (`CASDATASETS.*`), by Christophe Dutang and Arthur
  Charpentier, originally assembled for *Computational Actuarial
  Science with R*. GNU General Public License, version 2 or later.
  https://dutangc.github.io/CASdatasets/
  Cite as: Dutang, C. and Charpentier, A. (2026). CASdatasets:
  Insurance datasets, R package version 1.2-1. DOI 10.57745/P0KHAG

These rest in turn on the wider scientific Python and R ecosystems,
including NumPy, pandas, SciPy, statsmodels, scikit-learn, polars and
patsy. We are grateful to everyone who maintains them.

Tesora Compute is a separate work that calls these libraries. It is
not affiliated with, endorsed by, or a product of the Casualty
Actuarial Society, the R Foundation, or any author named above. Each
library remains under its own license and nothing here alters those
terms. Corrections to this notice are welcome at support@tesora.ai.

---

## Saved operations

A saved operation is a named, reusable pure-calc step (a template plus its
`inputs`/`outputs`/`functions` and a typed `inputSchema`), addressed by `slug`.
Create once, then run by slug with only the input values.

Create (`POST /v1/operations`), body:

```
{
  "slug": "premium",                     // URL-safe (lowercase, digits, hyphens)
  "name": "Premium Calc",
  "description": "base times factor",
  "step": {
    "template": { "sheets": [{ "name": "Calc", "data": [[0, 0, "=A1*B1"]] }] },
    "inputs":   { "Calc!A1": "trigger.data.base", "Calc!B1": "trigger.data.factor" },
    "outputs":  { "premium": "Calc!C1" },
    "inputSchema": {
      "base":   { "schemaType": "PRIMITIVE", "schemaItemType": "NUMBER" },
      "factor": { "schemaType": "PRIMITIVE", "schemaItemType": "NUMBER" }
    },
    "failOnFormulaErrors": true
  }
}
```

- `inputSchema` is a map of every input name -> `{ "schemaType": "PRIMITIVE"|"LIST", "schemaItemType": "NUMBER"|"INTEGER"|"STRING"|"BOOLEAN" }`. Every input referenced by a binding must be declared, and every declared input must be referenced by a binding; otherwise create returns 400.
- Run: `POST /v1/operations/premium/run` with `{ "inputs": { "base": 200, "factor": 2.5 } }` -> `{ "output": { "premium": 500 } }`. Missing/mistyped required inputs return 400; an input the operation does not read (not in its `inputSchema`/bindings) also returns 400 instead of being silently dropped: `{ "error": "unknown inputs", "details": ["this operation does not accept bogus; it accepts: base, factor."] }`.
- List/fetch responses omit any internal id; the `slug` is the only handle.
- Fetch/update/delete/run of an unknown slug -> HTTP 404 (`unknown operation "<slug>"`). Creating a slug that already exists -> HTTP 409 (`operation "<slug>" already exists`).
- Operations are scoped to the caller when the deployment configures auth: each bearer token sees and manages only its own operations, and an unauthenticated request is refused with 401. A deployment with no auth configured shares one anonymous bucket (the prior behavior).

---

## Ephemeral files — compute over large datasets

Stage a tabular file next to the engine for 24 HOURS, then bind its columns into
`/v1/run` inputs. This is how book-scale data (hundreds of thousands of rows)
reaches a computation: bound columns stream through the engine's off-grid
columnar path (a DuckDB scan), so they may exceed the in-memory grid's ~100k-row
cap. Formats: `csv` and `parquet` only, validated on upload with the same
scanner the engine uses (a mislabeled file fails the upload, not your run).

Small file (<=10 MB decoded), inline:

```
POST /v1/files
{ "name": "claims.csv", "format": "csv", "contentBase64": "<base64 bytes>" }
-> { "fileId": "<uuid>", "columns": [{"name":"claim_amount","type":"DOUBLE"}, ...],
     "rowCount": 400000, "expiresAt": "...", "reference": "..." }
```

Larger file (up to 512 MB): mint a single-use streaming PUT, then run the
returned `curl` command (the bytes never traverse the minting request):

```
POST /v1/files/upload-urls   { "name": "book.parquet", "format": "parquet" }
-> { "fileId", "uploadUrl", "header": "x-compute-upload-token", "token",
     "tokenExpiresAt", "maxBytes", "curl" }
PUT <uploadUrl>  (raw bytes; token in the x-compute-upload-token header)
-> the same committed file record as an inline upload
```

Then reference columns by binding a sheet RANGE input (header row above; the
range starts at the data row, and the bound column reads its full on-disk
extent, so the declared height is a label rather than a row count):

```
POST /v1/run
{
  "template": { "sheets": [
    { "name": "Data", "data": [["claim_amount"], [""]] },
    { "name": "Calc", "data": [["=SUM(Data!A2:A400001)"]] } ] },
  "inputs": { "Data!A2:A400001": "file.<fileId>.claim_amount" },
  "outputs": { "total": "Calc!A1" }
}
```

Rules:

- The upload response's `columns` are the ONLY valid reference targets (column names may contain dots; the reference is `file.<fileId>.<everything after the id>`).
- A column can also be bound by CONTENT: `"file.sha256:<hex>.claim_amount"`, using the `contentHash` the upload returned. It resolves to whichever of your live uploads holds those bytes (the newest, if several do). Prefer it in anything you intend to replay: a fileId is minted per upload and will not exist for whoever re-runs your request, and the `provenance.requestHash` is taken over the content hash either way, so a replay over re-uploaded identical bytes reproduces the recorded hash instead of a new one.
- Files expire 24 hours after upload and their bytes are deleted; a run must START before the file's `expiresAt` (an admitted run always finishes). An unknown/expired reference fails the run with 400 before any compute.
- Files are private to the uploader (with auth configured, all `/v1/files*` routes require a bearer token; another caller's fileId behaves like a nonexistent one). Uploads are quota-bounded per caller (default 20 live files / 2 GiB).
- `GET /v1/files` recovers fileIds and remaining lifetimes; `DELETE /v1/files/:fileId` frees quota early (runs already admitted still finish).
- Saved operations (`POST /v1/operations`) reject `file.` references at save time: a saved operation is persistent, an upload is not. Use `/v1/run` for file-backed computations.
- The declared range height is a LABEL, not a row count. A bound column always reads its full on-disk extent, so over-declaring (`Data!B2:B95` over a 37-row column) does not pad it and under-declaring does not truncate it. `SUM`, `COUNT` and a coverage `SUMPRODUCT` all answer over the same 37 rows.
- What DOES change an answer is a BLANK FIELD inside the column, which is a property of the data and not of the binding. A blank stays blank, and an Excel comparison ranks a blank as 0. So over a column of 94 rows with 57 blanks, `SUMPRODUCT(--(col>=lo),--(col<=hi))` returns 94 whenever 0 falls inside `[lo,hi]`, while `COUNT` returns 37 — a coverage ratio of 2.54 rather than something at most 1. That is Excel's own behaviour, reproduced deliberately, not an engine defect.
- Guard any coverage-style formula over a possibly-ragged column with `ISNUMBER`: `SUMPRODUCT(--ISNUMBER(col),--(col>=lo),--(col<=hi))`. `COUNT` skips blanks, and so does `COUNTIF` under a comparison criterion like `"<=60"`, which is why either disagrees with an unguarded `SUMPRODUCT` over the same column. If a count and a ratio disagree, suspect blanks in the data before suspecting the binding.

---

## POST /v1/feedback — tell us what broke

This API is used mostly by agents, and an agent that hits a bug has nowhere to
put it: it works around the problem and the context that made the report useful
is gone by the end of the session. This endpoint is that place. File it at the
moment of the problem, not at the end.

Body: `{ "summary": string (required), "details"?: string, "kind"?: "bug"|"limitation"|"docs"|"idea", "severity"?: "low"|"medium"|"high", "surface"?: string }`

- `kind` defaults to `bug`. `limitation` = it works but cannot express your task; `docs` = a description here misled you; `idea` = a capability worth having.
- `surface` names the route or tool the report is about, e.g. `"/v1/run"`.
- Be specific in `summary`: "SUMPRODUCT over a bound parquet column counts padding rows" is actionable, "compute is broken" is not. Put the reproduction in `details`.
- Report what the platform got wrong, not your own mistakes.
- A report leaves this service: it is written to the log and may be posted to a team channel. Describe the SHAPE of the failure in `details` (the formula, the column names and types, the range, the row count, where the blanks fall) and not the contents of the data. No cell values, no rows from an uploaded file, no customer or claimant identifiers. Where the failure depends on the data, name the property that matters or reproduce it over synthetic rows. Nothing is redacted for you, and a report that names the shape is the more useful report anyway.

```
POST /v1/feedback
{
  "summary": "compute_run rejects a range bound to a parquet column of dates",
  "details": "Binding Data!A2:A500 to file.<id>.effective_date returns 400 ...",
  "kind": "bug",
  "surface": "/v1/run"
}
-> { "received": true, "delivery": ["log", "slack"] }
```

`delivery` names the sinks that accepted the report, so you can tell whether a
human will see it: `log` always, `slack` only where a webhook is configured.
Rate-limited to 20/min.

---

## Verification stamp

Every `/v1/run` and `/v1/run-tsra` result carries `provenance`:

```
"provenance": { "requestHash": "<64 hex>", "engineVersion": "<16 hex>" }
```

- `requestHash` is a digest over the canonicalized computation (template, bindings, outputs, declared types, simulation spec, inline functions, data) PLUS the SHA-256 of every bound ephemeral file. Re-running the same computation reproduces it; changing the data behind a `file.` reference changes it even though the request text is identical.
- `engineVersion` is opaque and stable per engine build: two results with the same token came from the same build. It reveals neither the engine source hash nor the commit.
- Key order does not affect the hash. An input you omit that has a declared default hashes the same as passing it explicitly.
- The hash names the BYTES a `file.` reference read, not the upload. A fileId is minted per upload, so re-sending the same CSV mints a new one; the digest substitutes that file's content hash first, so re-uploading identical data and replaying the same request reproduces the hash rather than producing a fresh one. `compute_upload_file` returns `contentHash`, and `file.sha256:<hex>.<column>` binds by it directly, resolving to whichever of your live uploads holds those bytes.

If you are shipping a reproducible artifact, record both alongside the expected
outputs, and write the `file.sha256:` form into the artifact rather than a fileId
that will not exist when someone replays it. That turns "I claim these numbers"
into a claim someone else can re-check. `compileOnly` responses carry no stamp,
having computed nothing.

---

## Errors

Errors say what went wrong and how to fix it. Only genuine internals are dropped (the synthetic step wrapper, stack frames, and internal locators like filesystem paths/URLs, redacted to `<path>`); the caller-actionable detail is kept:

- A formula error -> the offending cell, its Excel error code, and how to proceed, e.g. `{ "error": "formula errors in 1 output cell(s):\n  - Eval!A1: #DIV/0!\n\nAn error that reaches an output fails the step. Wrap it in IFERROR or set failOnFormulaErrors: false to tolerate it." }`. The Excel codes: `#DIV/0!`, `#GETTING_DATA`, `#UNKNOWN!`, `#SPILL!`, `#VALUE!`, `#FIELD!`, `#NAME?`, `#NULL!`, `#CALC!`, `#NUM!`, `#REF!`, `#N/A`, `#NA`.
- A malformed field (a bad `functions`/`simulation`/`outputSchema`/`inputs` shape) -> `{ "error": "invalid request: <field.path>: <reason>; ..." }`, naming each field and why.
- A failure with nothing intelligible to report -> `{ "error": "computation failed" }`.
- An unexpected server error -> HTTP 500 `{ "error": "internal error" }`.
- EXCEPTION (opt-in): a `/v1/run` or saved-operation `/run` called with an explicit `"failOnFormulaErrors": false` returns HTTP 200 instead of failing. Each errored output holds its bare Excel error code, and `output.formulaErrors` maps each errored cell reference to a diagnostic like `{ "error": "#DIV/0!", "message": "The formula divides by zero or by an empty cell.", "formula": "=A1/B1" }`: the Excel code, a plain explanation of it, and the formula echoed from your submitted template. When the underlying error carries none of the recognized Excel codes, `error` is the generic sentinel `#ERROR` (not a real Excel code) and `message` is omitted; `formula` is omitted when the cell holds no formula in your template. The engine's own error prose is never surfaced, so no platform internals leak. Omit the field or set it `true` (the default) to keep the fail-closed behavior (a sanitized 400).
- Request-validation failures (bad body/slug/inputs) -> HTTP 400 with a descriptive `error` and a `details` array naming exactly which field to change and how.

## Caching

`POST /v1/evaluate` and `POST /v1/run` cache their result for one hour, keyed by
a canonicalized hash of the request body (object keys are order-insensitive; array
order is kept since it's significant to the calculation) and isolated per caller
(by authenticated user id, else by source IP), so one caller never sees another's
cached result. A repeated identical request is served from that cache instantly
and does NOT count against your rate limit, which makes re-running the same
calculation effectively free. Caching is on by default; send `"cache": false` in
the body to force a fresh computation that neither reads nor writes the cache
(and does count against the limit). Saved operations (`/v1/operations/:slug/run`)
are not cached, since their stored step can change; a pathologically deeply
nested body is also left uncached (and computes normally) rather than risking the
stack. A cacheable-route response carries `x-compute-cache: hit` or `miss`; a
saved-operation response, a `"cache": false` request, or one whose body was too
deeply nested to key, carries no such header at all.

## Limits

- Per-IP rate limits (a stricter budget on compute routes than the global default). Requests served from the cache do not count against these limits.
- Request bodies are capped (default 8 MiB).
- Each computation has a wall-clock deadline; exceeding it returns HTTP 503.

## Discovery

Always prefer live discovery of function names and categories:

```
GET /v1/functions                       -> { "total": <n>, "functions": [{ "name": ... }] }
GET /v1/functions?nameContains=GLM      -> any name containing "GLM" (case-insensitive substring: GLM.FIT, ACTUAFLOW.FREQUENCYGLM, ...), not just the GLM.* namespace
GET /v1/functions?category=frequency    -> only that category
```
