Skip to content

Commit 1303a04

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-17985-authz-false-green-sweep
2 parents 73358cd + 226970b commit 1303a04

81 files changed

Lines changed: 5811 additions & 1296 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/16236-formula-return-type-measure-column.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@
44

55
fix(service-analytics): a `min`/`max` over a `formula` field is typed from the formula's declared `returnType`, not described as `number` (#16236)
66

7+
> ⚠️ **Superseded within the same release window — ⛔ do not act on this entry.**
8+
> Everything below was accurate when it was written and is kept as the record of what
9+
> #16236 measured and built. It never reached a published version: **#17560** (director
10+
> ruling, decision batch #127, 2026-09-13) refuses `min` / `max` over a `formula` field
11+
> outright, on the compatibility table's own storage ground — a formula is VIRTUAL in SQL
12+
> storage, no column is emitted, so no aggregate can be lowered to it whatever
13+
> `returnType` says. At the version that compiles this entry such a measure answers
14+
> `DATASET_INVALID` / **400** at compile time instead of carrying any `fields[].type`, and
15+
> the `returnType?: string` member described at the foot of this entry is **not** on
16+
> `AnalyticsServiceConfig.sourceFieldMeta` — it was added and removed inside one release
17+
> window, so no published version ever carried it. ⇒ Read #17560's entry instead; the
18+
> FROM → TO below never became a shipped behaviour.
19+
720
**Behaviour change — read this if any dataset measure aggregates a `formula`
821
field.** `AnalyticsResult.fields[].type` for such a measure column was always
922
`number`, whatever the formula computes. It is now translated from the field's
@@ -47,3 +60,8 @@ a word outside the declared four: left alone, never guessed at.
4760
host that returns the three-member shape still satisfies the contract and gets
4861
exactly today's behaviour for every column. `AnalyticsServicePlugin` relays the
4962
key automatically, so a host on the plugin needs no change at all.
63+
64+
⚠️ **Superseded — see the banner at the top.** #17560 removed that member again in
65+
the same release window, so the shape a host writes against is the three-member one
66+
this paragraph calls today's. Nothing to do either way: a host that returns the
67+
fourth key is ignored, not refused.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: `ListViewSchema.sort` retires the bare string clause — the PRODUCER half of the sort seam, so the contract stops minting documents its own consumer refuses (#17053; objectui#8221, decision batch #77 option B)
6+
7+
<!-- adr-0087: registered list-view-sort-string-clause-to-array -->
8+
9+
**BREAKING** accept-set narrowing at `view.sort` — the list-view doors
10+
(`ListViewSchema`, and the `ObjectListViewSchema` copy behind `object.list` /
11+
`object.listViews.*`) — shipped as `minor` under this repo's launch-window
12+
convention for breaking changes, the same grade its sibling
13+
`object-block-sort-item-array` took for the two `ComponentPropsMap` doors. The
14+
mechanical prescription is registered under protocol major 18 as
15+
`list-view-sort-string-clause-to-array`.
16+
17+
**Why this is graded on the seam, not on the string.** objectui ruled one sort
18+
orthography platform-wide — the array (objectui#8221, decision batch #77,
19+
2026-09-07, option B) — and objectui PR #8758 executes it: `convertSortToQueryParams`
20+
refuses a runtime string and its diagnostic names the array form. `ListViewSchema`
21+
is the producer of exactly those documents: `object.list.sort` is what
22+
`deriveRelatedLists` reads. So until this release a view authored with
23+
`sort: 'created_at desc'` **validated here, cleanly, and then failed downstream** —
24+
the contract minting a shape its consumer rejects, with the author told off by
25+
the wrong layer. Re-measured on this tree before the change, with `bogusProp`
26+
refused by name on the same call as the firing control: `'name desc'`, `'-name'`
27+
and the array form all returned `success: true`, and only a bare number was
28+
refused (`sort/invalid_union`).
29+
30+
`sort` survives as a key, one union arm lighter, so this is a VALUE narrowing with
31+
no `retiredKey()` tombstone to hang a prescription on. The surviving array member's
32+
own `error` map carries it, keyed on `issue.input` being a string — the same shape
33+
`view.type`'s retired `'page'` value and `view.exportOptions`' retired `'pdf'` value
34+
already use in this schema. Every other invalid value (a number, an object, a
35+
string reaching a *descendant* such as a misspelled `order`) keeps zod's default
36+
report, so nobody is told a clause they never wrote "was removed".
37+
38+
**Migration** (`list-view-sort-string-clause-to-array`, a D2 conversion, not a
39+
semantic TODO — the rewrite is lossless and wholly mechanical):
40+
`sort: 'created_at desc'` becomes `sort: [{ field: 'created_at', order: 'desc' }]`;
41+
a bare field name meant ascending, so `sort: 'created_at'` becomes
42+
`sort: [{ field: 'created_at', order: 'asc' }]` — `order` is required on the entry
43+
and is written out rather than omitted; a comma-separated clause becomes one array
44+
entry per key, in the same order. `os migrate meta --from 17` lists these edits for
45+
author sources, and stored rows replay them through `applyConversionsToStoredItem`.
46+
47+
**The narrowing was not free, and the population was measured rather than assumed.**
48+
A tree-wide census over both the TS and JSON spellings of a string-valued `sort`,
49+
read as STRUCTURES rather than counted as tokens, found the clause authored on
50+
three live in-tree sites, all converted here: the shipped showcase list view
51+
`examples/app-showcase/src/ui/views/task.view.ts` (`'estimate_hours desc'`, carried
52+
since objectui#2601 as a deliberate live coverage fixture for the string form), the
53+
frozen `packages/lint` snapshot of that same shipped shape, and the published
54+
`skills/objectstack-ui` list-view rule. The census fired: it *found* documents, and
55+
`tsc` independently reds on the first two the moment the arm is removed. Sites
56+
deliberately NOT converted, having been read rather than grepped: ObjectQL
57+
`query.sort` and the wire `normalizeSortNodes` (different doors, different
58+
dialects), `packages/spec`'s `book`/`doc` field-mapping records whose `sort: 'order'`
59+
is an unrelated key of the same name, and the `packages/lint` rule fixtures, which
60+
feed the PRE-parse walker and never reach this schema.
61+
62+
**Not moved by this release.** `RecordRelatedListProps.sort` keeps its declared
63+
string arm. That string is the `'field'` / `'-field'` dialect normalised by
64+
objectui's own `RelatedList.normalizeSortSpec`; it never reaches
65+
`convertSortToQueryParams`, and retiring it was not ruled. For the same reason the
66+
conversion above declines any clause that does not parse as `<field> [asc|desc]`:
67+
guessing a direction for `'-name'` would invent an ordering the author never wrote,
68+
so on a list view it meets the door's prescription instead.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
---
2+
"@objectstack/service-analytics": minor
3+
"@objectstack/spec": minor
4+
---
5+
6+
feat(service-analytics)!: `min` and `max` are judged by the aggregate × field-type table too — all 74 refused pairs answer `400 DATASET_INVALID` through one compile door (#17560)
7+
8+
<!-- adr-0087: registered dataset-measure-selecting-aggregate-field-type-refused -->
9+
10+
**BREAKING** — an accept-set narrowing on a published authoring surface, and the last
11+
one this table owed. A dataset measure pairing `aggregate: 'min'` (or `'max'`) with any
12+
of the **37** field types outside the numeric, temporal and boolean classes — for example
13+
`text`, `select`, `lookup`, `autonumber`, `json`, `multiselect`, `file`, `location`,
14+
`vector` or `formula`; the ADR-0087 entry registered below carries the full list — used to
15+
compile and reach the backend; it is now refused by
16+
`compileDataset` with `DATASET_INVALID` / **400** before any query is built. Shipped as
17+
`minor` under the repo's launch-window convention for accept-set narrowings.
18+
19+
⛔ This changeset adds no rows to any table and restates none. The verdict is
20+
`AGGREGATE_FIELD_TYPE_COMPATIBILITY`'s — the one table `@objectstack/spec` declared in
21+
#16353 under the director ruling of decision batch #59 ("both legs, table in spec") —
22+
read through `isAggregateCompatibleWithFieldType`.
23+
24+
## What was wrong
25+
26+
The table refused these 74 pairs from the day it was declared, and **four declarations
27+
gave three different answers about them**:
28+
29+
| declaration | what it said about `min` × `text` |
30+
|---|---|
31+
| `AGGREGATE_FIELD_TYPE_COMPATIBILITY` (spec) | refused |
32+
| `dataset-compiler`'s compile leg | never judged — `if (!DERIVING_AGGREGATES.has(aggregate)) return;` |
33+
| `measureResultType` (service-analytics, #15768) | a supported `'string'` result |
34+
| two shipped test files, in prose | "ruled C — the table is to be AMENDED to accept it" |
35+
36+
Driven through the real service door before anything was written, `min` / `max` over 13
37+
sampled refused pairs all compiled and emitted SQL, with `avg` × `datetime` as the
38+
firing control (refused, `DATASET_INVALID` / 400, no SQL) — so the zero was a reading of
39+
the tree rather than of a blind harness.
40+
41+
The fourth row had nothing behind it. The card it cited (#17513) is closed as a
42+
duplicate carrying zero rulings, and the one recorded ruling on this table says the
43+
opposite. ⇒ The director ruling of decision batch #127 (2026-09-13) settled all three
44+
sub-questions in one pass, because one shared fixture drove members of both halves:
45+
46+
1. **the string classes** (42 pairs) stay refused, as batch #59 ruled — ⛔ the table is
47+
not amended;
48+
2. **the non-string classes** (32 pairs) are refused **and enforced**;
49+
3. **`formula`** is refused on the table's own storage ground — it is VIRTUAL in SQL
50+
storage, no column is emitted, so no aggregate can be lowered to it whatever
51+
`returnType` says.
52+
53+
The divergence is real, and for these two aggregates it is the **ORDER** rather than the
54+
arithmetic: string order is collation-dependent, so two backends answer two different
55+
"smallest" values for one metadata document, and `min(jsonb)` does not exist on
56+
PostgreSQL at all.
57+
58+
## What changed
59+
60+
- **`dataset-compiler`**: the scope condition is gone. `assertAggregateFieldTypeCompatible`
61+
judges all six `AggregationFunction` members against the table, through the same
62+
`DATASET_INVALID` / 400 door. The refusal message names the divergence its own
63+
aggregate class really has (`min` / `max` SELECT a stored value and diverge on order;
64+
`sum` / `avg` DERIVE a number and diverge on arithmetic) and prescribes accordingly.
65+
- **`measureResultType`** asks `isAggregateCompatibleWithFieldType` before it answers, so
66+
the rule and the table agree **by construction**. Its `STRING_SOURCE_FIELD_TYPES`
67+
branch and its `formula` branch are retired with them; `min` / `max` over the temporal
68+
class still answers `'time'`, unchanged.
69+
- **`AnalyticsServiceConfig.sourceFieldMeta`** no longer declares `returnType`. It was
70+
carried (#16236) for one reader — the retired `formula` branch — and a declared input
71+
nobody consumes is the declared-not-enforced shape Prime Directive #10 refuses.
72+
73+
⚠️ **That key was never released, so against every published version this removal is a
74+
no-op.** #16236 is still a pending changeset in the same release window as this one;
75+
the last published entry (17.4.0) says in as many words that `FieldSchema.returnType`
76+
"is not on `AnalyticsServiceConfig.sourceFieldMeta`'s return shape". The key was
77+
therefore added and removed inside one window and no published tarball ever carried it.
78+
79+
**Host fix, one line:** drop `returnType` from whatever your `sourceFieldMeta` returns.
80+
You do not have to — the hook is a function RETURN position, so an extra key is not an
81+
excess-property error and is simply ignored at runtime — but keeping it declares an
82+
input nothing reads. Hosts on `AnalyticsServicePlugin` need no change at all: the plugin
83+
stopped relaying the key in this same change.
84+
85+
## FROM → TO, and the one-line fix
86+
87+
| you wrote | write instead |
88+
|---|---|
89+
| `{ aggregate: 'min' \| 'max', field: <a text/select/lookup/autonumber field> }` | `count` / `count_distinct` if you were counting; a **sort** on the list/report if you wanted the first or last RECORD |
90+
| `{ aggregate: 'min' \| 'max', field: <a json/multiselect/file/location/vector field> }` | store the quantity you meant as a numeric or temporal field and aggregate that |
91+
| `{ aggregate: 'min' \| 'max', field: <a formula field> }` | a formula emits no column; aggregate the stored field the formula reads, or persist the computed value |
92+
93+
⚠️ **Untouched:** those field types used as a **DIMENSION** (grouping, labelling,
94+
bucketing, filtering), `count` / `count_distinct` over any type, `min` / `max` over the
95+
numeric, temporal and boolean classes, and every `sum` / `avg` row #16778 and #16099
96+
already settled. The refusal also still stands down rather than guessing wherever the
97+
declared type cannot be resolved: no `sourceFieldMeta` wired, an unknown field, or a
98+
`relationship.field` path whose column lives on a joined object.
99+
100+
⚠️ The hand-migration prescription ships as the ADR-0087 semantic TODO registered above,
101+
which names the measure and the field type per affected pair — no lossless conversion
102+
exists, because nothing can compute "the smallest text value" in a way every backend
103+
agrees on.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: the four `system/logging.zod.ts` duration keys carry their unit in the key name (#17782, ruling A on #15939)
6+
7+
<!-- adr-0087: registered logging-durations-unit-in-key -->
8+
9+
**BREAKING** — the HTTP log destination's batch flush, retry backoff start and request deadline,
10+
and the logging buffer's flush, now carry `Ms` in the key name.
11+
12+
| def | before | after |
13+
|:--|:--|:--|
14+
| `HttpDestinationConfig` | `batch.flushInterval: 5000` | `batch.flushIntervalMs: 5000` |
15+
| `HttpDestinationConfig` | `retry.initialDelay: 1000` | `retry.initialDelayMs: 1000` |
16+
| `HttpDestinationConfig` | `timeout: 30000` | `timeoutMs: 30000` |
17+
| `LoggingConfig` | `buffer.flushInterval: 1000` | `buffer.flushIntervalMs: 1000` |
18+
| values, defaults, bounds | ms; 5000 / 1000 / 30000 / 1000; positive int | **unchanged** |
19+
20+
## Migration
21+
22+
```diff
23+
const destination = HttpDestinationConfigSchema.parse({
24+
url: 'https://logs.example.com/v1/logs',
25+
- batch: { maxSize: 500, flushInterval: 10000 },
26+
- retry: { maxAttempts: 3, initialDelay: 1000 },
27+
- timeout: 30000,
28+
+ batch: { maxSize: 500, flushIntervalMs: 10000 },
29+
+ retry: { maxAttempts: 3, initialDelayMs: 1000 },
30+
+ timeoutMs: 30000,
31+
});
32+
33+
const logging = LoggingConfigSchema.parse({
34+
name: 'app_logging',
35+
label: 'App logging',
36+
destinations: [],
37+
- buffer: { enabled: true, size: 5000, flushInterval: 2000 },
38+
+ buffer: { enabled: true, size: 5000, flushIntervalMs: 2000 },
39+
});
40+
```
41+
42+
Rename the keys. Every value is the same number of milliseconds it always was, and the
43+
5000 / 1000 / 30000 / 1000 defaults are unchanged; nothing else on either def moves.
44+
45+
## Why
46+
47+
Each key named milliseconds in a source JSDoc — "Flush interval in milliseconds", "Initial retry
48+
delay in milliseconds", "Timeout in milliseconds" — and the JSDoc above a key is not what
49+
`content/docs/references/**` renders; `.describe()` is, and **none of the four carried one at
50+
all**. Measured by the `check:duration-unit-keys` census on this tree before the change, all four
51+
read `[name: -] [prose: -]`: no unit in the key, and no published prose to supply it either. So
52+
`content/docs/references/system/logging.mdx` printed a bare `5000` / `1000` / `30000` / `1000`,
53+
and nothing on the page decided milliseconds from seconds. Under the #14478 rule, moving the unit
54+
into the describe alone would itself be a violation (unit in prose, none in the name), so each key
55+
is renamed and given the describe it never had in the same stroke. Executes director-seat ruling A
56+
on #15939 (2026-09-11, maintainer 「同意」, decision batch #115), the per-file remediation of the
57+
#14478 rule.
58+
59+
⚠️ `flushInterval` was declared **twice** on this file, in two different defs and with two
60+
different defaults — 5000 on the HTTP destination's `batch`, 1000 on the logging `buffer`. They are
61+
two keys, not one; each gets its own tombstone, its own registered row, and a prescription that
62+
names its def, so an author who lands on one is not sent to the other.
63+
64+
The `Ms` suffix is the family's own spelling, counted in key position on this tree: 272 `*Ms:`
65+
declarations in `packages/spec/src` against 75 `*Seconds:`. The only competing unit spellings are
66+
3 `*MS:` and 9 `*Millis:`, and every one of them mirrors a name fixed outside this repo — MongoDB's
67+
`maxCommitTimeMS` and `connectTimeoutMS`, node-postgres's `idleTimeoutMillis` and
68+
`connectionTimeoutMillis` on `PoolConfigSchema` — so unlike the `Ttl`-versus-`TTL` question a
69+
sibling round had to settle, there was no in-repo alternative to choose between. All three target
70+
spellings were already attested as key-position `*.zod.ts` declarations before this change:
71+
`flushIntervalMs` 1 (on `kernel/events/integrations.zod.ts`, at the same 1000 default),
72+
`initialDelayMs` 5, `timeoutMs` 30.
73+
74+
## The kit
75+
76+
- a `retiredKey()` tombstone on each of the four old spellings, so `tsc` types it `never` and a
77+
value reaching the parse raises the rename prescription instead of being silently stripped — none
78+
of the four enclosing objects is `.strict()` (`HttpDestinationConfig` itself and its nested
79+
`batch` and `retry`; `LoggingConfig`'s nested `buffer`)
80+
- the ADR-0087 D3 semantic entry `logging-durations-unit-in-key` and four
81+
`RETIRED_KEYS_BY_MAJOR[18]` rows, one per key. No D2 conversion: `stack.zod.ts` declares no
82+
logging collection and neither `LoggingConfigSchema` nor `HttpDestinationConfigSchema` is
83+
referenced anywhere in `packages/spec/src` outside `system/logging.zod.ts`, so the chain has no
84+
rehydration seam that runs on an authored logging document — the same reading
85+
`tenant-schema-cache-ttl-unit-in-key` recorded for its sibling key
86+
- pin tests per key: the refusal carries the rename prescription and names the def, the suffixed
87+
key parses at the magnitude the retired one carried with the same default, and the describe
88+
publishes the unit
89+
- exactly one authorable-surface row pair moves, and it is the one that should: that ratchet records
90+
top-level keys per def (`build-schemas.ts` reads `schema.properties` one level deep), and
91+
`HttpDestinationConfig.timeout` is the only top-level key of the four —
92+
`system/HttpDestinationConfig:timeout` becomes `[RETIRED]` beside a new
93+
`system/HttpDestinationConfig:timeoutMs`, and the `authorable-defaults/` row is renamed with it.
94+
The three nested keys move neither file, which is correct and not an omission
95+
- the pinned objectui checkout is untouched by this rename: at `.objectui-sha` pin
96+
`53ded82bf7a494f54e344e19099dbf00854b8694` it spells `flushInterval` 0 times, `initialDelay` 0,
97+
`HttpDestinationConfig` 0 and `LoggingConfig` 0 across its 6409 tracked files, against lit
98+
controls `useState` 2304 and `timeout` 702 on the same corpus

0 commit comments

Comments
 (0)