Skip to content

Commit a1c6ed5

Browse files
committed
Merge origin/main into claude/issue-5553-file-description-rendering
生成物本次无冲突且未被合并改动(#6222 触及 lib/zod-graph.ts 但未重生成任何页面)。 仍按 #4675 分两步:本提交只合并,下一提交整体重跑 gen:docs 验证无进一步漂移。
2 parents 201ab12 + 3e0f7e4 commit a1c6ed5

16 files changed

Lines changed: 1497 additions & 63 deletions
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: 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: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
fix(service-automation): resume 时「存储不可达」的日志不再把驱动错误拼进 message,改走 meta (#5912)
6+
7+
`engine.ts``resumeInternal` 在读挂起态存储失败的那一支,把**我们不控制文本**
8+
数据源驱动失败原因直接插进了 `logger.error` 的 message。`ObjectLogger.write()` 一次
9+
调用只加一个「时间戳 + 级别」记录头,所以 message 里的换行会把**一条**记录变成多个
10+
物理行,后面几行既无级别也无时间戳。在 `pretty` / `text` 格式(`os dev` / `os serve`
11+
的默认)下,文件 sink 会把它们当成独立记录存,而 `grep ERROR` 只捞得到不含任何事实
12+
的那一行 —— 恰恰是运维正在找的那条。实测:一个三行的 better-sqlite3 驱动错误把这条
13+
告警切成 **3 个物理行**,只有第 1 行带 `ERROR` 头。
14+
15+
改法与 #5048 / #5575 / #5636 / #5661 / #5737 完全同一套,零新词汇:**message 单行
16+
自足**,外来 cause 交给 `Logger` 契约(`packages/spec/src/contracts/logger.ts`)
17+
`error(message, error?, meta?)`**第三**参(第二参留空,否则每条记录都会带上整个栈)。
18+
19+
这是这条 resume 路径上最后一处。#5737(PR #5911)修完 `wait` 节点五处之后,同一次
20+
「resume 时存储不可达」会产生两条记录:wait 节点那条已是干净单行,engine 这条仍被
21+
切碎;本次之后两条都干净。
22+
23+
对运维可见的变化(日志形状,非行为):
24+
25+
- 这条记录恒为**一个**物理行,不论日志格式;
26+
- 原因文本从 `msg` 末尾的 `: <驱动文本>` 移到记录的 `error` 字段(`meta`),多行驱动
27+
错误由 `JSON.stringify` 转义换行后完整保留 —— 一个字节都不丢;
28+
- message 补齐了 #4632 要求的后果与修法(挂起态**未被消费**、运行仍停在原处、存储
29+
恢复后可原样重试),并指明 cause 在本记录的 meta 里。
30+
31+
刻意**不变**的两处,已各自钉上回归测试:
32+
33+
- **返回值信封** `AutomationResult.error`(`STORE_UNAVAILABLE`)仍逐字拼接驱动文本。
34+
它是给调用方读的结构化返回值,经 REST 出去是 JSON 字符串字段、不按行切分;#5636
35+
`degradedReason` 是同源取舍,且 PR #5911 已让 wait 节点侧把它整体放进 meta 保留。
36+
- **级别仍是 `error`**。运行在盘上而 resume 没落地,正是 #4632 定义的耐久性降级,
37+
`pnpm check:durability-log-level` 照旧覆盖。
38+
39+
按记录末尾驱动文本字面量 grep 这条记录的日志查询,需要改成读记录的 `error` 字段。

.claude/agents/os-dev.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,32 @@ all real:
206206
forcing the template; #4984 is the family origin — fixtures spelling
207207
rejected aliases kept the tests green while the rule was dead).
208208

