From 0bedb4332b4a4da0a3e6ce1fa66bad8926d4e3fa Mon Sep 17 00:00:00 2001 From: delchev Date: Mon, 24 Aug 2026 11:05:31 +0300 Subject: [PATCH] docs(spec): a report may declare user-set parameters A report is usually read for a period, a threshold or a name the reader chooses, and filter is fixed when the report is generated. Documents reports[].parameters on the presentation page and in the reference: the key table, the bound-on-every-read contract that makes initial the value the untouched report shows, which comparisons carry a neutral default and which must declare one, and what a target may be. Co-Authored-By: Claude Opus 5 --- docs/reference.md | 5 ++++- docs/spec/presentation.md | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/reference.md b/docs/reference.md index 913c4fb..a521b3d 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -38,7 +38,7 @@ The quick lookup surface: one line and a minimal snippet per construct. For rule | [`actions`](/spec/processes#actions-custom-buttons) | developer-defined buttons opening custom pages | | [`view`](/spec/presentation#view-calendar-range-slots) | an additional calendar / range page, or a slot-booking page | | [`documentItemsLayout: chat`](/spec/presentation#documentitemslayout-chat-conversation-threads) | render a document's items as a chat thread | -| [`reports`](/spec/presentation#reports) | aggregations, charts, dashboard KPI tiles, balance reports | +| [`reports`](/spec/presentation#reports) | aggregations, charts, dashboard KPI tiles, balance reports, user-set parameters | | [`scope`](/spec/presentation#lifecycle-scope) | which lifecycle rows an aggregating report counts | | [`widgets`](/spec/presentation#widgets-custom-dashboard-tiles) | custom KPI / embedded-page dashboard tiles | | [`notify.forEach`](/spec/glue#one-message-per-related-row-foreach) | fan the block out over a related collection: one message per row, every bare path resolved against the row | @@ -141,6 +141,9 @@ reports: measures: ["count(*)", "sum(total)"] filter: "total > 0" scope: live # all | draft | live | cancelled | void + parameters: # user-set inputs bound into the WHERE + - { name: fromDate, target: orderDate, op: ge } + - { name: minTotal, target: total, op: ge, initial: "0" } chart: bar widget: { value: "sum(total)", at: { "month(orderDate)": now }, label: Revenue (this month) } ``` diff --git a/docs/spec/presentation.md b/docs/spec/presentation.md index b386a10..68d9da1 100644 --- a/docs/spec/presentation.md +++ b/docs/spec/presentation.md @@ -63,6 +63,39 @@ With no `scope`, a report counts every row **except** when all of the following A report that aggregates over a lifecycle-carrying source while declaring no `scope`, filtering on no status, and resolving no stage classification is the case this construct exists to eliminate: a generator MUST report it as a diagnostic naming the report and its status relation. Emitting the unrestricted aggregation silently is non-conforming. ::: +### Parameters + +A report is usually read for a period, a threshold or a name the reader chooses. `filter` cannot express that — it is fixed when the report is generated — so `parameters` declares the report's own inputs: + +```yaml +reports: + - name: Revenue + source: Invoice + dimensions: [date, customer.name] + measures: ["sum(total)"] + parameters: + - { name: fromDate, target: date, op: ge } # a From picker + - { name: toDate, target: date, op: le } # a To picker + - { name: minTotal, target: total, op: ge, initial: "0" } # an amount threshold + - { name: customer, target: customer.name, op: like } # a name search +``` + +Each entry becomes an input rendered above the report, whose value is bound into the report's `WHERE`: + +| key | meaning | +| --- | ------- | +| `name` | the input's label source and the name its value is sent under | +| `target` | the field it filters: a field of the source, or a one-hop `relation.field` path — joined exactly as a dimension is, so a report may be filtered by a column it does not display | +| `op` | `ge`, `le`, `eq` or `like`; `like` matches anywhere in the value | +| `type` | optional `date` / `timestamp` / `number` / `string` — the target field already types the parameter, so this is a declaration checked against it, never a conversion | +| `initial` | the value bound when the input is left empty — what the report shows before the reader touches it | + +A parameter is bound on **every** read, so `initial` is what makes the untouched report the unfiltered one. Two comparisons have a neutral "any value" default and therefore need no `initial` — a date `ge`/`le` bound (widened to all time) and a `like` search (the empty pattern, which matches every value). An `eq` selector and a numeric bound have none: the declaration must say what the report opens with. + +A row holding no value in the target column is still in the report while the input is empty; once a value is set, that row is outside the filter. A `timestamp` target is compared as a date, so a `le` bound includes the whole day chosen. + +The target is a **field**: a relation itself is not one (name a field of it), and boolean and long-text fields are not parameterizable. A balance report declares its own `fromDate`/`toDate` window and may add further parameters, but not redeclare those two. + ### Chart `chart:` renders the report page as a chart instead of a table (the page keeps a table / chart toggle, so filters, export and print still work). A chart wants exactly one dimension and one or more measures — the dimension labels the axis and each measure becomes a series: