Skip to content

Commit d181b4f

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-18181-carrier-is-the-seats-to-hang
2 parents 763aefa + 75237a9 commit d181b4f

22 files changed

Lines changed: 911 additions & 59 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
`try_catch`'s catch-region binding is annotated as the plain declared type. `TryCatchErrorValueSchema` declares `code: z.string().optional()`, so the local `TryCatchErrorValue & { code?: string }` intersection in `builtin/try-catch-node.ts` added nothing the exported `TryCatchErrorValue` did not already carry, and the comment paragraph beside it explained a spec/engine divergence that no longer exists (#15669).
6+
7+
**No behaviour change, and nothing executable moves.** The object literal is untouched: `nodeId`, `message`, `code` and `iteration` / `item` are bound under exactly the same conditions as before, so a catch region still branches on `{$error.code}` and still reads an absent `code` as "no classified code", never as "nothing failed". Measured on the built package: `index.js`, `index.cjs`, `index.d.ts` and `index.d.cts` are **byte-identical** before and after; only `index.js.map` / `index.cjs.map` shift (by one byte each), because the replacement comment is two lines longer and the sourcemap encodes line positions.
8+
9+
The annotation was proven redundant before it was removed — `TryCatchErrorValue` and `TryCatchErrorValue & { code?: string }` are mutually assignable, and `TryCatchErrorValue['code']` is exactly `string | undefined` — and the binding it describes is genuinely pinned: dropping `code` from the literal reddens the two `#14419` discriminator tests in `builtin/create-record-duplicate-code.test.ts`.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/core": minor
4+
"@objectstack/types": patch
5+
"@objectstack/rest": patch
6+
---
7+
8+
fix(spec)!: `timeDimensions[].dateRange`'s array arm is exactly two string bounds, and each refusal ORIGIN gets a true sentence (#17598; ruling A, decision batch #117 item 3)
9+
10+
<!-- adr-0087: registered analytics-date-range-array-two-bounds-required -->
11+
12+
**BREAKING** accept-set narrowing at `timeDimensions[].dateRange` — shipped as
13+
`minor` under this repo's launch-window convention for breaking changes
14+
(`scripts/check-changeset-no-major.mjs`), above the `patch` floor the `fix`
15+
commit type sets, and the same grade the one comparable precedent took: the
16+
STRING-arm closing on this same schema is #16041, and it shipped
17+
`"@objectstack/spec": minor` (`packages/spec/CHANGELOG.md` 17.4.0, under Minor
18+
Changes). ⚠️ Its driver half #16322 declares `"@objectstack/spec": patch`, but
19+
that entry is — in that changeset's own words — "a `PROVENANCE_WAIVERS` row
20+
only", not an accept-set narrowing, so it is not a grade this one is measured
21+
against. The maintainer
22+
ruling calls it a "major changeset"; under the launch window that phrase maps to
23+
the protocol MAJOR the migration registers against (18), not to the changeset's
24+
bump level, which `scripts/check-changeset-no-major.mjs` reserves. The semantic
25+
prescription is registered under protocol major 18 as
26+
`analytics-date-range-array-two-bounds-required`.
27+
28+
### What changed
29+
30+
`AnalyticsDateRangeSchema`'s array arm was `z.array(z.string())` with **no length
31+
constraint**, so `['2026-01-01']`, `[]` and `['a', 'b', 'c']` were schema-valid.
32+
It is now `z.tuple([z.string(), z.string()])` — a tuple rather than a length
33+
refinement, so the arity is stated to the author's compiler before any parse runs.
34+
Preset names, two-bound windows and an absent `dateRange` parse byte-identically
35+
to before.
36+
37+
`analyticsDateRangeRefusalMessage(input)` becomes
38+
`analyticsDateRangeRefusalMessage(input, origin)`, where `origin` is `'schema'` or
39+
`'runtime'` and is **required** — there is deliberately no default.
40+
41+
### Migration: FROM → TO
42+
43+
| You wrote | Write instead |
44+
| --- | --- |
45+
| `dateRange: ['2026-01-20']` | `dateRange: ['2026-01-20', '2026-01-20']` — a single day is that day as both bounds, the shape the shipped #16322 table already prescribes |
46+
| `dateRange: []` | no conversion. An empty array names no window: write the two bounds the widget was meant to show, or omit `dateRange` (it is optional, and absent means the query is not time-bounded) |
47+
| `dateRange: ['a', 'b', 'c']` | no conversion. Decide which two bounds you meant and write them |
48+
| `analyticsDateRangeRefusalMessage(value)` | `analyticsDateRangeRefusalMessage(value, 'schema')` at a parse door, `…(value, 'runtime')` past one |
49+
50+
`os migrate meta --from 17` emits the first three as a structured TODO rather than
51+
rewriting them: rewriting a one-element array to the same day twice at load would
52+
be the platform deciding, silently, that the author meant one day rather than a
53+
window whose end they forgot, and for the other two shapes there is nothing to
54+
decide from.
55+
56+
### Why it is not a new class of breakage
57+
58+
Since PR #17593 all four analytics faces (`ObjectQLStrategy`, `NativeSQLStrategy`,
59+
the draft-preview evaluator, `DatasetExecutor.runCompare`) already refused anything
60+
that is not exactly two bounds with `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED`, so
61+
every stored range this narrowing refuses was **already failing at query time**.
62+
The contract door was looser than every reader behind it; this moves the refusal
63+
to authoring time and states it accurately. Blast radius is the WIDGET, not the
64+
page: a stored dashboard carrying a now-refused range loses that widget with the
65+
refusal shown and still loads.
66+
67+
### The wording half
68+
69+
The shared sentence ended `"Refused at the schema"` and described every refused
70+
array as `"received an array with a non-string bound"`. For a one-element window
71+
refused by a face **both clauses were false** — every bound present is a string,
72+
and it was refused past the schema, not at it — which is why
73+
`@objectstack/service-analytics` had to overwrite the message rather than reuse it,
74+
leaving one condition with two wordings. The origin is now a parameter and the
75+
`received …` clause names the arity and the bad bound separately, so the sentence
76+
is true for each origin both before and after the arm narrows.
77+
78+
The same rule reaches the WIRE. Narrowing the arm to a tuple gave the union a
79+
second voice: its arm answers `Too small: expected array to have >=2 items` for
80+
the very arity the prescription just prescribed, and the ADR-0114 union
81+
expansion emitted both as `fields[]` entries on `POST /analytics/query` and
82+
`POST /analytics/dataset/query`. `fieldsFromZodIssues` (`@objectstack/types`),
83+
the one mapper both doors report through, now drops the branch issues that land
84+
at the union's OWN path for this refusal — recognised structurally through
85+
`isAnalyticsDateRangeRefusalIssue`, never by message prose. A refusal that names
86+
a DEEPER position keeps it: `dateRange: ['2026-01-01', 3]` still reports
87+
`timeDimensions.0.dateRange.1`, because WHICH bound is not a string is a
88+
location the prescription does not carry. Every other union expands exactly as
89+
before. Client-visible effect: one `fields[]` entry for an arity refusal instead
90+
of two, with the prescriptive one kept.

‎content/docs/references/api/analytics.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const result = AnalyticsEndpoint.parse(data);
8484
| **measures** | `string[]` | ✅ | List of metrics to calculate |
8585
| **dimensions** | `string[]` | optional | List of dimensions to group by |
8686
| **where** | `any` | optional | Filtering criteria (canonical Query DSL FilterCondition). An authored `FilterArray` is lowered by `parseFilterAST` on the client before the wire; this field admits only the lowered `FilterCondition` (see `FilterArray` in `data/filter.zod.ts`). |
87-
| **timeDimensions** | `{ dimension: string; granularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; dateRange?: Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| …> \| string[] }[]` | optional | Time-bucketed dimensions. Each entry names a dimension, an optional bucket `granularity`, and an optional `dateRange` — a preset name from the closed date-range vocabulary (e.g. `'last_7_days'`) or an explicit `[start, end]` window; an unrecognised string answers `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED` instead of silently widening. |
87+
| **timeDimensions** | `{ dimension: string; granularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; dateRange?: Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| …> \| [string, string] }[]` | optional | Time-bucketed dimensions. Each entry names a dimension, an optional bucket `granularity`, and an optional `dateRange` — a preset name from the closed date-range vocabulary (e.g. `'last_7_days'`) or an explicit `[start, end]` window; an unrecognised string answers `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED` instead of silently widening. |
8888
| **order** | `Record<string, Enum<'asc' \| 'desc'>>` | optional | |
8989
| **limit** | `number` | optional | |
9090
| **offset** | `number` | optional | |
@@ -98,7 +98,7 @@ const result = AnalyticsEndpoint.parse(data);
9898
| :--- | :--- | :--- | :--- |
9999
| **dimension** | `string` | ✅ | |
100100
| **granularity** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | |
101-
| **dateRange** | `Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| …> \| string[]` | optional | Time window for this dimension: a date-range PRESET name from the closed vocabulary in `data/date-range-presets.ts` (today, yesterday, this_week, last_week, this_month, last_month, this_quarter, last_quarter, this_year, last_year, last_7_days, last_30_days, last_90_days — e.g. `'last_7_days'`), or an explicit `[start, end]` array of ISO dates / `{date-macro}` tokens (e.g. `["2023-01-01", "2023-01-31"]`). Any other string is refused at the schema with `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED`. |
101+
| **dateRange** | `Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| …> \| [string, string]` | optional | Time window for this dimension: a date-range PRESET name from the closed vocabulary in `data/date-range-presets.ts` (today, yesterday, this_week, last_week, this_month, last_month, this_quarter, last_quarter, this_year, last_year, last_7_days, last_30_days, last_90_days — e.g. `'last_7_days'`), or an explicit `[start, end]` array of ISO dates / `{date-macro}` tokens (e.g. `["2023-01-01", "2023-01-31"]`). Any other string is refused at the schema with `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED`. |
102102

