Skip to content

Commit f3e2f40

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-4793-batch-row-result-schema
2 parents 0be7d20 + 9c040f1 commit f3e2f40

45 files changed

Lines changed: 2669 additions & 1256 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
fix(plugin-auth): 每号码 OTP 发送预算改用惰性解析的共享计数存储 —— 多节点下不再按节点数倍增 (#4790)
6+
7+
#2780 的「每号码 OTP 发送预算」(60s 冷却 + 每小时 5 条)此前**只有宿主显式提供
8+
better-auth `secondaryStorage` 时才跨节点共享**`AuthManager.getOtpSendGuard()` 唯一的
9+
存储来源就是 `AuthManagerOptions.secondaryStorage`,而标准 `serve` 组合里没有任何一处
10+
提供它(#4788 之后 `AuthPlugin` 也明确不再从 cache 服务派生它)。于是预算落在**每个进程
11+
一份**:N 个节点的部署,一个号码实际能收到的是声明值的 N 倍,而且**没有任何信号**告诉你
12+
它没兑现(ADR-0049 声明 ≠ 强制)。这里的计价单位是**真金白银的短信**
13+
14+
这是 #4772 那条限流洞的同类,但是独立的一处:#4788 修的是 better-auth 自己的 `rateLimit`
15+
计数器(走 `rateLimit.customStorage`),OTP 预算是 ObjectStack 在 `AuthManager` 里自己实现
16+
的另一套计数,行为未被 #4788 改变。
17+
18+
**修法:复用 #4788 建好的那条路径,而不是再写一份。** `rate-limit-storage.ts` 中把「惰性
19+
解析 → 绑定即宣告 → 解析不到就降级到有界的进程内存储并响亮告警」抽成
20+
`createLazyCounterStore()``createLazyCacheRateLimitStorage()` 现在就是它的一层薄封装),
21+
OTP 预算经由新的 `AuthManagerOptions.sharedCounterStore` 接同一条路径:
22+
23+
- **存储在每次发送校验时才解析**,因此 `CacheServicePlugin` 晚于 `AuthPlugin` 注册也照样
24+
绑定得上(插件启动顺序不再决定任何事)—— 这正是 #4772 冻结结论造成的那个洞;
25+
- 配了 cache 的多节点部署,每号码预算**现在真的是一份**,换节点不会重新获得冷却额度;
26+
- 没有 cache 服务的部署**仍然限额**,只是降级为进程内计数,并在第一次真正计数时打一条
27+
点名代价的 warn(「an N-node deployment can send up to N× the configured number of PAID
28+
SMS to one number」)—— 降级不是关闭,两种情况在日志里可区分(绑定打 info,降级打 warn)。
29+
30+
**刻意不引入 `secondaryStorage` 来修它**#4785):那会把会话的记录之处搬进缓存,静默废掉
31+
ADR-0069 D4 的三个会话管控。宿主自己提供的 `secondaryStorage` 对这个预算仍然优先且行为不变。
32+
33+
冷却与滚动小时窗的语义**未做任何改动**:计数依旧是按号码的时间戳滚动窗口,只是换了它所在的
34+
存储。(固定窗口计数器无法表达「距上一次发送满 N 秒」,把它改成定窗会在窗口边界放行两倍突发
35+
——用一种倍增换另一种倍增。)
36+
37+
对使用者的影响:
38+
39+
- 新增 `AuthManagerOptions.sharedCounterStore``AuthPlugin` 自动填充,一般宿主无需感知;
40+
- 新增导出 `createLazyCounterStore()``counterStoreFromKv()`
41+
- `OtpSendGuard` 新增 `resolveStore` 选项,原有的 `storage`(字符串 KV)选项保持可用。
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
feat(lint): `has(x)` 不是 null 守卫 —— 发布期直接拒绝未守卫的可空比较 (#4763)
6+
7+
CEL 的 `has(x)` 问的是**键是否存在**。自 #4649 起,谓词读到的记录对对象声明的每个
8+
字段都是**全量**的:一个声明了却存 `NULL` 的列同样"存在",所以
9+
`has(record.end_date)` 对声明字段恒为 `true`,什么也没告诉作者。于是这个读起来
10+
像守卫的写法根本不是守卫:
11+
12+
```text
13+
has(record.start_date) && has(record.end_date) && record.end_date < record.start_date
14+
```
15+
16+
它会走到 `null < null`,CEL 没有对应重载,整个谓词中断。#4761 之前中断被吞掉
17+
(规则跳过,一条 WARN),也就是说**这一形状的规则在任何含 null 值的行上从未生效
18+
**——它写在元数据里、读起来完全正确、却什么都没有强制执行。#4761 把运行时改成
19+
fail-closed 之后,当场就在我们自己的两个示例对象里抓到了它。
20+
21+
运行时拒绝是兜底,不是该学到这件事的地方:作者会在真实数据(很可能是生产数据)
22+
上收到一个 400,离写下规则可能已经过去几个月。而这个错误**仅凭元数据就可判定**
23+
——谓词的 AST 加上对象声明的字段类型,就足以判断某个操作数是否可能为 null。按
24+
AGENTS.md PD #12(在创作期拒绝,不要在消费端容忍),它属于发布闸门。
25+
26+
**新增闸门(error,直接拒绝,没有降级开关)。** `os build` / `os validate` /
27+
`os lint` 与运行时发布闸门共用的 `validateStackExpressions` 现在会拒绝这样的谓词:
28+
**声明为可空**的字段(没有 `required: true`、没有 `defaultValue`、没有默认选项、
29+
不是 autonumber)应用**排序**(`< <= > >=`)或**算术**(`+ - * / %`,含一元 `-`)
30+
运算符,而该操作数没有被同一布尔分支内支配它的 `!= null` / `== null` / `!isBlank()`
31+
显式判空所守卫。`has(x)` **刻意不**计入守卫——这正是本规则存在的理由。错误信息点名
32+
规则、操作数与修法,收尾句逐字取自 `rule-validator.ts``unevaluableRuleError`,
33+
两道闸门措辞完全一致。
34+
35+
覆盖面(有意划定,而不是含糊地覆盖一半):对象**校验规则**(含 `conditional` 规则
36+
`then` / `otherwise` 里嵌套的谓词)与**生命周期 hook 的 `condition`** ——即真正由 CEL
37+
在全量记录上求值、会 fail-closed 的两类面。共享规则条件(下推成 SQL 过滤,`NULL > x`
38+
是三值逻辑,不会 fault)、flow 的扁平作用域条件(裸标识符可能是 flow 变量)与
39+
`Field.formula`(有自己的 #3306 `guard ? value : null` 处理)不在此列。
40+
41+
**未声明**键的 `has()` 完全不受影响——那才是它的正当用途:区分"这次 PATCH 里
42+
根本没提到这个键"与"显式写了 null"。示例应用无需改动即通过新闸门。
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/objectql': minor
3+
'@objectstack/cli': patch
4+
---
5+
6+
修复:每个 `os migrate` 子命令关停后,#4551 悬空引用巡检都会把 `sys_metadata` / `sys_view_definition` 报成 `unreadableObjects`(#4747)
7+
8+
一条**成功**的命令过去会在返回 JSON 之后打出两行 `ERROR Find operation failed` 和一份
9+
`unreadableObjects` 非空的巡检报告 —— 对抓 ERROR 的 CI 流水线是直接误报源,更要命的是它把
10+
`unreadableObjects` 变成了恒为真的告警:那个桶存在的意义正是区分「我没能检查」和「我检查了,
11+
没问题」,一个每次健康运行都非空的桶不再携带任何信息。
12+
13+
两处静默空转叠出了这个结果:
14+
15+
- `ObjectQLPlugin` 的关停逻辑写在 `stop()` 里,而内核的插件契约是 `init`/`start`/`destroy` ——
16+
`stop()` 从来没有被任何人调用过,ADR-0057 巡检定时器因此在任何宿主上都不会被解除。改为
17+
`destroy()`(与 `DefaultDatasourcePlugin` 一致)。
18+
- `bootSchemaStack().shutdown()` 调的是 `(runtime as any).stop?.()`,而 `Runtime` 根本没有
19+
`stop` —— 可选调用把「没有关停」伪装成了「关停过了」。改为走内核自己的 `kernel.shutdown()`,
20+
`os serve` 收到 SIGTERM 时同一条路径。
21+
22+
同时 `LifecycleService.stop()` 不再只是清定时器:它还会把「引擎正在拆」这一位交给正在飞行中的
23+
sweep,巡检据此在读之前停手。因关停而失败的读**不再进入** `unreadableObjects` —— 那不是关于
24+
数据源的证据;报告改用新增的 `DanglingReferenceReport.aborted` 记录「这次没跑完」,所以不完整
25+
依然是响的,只是不再占用发现桶。
26+
27+
**真正读不出来的对象(数据源故障)照旧进 `unreadableObjects`**,巡检在 CLI 场景也照旧运行 ——
28+
这里没有「一次性命令不跑巡检」的开关,只有「引擎活着才读」的生命周期边界。
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
feat(spec)!: `@objectstack/spec/system` no longer exports the orphan notification-template vocabulary — `EmailTemplate(Schema)`, `SMSTemplate(Schema)`, `PushNotification(Schema)`, `InAppNotification(Schema)` (#4616)
6+
7+
These four schemas existed **only** as the member shapes of the
8+
`NotificationConfigSchema.template` union, and #4610 (#4535 C3) deleted that
9+
union. Since then they have been reachable from no parent schema and from no
10+
metadata-type root: nothing in framework, cloud or objectui parsed a document
11+
against them, so they declared delivery capability the runtime never read
12+
(ADR-0049 enforce-or-remove, resolved by REMOVE in the v17 breaking window).
13+
14+
Migration — one line each, and in every case the replacement already exists:
15+
16+
- FROM `import { EmailTemplateSchema, type EmailTemplate } from '@objectstack/spec/system'`
17+
TO `import { EmailTemplateDefinitionSchema, type EmailTemplateDefinition } from '@objectstack/spec/system'`.
18+
**Shape change** — this is a different, richer contract, not a rename:
19+
`EmailTemplateDefinitionSchema` is keyed `name` + `locale` (not `id`), splits
20+
the body into `bodyHtml` / `bodyText` (not `body` + `bodyType`), and adds
21+
`label` / `category` / `active` / `fromOverride` / `replyTo`. It is also a
22+
`strictObject`, so the old keys are rejected loudly rather than stripped.
23+
This is the schema the `email_template` metadata kind has resolved to since
24+
spec **7.1.0**, which demoted `EmailTemplateSchema` when it fixed that Prime
25+
Directive #8 double-declaration and kept it "only as an inline sub-shape
26+
inside `Notification`" — #4610 removed that holder, and #4616 finishes the
27+
job. If your code registers a client-side or publish-time validator for
28+
`email_template`, it must point at `EmailTemplateDefinitionSchema`;
29+
`BUILTIN_METADATA_TYPE_SCHEMAS` (`kernel/metadata-type-schemas.ts`) is the
30+
authority.
31+
- FROM `import { SMSTemplateSchema, type SMSTemplate } from '@objectstack/spec/system'`
32+
TO: no spec replacement, and none is needed. SMS templates are
33+
`sys_notification_template` rows resolved by `(topic, 'sms', locale)`
34+
(`service-messaging/src/sms-channel.ts`) and rendered by
35+
`template-renderer.ts`; the provider-side template is Aliyun's pre-registered
36+
`TemplateCode` in `service-sms` — a vendor API shape, never a spec constant.
37+
- FROM `import { PushNotificationSchema, type PushNotification } from '@objectstack/spec/system'`
38+
and FROM `import { InAppNotificationSchema, type InAppNotification } from '@objectstack/spec/system'`
39+
TO: no replacement. Neither channel has a delivery implementation (#3197):
40+
the dispatcher dead-letters any message addressed to them, so these payload
41+
shapes advertised a capability nothing delivers. The live delivery ingress is
42+
`NotificationService.emit` (`INotificationService`,
43+
`@objectstack/spec/contracts`); the in-app bell reads `./api`'s
44+
`Notification(Schema)` inbox row; the presentation vocabulary is
45+
`@objectstack/spec/ui` (`NotificationTypeSchema`, `NotificationSeveritySchema`,
46+
`NotificationPositionSchema`, `NotificationActionSchema` — all unchanged).
47+
48+
Unchanged and explicitly NOT part of this removal:
49+
`@objectstack/spec/system`'s `NotificationChannel(Schema)` (live — re-exported
50+
by `@objectstack/spec/contracts`, consumed by `service-messaging`),
51+
`EmailTemplateDefinition*`, and every `@objectstack/spec/ui` notification
52+
export.
53+
54+
No ADR-0087 D2 conversion accompanies this change, deliberately: a conversion
55+
rewrites authored or stored sources, and these defs were reachable from no
56+
metadata-type root, so `os migrate meta` would have nothing to match. The
57+
removal is a TypeScript export-surface break only — same disposition as #4610
58+
in this very module. `json-schema.manifest.json` loses 4 keys and
59+
`authorable-surface.json` loses their 22 lines; both deletions are adjudicated
60+
by `gen:schema`'s #4650 route-3 check ("def no longer emitted by this build").
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
---
3+
4+
docs(pm-dispatch): domain 车道协议 —— 按「修复落点的包」划域,支持同仓多 PM 并发 (#4819)
5+
6+
`.claude/skills/pm-dispatch/SKILL.md` 新增「Domain lanes(同仓多 PM 并发)」一节:
7+
锚定规则(每个包恰好属于一个 domain,`domain:*` 标签取**修复落点所在包**的域,分诊时
8+
读代码后打,不从标题词汇猜 —— #4775 的 hook condition 概念属 automation,落点却是
9+
`packages/objectql/src/hook-wrappers.ts`,故归 `domain:engine`)、六域分类表、标签纪律
10+
(打标 ≠ 认领;未打标不得认领)、认领范围(在 #4604 登记 domain 集合)、跨域单与借单
11+
规则、选批时的全局在飞检查,以及合并队列仍是全体共享串行资源的提醒(flaky 税,#4796)。
12+
认领注释模板加「域」「文件面」两行(跨域与借单必填);Multi-repo coordination 规则 4 的
13+
「同队列多 PM 一律禁止」改为「仅在 domain 车道协议生效时允许」,repo 分片阶梯保留,
14+
domain 车道作为第三级。
15+
16+
仅改内部 agent 协议文本,不发布任何包。
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
docs(protocol): retire `protocol/kernel/runtime-capabilities` — the page taught `ObjectStackCapabilities`, a schema removed in #3605. Docs-only; releases nothing.

0 commit comments

Comments
 (0)