209+
**Rejection-class cases assert the envelope, not the throw.** For any case whose
210+
point is that bad input is *refused*, the minimum assertion set is the error's
211+
**`code` AND `status`** (the ADR-0112 envelope). `expect(...).toThrow()` /
212+
`rejects.toThrow()` on its own is not a rejection test: it carries one bit where
213+
the defect has two, and PR #6142 (#6050) measured both ways it goes blind —
214+
opposite directions, same hole:
215+
216+
- **A bare `Error` ⇒ permanently green.** Deleting the new refusal gate turned
217+
22 of `driver-sql`'s 28 cases red, and *most* of those reds were the driver
218+
throwing knex's bare `Undefined binding(s)` — an `Error` whose `code` and
219+
`status` are both `undefined`. The unfixed driver already throws; only the
220+
envelope is missing. A throw-only assertion therefore stays **green on the
221+
very driver the issue targets**.
222+
- **A transport that never throws ⇒ red, but pointing away from the defect.**
223+
The same deletion turned 20 of `driver-turso`'s 29 remote cases red, and all
224+
20 failed by *answering* — that transport never throws. A throw-only
225+
assertion reports "the promise resolved", which names the absence of a throw
226+
and never the absence of an envelope, so it cannot separate "refused with the
227+
wrong envelope" from "did not refuse at all" — and those are exactly the two
228+
defects.
229+
230+
Where the wording is itself contract (#5240, one condition ⇒ one wording),
231+
assert the message's first sentence **on top of** `code`+`status`, never instead
232+
of them. A rejection test that cannot go red on a missing envelope reads as
233+
coverage and is not.
234+
209235
**Key-vs-value reachability criterion.** Match a fixture guard's assertion to
210236
what the rule guards. Guarding that a **key** is a real authoring surface →
211237
assert the schema reports no `unrecognized_keys` on the fixture. Guarding a

.claude/skills/pm-dispatch/SKILL.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,28 @@ prompt:
11951195
实现面逐层收口」这类工作上,没有第二个实现面的活(纯 UI、文档、单面脚本)这条无处可
11961196
绑,该省掉而不是改写它。
11971197

1198+
**拒收类用例的最低断言集是 `code` + `status`,不是「它抛了」—— 派发令的标准条款。**
1199+
适用判据:本单会**新增或改写拒收 / 错误类用例**(验收点里出现「应当被拒收」的活)。
1200+
满足时派发令带这一句(原话):
1201+
1202+
> 拒收类用例最低断言**错误的 `code``status`**(ADR-0112 信封)。
1203+
> `expect(...).toThrow()` / `rejects.toThrow()` 单独使用**不构成**拒收测试;措辞本身
1204+
> 是契约时(#5240「一个条件一种措辞」),首句断言**加在** `code`+`status` **之上**,
1205+
> 而不是代替它。本条约束你**新写或改写**的用例 —— 顺手回填存量套件不在本单范围内。
1206+
1207+
出处是 #6142(#6050)的反向验证实测。两种失明机制方向相反,同一个洞:
1208+
1209+
- **`Error` ⇒ 恒绿。** 删掉拒收闸后 `driver-sql` 28 例红 22,**多数红在抛出 knex 的
1210+
`Undefined binding(s)`** —— 一个 `code` / `status` 均为 `undefined` 的 Error。未修的
1211+
驱动本来就抛,缺的只是信封:只断言「它抛了」的用例,**在本单所针对的那个驱动上保持
1212+
绿色**
1213+
- **从不抛的 transport ⇒ 红,但红得不指向缺陷。** 同一次删闸,`driver-turso` remote
1214+
29 例红 20,**20 个全部**红在「本该拒收却编译出了 SQL」—— 该 transport 从不抛。只断言
1215+
抛出的用例在这里报的是「promise 没有 reject」,说的是**没抛**而不是**没信封**,分不开
1216+
「拒收了但信封错」与「根本没拒收」—— 而这正是这一族的两个缺陷。
1217+
1218+
一句话:**一个在缺信封的实现上无法转红的拒收用例,读起来是覆盖,实际不是。**
1219+
11981220
**Issue 正文是线索,不是规格 —— and the dispatch wording is what makes an
11991221
honest "the premise is dead" cheap to return.** Step 1's stale-premise check
12001222
is the PM's sample; the dev's verification is the real thing, so the prompt
@@ -1478,6 +1500,12 @@ against the report's own claims:
14781500
genuinely invalid shapes are still there (step 5's two lines). #5365 slipped
14791501
through exactly this review layer and was caught by CI instead: CI does catch
14801502
it, at the price of one extra lap.
1503+
- **拒收类用例的绿,是不是「它抛了」的绿?** 判据:本单验收点含「应当被拒收」。抽查
1504+
diff 里的拒收用例有没有断言 `code``status`(ADR-0112 信封)—— 只写 `toThrow()` /
1505+
`rejects.toThrow()` 的用例,在**未修实现本来就抛裸 Error** 的那一族上恒绿(#6142
1506+
实测:`driver-sql` 删闸后 22 红中多数是裸 knex Error,`code` / `status`
1507+
`undefined`),于是「28 例全绿」这种报告读起来是覆盖、实际证不了拒收。缺断言判
1508+
REWORK 补齐,而不是接受绿色输出。本条是 step 5 那条标准条款在复核侧的对账。
14811509
- **Did the dev verify the issue's premise?** The report's
14821510
`premise_still_valid` field makes the answer explicit — a `false` there
14831511
reopens triage rather than failing review. A report that falsifies the

0 commit comments

Comments
 (0)