103103

104104
---

‎content/docs/references/data/analytics.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Allowed Values: `today`, `yesterday`, `this_week`, `last_week`, `this_month`, `l
6060

6161
#### Option 2
6262

63-
Type: `string[]`
63+
Type: `[string, string]`
6464

6565
---
6666

@@ -98,7 +98,7 @@ Type: `string[]`
9898
| **measures** | `string[]` | ✅ | List of metrics to calculate |
9999
| **dimensions** | `string[]` | optional | List of dimensions to group by |
100100
| **where** | `any` | optional | Filtering criteria (canonical Query DSL FilterCondition). An authored `FilterArray` is lowered by `parseFilterAST` on the client before the wire; this field admits only the lowered `FilterCondition` (see `FilterArray` in `data/filter.zod.ts`). |
101-
| **timeDimensions** | `{ dimension: string; granularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; dateRange?: Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| …> \| string[] }[]` | optional | Time-bucketed dimensions. Each entry names a dimension, an optional bucket `granularity`, and an optional `dateRange` — a preset name from the closed date-range vocabulary (e.g. `'last_7_days'`) or an explicit `[start, end]` window; an unrecognised string answers `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED` instead of silently widening. |
101+
| **timeDimensions** | `{ dimension: string; granularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; dateRange?: Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| …> \| [string, string] }[]` | optional | Time-bucketed dimensions. Each entry names a dimension, an optional bucket `granularity`, and an optional `dateRange` — a preset name from the closed date-range vocabulary (e.g. `'last_7_days'`) or an explicit `[start, end]` window; an unrecognised string answers `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED` instead of silently widening. |
102102
| **order** | `Record<string, Enum<'asc' \| 'desc'>>` | optional | |
103103
| **limit** | `number` | optional | |
104104
| **offset** | `number` | optional | |
@@ -110,7 +110,7 @@ Type: `string[]`
110110
| :--- | :--- | :--- | :--- |
111111
| **dimension** | `string` | ✅ | |
112112
| **granularity** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | |
113-
| **dateRange** | `Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| …> \| string[]` | optional | Time window for this dimension: a date-range PRESET name from the closed vocabulary in `data/date-range-presets.ts` (today, yesterday, this_week, last_week, this_month, last_month, this_quarter, last_quarter, this_year, last_year, last_7_days, last_30_days, last_90_days — e.g. `'last_7_days'`), or an explicit `[start, end]` array of ISO dates / `{date-macro}` tokens (e.g. `["2023-01-01", "2023-01-31"]`). Any other string is refused at the schema with `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED`. |
113+
| **dateRange** | `Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| …> \| [string, string]` | optional | Time window for this dimension: a date-range PRESET name from the closed vocabulary in `data/date-range-presets.ts` (today, yesterday, this_week, last_week, this_month, last_month, this_quarter, last_quarter, this_year, last_year, last_7_days, last_30_days, last_90_days — e.g. `'last_7_days'`), or an explicit `[start, end]` array of ISO dates / `{date-macro}` tokens (e.g. `["2023-01-01", "2023-01-31"]`). Any other string is refused at the schema with `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED`. |
114114

115115

116116
---

0 commit comments

Comments
 (0)