Skip to content

Commit ab322f0

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-5599-view-union-identity-precondition
2 parents a5c5385 + 8140915 commit ab322f0

188 files changed

Lines changed: 8344 additions & 840 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/cel-parse-fault-kind.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/formula': patch
3+
---
4+
5+
fix(formula): 括号/引号/转义等 parse 期错误不再被误报为 `runtime`
6+
7+
`celEngine` 的错误分类此前完全靠**错误文案关键词**判定,而 cel-js 8.0.0 的 parse 期错误有约 19 种措辞,只有 3 种含 `parse` / `unexpected` / `syntax`。其余整类 —— 最典型的括号/方括号/花括号不配对(`Expected RPAREN, got EOF`)、未闭合字符串、非法转义、保留字 —— 全部落到默认值 `runtime`
8+
9+
`kind` 不是内部字段:它被原样拼进作者可见的写入拒绝文案(`@objectstack/objectql``rule-validator` / `cel-fault`)与 REST 错误响应体的 `reason`。少写一个右括号的校验规则,作者读到的是 `(runtime: …)` —— 指向数据与求值期,而真正该改的是表达式本身,与 ADR-0032 D1d 的"消息面向自纠"相悖。
10+
11+
改为按 cel-js 抛出的**错误类**判定:`ParseError``parse`(其中 `code: 'limit_exceeded'` 仍 → `bounds`,cel-js 的越界一律由 parser 抛出)。这一层不再读文案,因此也修掉了关键词方案无法修的一格:cel-js 会把**作者自己的源码行**嵌进 `message`(`formatErrorWithHighlight`),于是字段名能决定错误分类 —— 实测 `((record.type_id)` 这条普通的括号不配对,此前被判为 `type`,只因回显的源码里含子串 "type"。
12+
13+
`type` / `runtime` 两支暂仍走原关键词表:cel-js`TypeChecker`**阶段**而非按故障选择错误类(`isEvaluating ? evaluationError : typeError`),同一个 `unknown_variable` 在 check 期是 `TypeError`、在 eval 期是 `EvaluationError`,整体结构化会改变这些既有判定。审计见 #6133
14+
15+
kind 词表本身(`parse` / `type` / `runtime` / `bounds` / `dialect`)未变,消费方未改。
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
fix(objectql): 集合算子的标量比较值答 400 INVALID_FILTER 并点名期望形状,不再 500 DATABASE_ERROR
6+
7+
`FieldOperatorsSchema` 声明 `$in` / `$nin` 的比较值是数组、`$between``[min, max]` 二元组,但入口处没有任何一层强制这条声明:`isFilterAST` 只看算子,`parseFilterAST` 照单下降,于是 `['status', 'not_in', 'done']` 变成 `{ status: { $nin: 'done' } }` 一路走到驱动。
8+
9+
**行为变化(用户可见)**:此前 `driver-sql` 把标量交给 `whereIn(field, scalar)`,答 **500 `DATABASE_ERROR`** —— 用服务端故障码报告一个调用方能自己改好的过滤器,且不说明是哪个算子、哪个字段、该写成什么。现在引擎在唯一收口点拒收,答 **400 `INVALID_FILTER`**,信息点名算子(同时给出 `not_in` / `nin` / `notin` 这类作者实际书写的拼法)、字段、收到的值与位置、以及可直接粘贴的正确形状,并声明该过滤器**未被应用**
10+
11+
覆盖两道门:直接调用引擎(`FilterArray` 下降路径)与 HTTP 面(协议层已自行下降成 `FilterCondition` 对象后再交给引擎)—— 后者正是本问题实测到的那道门。`find` / `findOne` / `count` / `aggregate` / `update` / `delete` 六个入口一致。
12+
13+
`$between` 的非二元组比较值一并收在同一处:`driver-sql``driver-memory` 各自已经拒收(措辞保持逐字一致),`driver-mongodb` 的分支则直接落空、不发射区间谓词 —— 收在收口点后三家答案一致。
14+
15+
**不变的**:`$in: []` / `$nin: []` 仍是合法谓词(分别表示「不匹配任何行」与「匹配所有行」);列表**成员**的类型不在此处复判(那是 #5234,另一个面);非集合算子的标量比较值不受影响,包括 `$gt` 的 ISO 日期字符串这类 `FieldOperatorsSchema` 声明更严、而各后端一致接受的形状。
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/service-analytics": patch
3+
---
4+
5+
fix(service-analytics): `compareTo` 在「日期维度本身就是网格维度」时把比较桶键平移回当期 (#6007)
6+
7+
趋势图 + 同比是 `compareTo` 最常见的形状:日期维度既写进 `selection.dimensions`
8+
(它就是图表的时间轴),又被 `compareTo` 用作锚点。这个形状下比较趟从来没有对齐过。
9+
10+
比较趟查询的是**平移后**的窗口,所以它的行按平移后的桶键落地;而
11+
`mergeByDimensions``selection.dimensions` 元组建键 —— `2025-01` 不等于
12+
`2026-01`,于是**没有一条**比较行合并得进去,全部作为新行追加。两趟各自只报告了自己
13+
那一半,`fillEmptyGroups` 把另一半填成自信的 `0`,再加上平移后的桶键坐在网格里,而它们
14+
落在调用方筛选窗口之外。一个 2 桶窗口的「今年 vs 去年同期」回来是这样的:
15+
16+
```
17+
[{"close_date":"2025-01","opp_count__compare":5,"opp_count":0},
18+
{"close_date":"2025-02","opp_count__compare":7,"opp_count":0},
19+
{"close_date":"2026-01","opp_count":1,"opp_count__compare":0},
20+
{"close_date":"2026-02","opp_count":2,"opp_count__compare":0}]
21+
```
22+
23+
四行、每行一个 0、两行在窗口外;期望是 2 行 × 2 列。
24+
25+
**修法(维护者裁决 2026-08-07,方向 1):合并之前,把每个比较桶键用当期的说法重述一遍。**
26+
上例现在返回 `[{close_date:'2026-01',opp_count:1,opp_count__compare:5},
27+
{close_date:'2026-02',opp_count:2,opp_count__compare:7}]`。
28+
29+
- `previousYear` —— 窗口是按日历年平移的,所以逆运算就是按日历年往前推一年:对桶自己的
30+
首日做平移再重新分桶。`2025-01``2026-01``2025-Q1``2026-Q1`
31+
`2025-W03``2026-W03`。它刻意是 `shiftRange` 那套年运算的精确逆运算(含
32+
`setUTCFullYear` 的溢出行为),窗口与桶键因此不可能对「一年」有两种理解。
33+
- `previousPeriod` —— 任意天数窗口没有日历对应物,所以按**桶序(bucket ordinal)**对齐:
34+
上一窗口的第 n 个桶对上本窗口的第 n 个桶,n 各自从自己窗口的起点数起。序号由**日历**算出
35+
而不是数组下标,所以本期网格里某个桶没有数据(存在空档)不会让其后每个桶都错位一格。
36+
37+
**响应形状不变** —— 仍然是 `<measure>__compare` 列,行仍然是网格维度元组,所以消费端
38+
(objectui#3337 正在收敛的那条契约)不受影响。
39+
40+
不确定时一律**保持原样**(即改动前的行为),而不是猜:空桶(两条聚合路径上键都是 `null`,
41+
两趟本来就互相合并)、未分桶的日期维度(分组的是原始时间戳,不是桶键)、以及平移回来落在
42+
当期窗口之外的桶(两个等长的天数窗口可以切出不同的桶数)。
43+
44+
范围严格限定在坏掉的那个形状:锚点必须是**网格维度**(仅作窗口的锚点两趟都不是列,#5688
45+
之后本来就对齐)且必须**被分桶**。两趟通过同一个 `granularityOf` 读取桶大小,所以这里重述
46+
的桶大小按构造就是查询分组用的桶大小。
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
fix(spec)!: `composeStacks``i18n` 退出 last-wins —— 同值放行、冲突报错带处方 (#5051)
6+
7+
#5005`composeStacks` 的非数组顶层键统一成「同值放行 / 冲突报错」,并由维护者
8+
2026-08-04 裁决点名否掉了 last-wins(**先声明的栈被后组合者无声覆盖**)。`i18n`
9+
唯一逃过那一轮的键 —— 因为 #5005 的主题是**被丢掉**的键,而 `i18n` 本来就有一个能
10+
工作(只是无声)的策略。于是它成了整个顶层键面上**仅剩的一个 last-wins**
11+
2026-08-06 裁决取 A(对齐),2026-08-07 窗口拉入 v17。
12+
13+
## FROM → TO(行为激活面)
14+
15+
| 场景 | FROM(v17 之前) | TO(本次) |
16+
|:--|:--|:--|
17+
| 只有一个栈声明 `i18n` | 保留该声明 | **不变** |
18+
| 多个栈声明**相同**`i18n` | 保留(数组里最后那个,值相同) | **不变**(同值放行) |
19+
| 多个栈声明**不同**`i18n` | 数组里靠后的栈胜出,**靠前那个栈的声明被静默丢弃** —— 不报错、不告警,作者无从分辨「被覆盖」与「从没写过」 | **抛错**,点名冲突键 `i18n`、两个来源栈(manifest id,无 manifest 时用 `stack #N`)与两条出路 |
20+
21+
被静默丢弃的那一侧,过去从组合结果里彻底消失;现在它不再消失 —— 它以一条点名两个
22+
来源栈的错误的形式出现,由作者决定保留哪一个。这就是本次「静默丢弃 → 不再丢弃」的
23+
激活面:凡是今天**已经**在依赖 last-wins 覆盖语言配置的组合,升级后会在
24+
`composeStacks` 处响亮地停下。
25+
26+
## 为什么本地化配置不能择一,也不能深合并
27+
28+
一个栈的 `translations` 语言包是**照着它自己声明的 `supportedLocales` 写的**。让附加
29+
包的 `i18n` 胜出,等于让基础栈的语言包去寻址一组组合后的应用不再承认的 locale ——
30+
运行时看到的只是「查不到翻译」,而不是「你的语言配置被覆盖了」。深合并同样出局
31+
(#5005 原裁决):它造出一个两位作者都没写过的第三种 locale 集合。
32+
33+
仓内实测:`examples/app-crm`(`['en','zh-CN']`)与 `examples/app-todo`
34+
(`['en','zh-CN','ja-JP']`)正是这一对 —— `defaultLocale` 一致,locale 集合不一致。
35+
36+
## 迁移
37+
38+
组合时报 `composeStacks conflict: top-level key 'i18n' …`,按错误信息里的处方二选一:
39+
40+
```ts
41+
// A. 让两处声明一致(通常是取并集,由作者决定)
42+
i18n: { defaultLocale: 'en', supportedLocales: ['en', 'zh-CN', 'ja-JP'], fallbackLocale: 'en' }
43+
44+
// B. 只在应当拥有它的那个栈里声明 `i18n`,其余栈删掉这一段
45+
```
46+
47+
`ComposeStacksOptionsSchema` **不加旋钮**:显式覆盖机制按 #5005 裁决,留给定制故事真
48+
拉动时统一设计,不为单个键预支。数组键(含 `translations`)的拼接语义一字未变。
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
"@objectstack/service-datasource": patch
3+
---
4+
5+
fix(service-datasource): a `pool` block on a `memory` datasource is rejected, not dropped in silence (#5931)
6+
7+
#5714 made a `pool` block the driver cannot honour a loud authoring error, but
8+
its ruling was scoped to the two sqlite arms — `memory` kept dropping it. The
9+
`memory` arm hands `InMemoryDriver` nothing but `buildMemoryConfig(spec)`, which
10+
reads `spec.config` and never `spec.pool`, so a sized pool reached nothing and
11+
said nothing. Measured through the real factory:
12+
13+
```text
14+
memory + pool{min:3,max:9} driver config {"persistence":false} pool undefined
15+
sqlite + pool{min:3,max:9} rejected (since #5714)
16+
postgres + pool{min:3,max:9} knex config.pool {"min":3,"max":9} live {min:3,max:9}
17+
```
18+
19+
`memory` now joins `POOL_UNSUPPORTED_DRIVER_IDS`, so the same three doors that
20+
already rejected sqlite reject it: the Setup wizard's create/update, the
21+
boot-time auto-connect pre-pass, and the driver factory itself.
22+
23+
**Behaviour change.** A datasource declaring `driver: 'memory'` (or `inmemory` /
24+
`in-memory` / `mingo`) together with a non-empty `pool` block used to load and
25+
run; it now throws at whichever door it arrives through. The fix is the one edit
26+
the message names — delete the `pool` block. Nothing is lost by deleting it: it
27+
configured nothing before. An absent or empty `pool` is unchanged, and every
28+
`memory` datasource without one builds exactly as it did. No declaration in this
29+
repo, the example apps included, carried the combination.
30+
31+
**Its own explanation, not SQLite's.** SQLite is rejected because a second
32+
connection to `:memory:` opens a separate, empty database, so sizing the pool
33+
would split one datasource across several stores. That reasoning is false for
34+
`memory`: there is no connection at all — the store is a plain data structure in
35+
this process — so the message says that instead. Telling an author their driver
36+
picked a connection strategy for them would send them looking for a knob that
37+
does not exist. Reasons are now keyed by driver id, which makes an arm joining
38+
the set without writing one a type error.
39+
40+
Maintainer ruling 2026-08-07, which also set the default for the next sister
41+
arm: when a declared key is silently dropped on one arm and an earlier ruling
42+
already made it a loud authoring error on a sibling, the new arm joins the
43+
existing rejection set rather than queueing for a ruling of its own — unless the
44+
original rationale was measured to be arm-specific.
45+
46+
No API surface is added — `POOL_UNSUPPORTED_DRIVER_IDS`,
47+
`driverReadsDeclaredPool`, `unsupportedPoolIssue`, `unsupportedPoolMessage` and
48+
`assertDatasourcePoolSupported` keep the signatures #5714 published, and the
49+
sqlite arms' rejection text is byte-for-byte unchanged.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): the reference generator elides an over-wide enum inside an inline shape summary, and says how many members it hid (#5340)
6+
7+
`formatType()` capped how many **keys** an inline object summary prints
8+
(`INLINE_KEY_LIMIT = 4`) but never capped how wide a single key's **type** could
9+
be. One long enum reached through a summary therefore printed every member into
10+
one table cell. The issue was filed on `BulkActionDef.params` at ~900
11+
characters; measuring the whole corpus found that is not close to the worst —
12+
the 261-member error-code vocabulary is inlined into the `error` shape of 80
13+
rows across 13 `api/*.mdx` pages, at **6242 characters in a single cell**.
14+
15+
An `Enum` body rendered below a summary's `{ … }` is now cut to 80 characters
16+
and the count of what was cut is printed in its place:
17+
18+
```
19+
type: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | … +42 more>
20+
```
21+
22+
The count is the safety property, not decoration. A silent prefix would leave
23+
the page looking complete while it was not — a reader cannot tell a 7-member
24+
vocabulary from the first 7 of 49 — and these pages are the authoritative input
25+
for AI authors (ADR-0033), so a page that lies by omission is a worse defect
26+
than a wide cell.
27+
28+
**Nothing that owns its vocabulary is elided.** The cut applies only below an
29+
inline shape summary, which is by construction a *second* copy: a schema's own
30+
row (`BulkActionParam.type`, `ErrorResponse.code`), a union variant on its own
31+
row (`Enum< … > | string`, the `PageComponent.type` shape), a top-level
32+
`Record< string, Enum< … > >` and an array of a top-level enum all still print
33+
every member. For 457 of the corpus's 805 in-shape occurrences the elided
34+
copy's full list is still on the same page for that reason; for the remaining
35+
348 the count carries it, and the JSON Schema under `json-schema/` remains the
36+
authority it always was.
37+
38+
**The 80 is measured, not chosen.** Across 216 pages / 8541 type cells / 1768
39+
`Enum` occurrences, the 805 in-shape ones are bimodal and their density per
40+
character collapses at 80 (3.6 occurrences/char over `(64,80]`, 1.6 over
41+
`(80,100]`, 0.5 over `(100,200]`). Below it sit the ordinary short vocabularies
42+
a reader wants spelled out; above it sit listings. A tighter budget buys almost
43+
nothing and costs real information — budget 24 would elide 79% of them to save
44+
4% more characters — and a fixed member cap is worse at every setting.
45+
46+
An elision must also **pay for its own marker**: a body only a member or two
47+
over budget gives back less than `… +N more` costs to print, so it is left
48+
whole. That is why 31 in-shape enums between 81 and 107 characters are
49+
unchanged, and why the limit is not a cliff at exactly 81.
50+
51+
42 reference pages change, 144 rows, all in one direction: every changed row is
52+
shorter (462,140 characters removed in total, largest single row -6266), and
53+
every one carries a `… +N more` marker — no row is silently truncated and none
54+
grew. Cells over 900 characters go from 76 to 4, over 200 from 246 to 145;
55+
the p95 cell width is unchanged at 145, i.e. ordinary cells do not move.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/driver-memory": major
3+
"@objectstack/driver-mongodb": major
4+
"@objectstack/driver-sql": major
5+
"@objectstack/driver-sqlite-wasm": major
6+
"@objectstack/driver-turso": major
7+
---
8+
9+
refactor(drivers)!: 五个驱动的 query 参数跟进 `DriverQuery`,休眠的类型谎言就此没有藏身处 (#6075)
10+
11+
#5181(PR #6076)把 `IDataDriver.find/findOne/count/updateMany/deleteMany/explain` 的 query 参数收窄为 `DriverQuery``Omit<QueryAST, 'object'>`),并在同一条 changeset 里写明:「把驱动签名一并迁到 `DriverQuery` 是后续的机械收尾」。这就是那次收尾。
12+
13+
在此之前,五个驱动的实现仍旧声明 `query: QueryAST`(turso 侧是 `query: any`)。**它不红,也不会红** —— 方法参数按双变比较,实现声明得比契约宽照样满足契约。但调用方现在**有权**省略 `object`,于是这些实现的类型说 `query.object``string`,运行期却可能是 `undefined`:一句休眠的谎言,没有任何门拦得住下一个照着它写代码的人。
14+
15+
收尾之后,「驱动读 `query.object`」直接变成编译错误:
16+
17+
```ts
18+
// 收窄前:编译通过,运行期可能是 undefined —— 谎言
19+
// 收窄后:error TS2339: Property 'object' does not exist on type 'DriverQuery'.
20+
const name = query.object;
21+
```
22+
23+
**零运行时改动。** 本次改的全部是类型注解:五个驱动的六个契约方法签名,以及为让类型自洽而必须跟进的少量私有辅助方法参数(mongodb 的 `buildFindOptions` / `buildSortSpec`,sql 的 `findRows` / `orderKeysFor`,turso 的 `toRemoteQuery` / `toRemoteReadQuery`,memory 的 `performAggregation`)—— 它们都只转发或读取 `where` / `orderBy` / `groupBy` 这些字段,本来就不读 `object`。turso 的几处 `query: any` 一并收紧,多拿回一批本已放弃的检查。emit 无差异,测试全绿(memory 524、mongodb 206、sql 906、sqlite-wasm 254、turso 788)。
24+
25+
**迁移面:删掉驱动调用字面量里的 `object:`**,与 #5181 是同一句话,只是现在也覆盖了直接按具体驱动类(`SqlDriver` / `MemoryDriver` / …)而非按 `IDataDriver` 取类型的调用方。编译器会逐处指出来(TS2353 `'object' does not exist in type 'DriverQuery'`)。本仓下游 25 个包实测零处需要改动,改动只落在五个驱动自己的测试里。
26+
27+
标 major 的依据与 #5181 一致:**源码级破坏性**(调用点内联字面量),运行时行为零变化。`check:api-surface` 只记录导出的存在与否、不记录签名,因此这条说明同样是该变更唯一的下游载体。
28+
29+
`aggregate` / `distinct` / `syncSchemasBatch` 不在本次范围内 —— 它们不是 `IDataDriver` 收窄的那六个方法,其中 `syncSchemasBatch` 的条目里 `object` 是被真实读取的必填键,`expand` 条目里的 `object` 同理命名的是关联对象,都不是冗余。

0 commit comments

Comments
 (0)