Skip to content

Commit dd7a961

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-4775-hook-condition-fail-loud
2 parents 11b7a99 + c4ab50b commit dd7a961

44 files changed

Lines changed: 2694 additions & 1213 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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@objectstack/metadata": patch
3+
---
4+
5+
fix(metadata): `sys_metadata` 的 DDL 失败不再被静默吞掉 —— 只有「表已存在」这一种原因可以静音 (#4728)
6+
7+
`DatabaseLoader.ensureSchema()` 过去用一个空 `catch` 吞掉 **全部** DDL 失败,并且照样把
8+
`schemaReady` 置为 `true`:
9+
10+
```ts
11+
} catch {
12+
// If syncSchema fails (e.g. table already exists), mark ready and continue
13+
this.schemaReady = true;
14+
}
15+
```
16+
17+
注释里的免责理由只覆盖了失败原因中最良性的一种,却用它为**所有**原因开脱。真实的失败
18+
(权限不足、数据源根本没连上、列类型冲突)之后,表或新列压根不存在,而进程的状态与成功
19+
路径**逐字节相同**,启动日志里一行痕迹都没有 —— 这正是 #4420 的形态:声称已持久化、实
20+
际没落盘、系统看起来完全健康。#4632 把它定成规则(AGENTS.md → "Degradation log levels"),
21+
机械检查 `pnpm check:durability-log-level` 已经能发现这一处。
22+
23+
现在按**错误类型**判别,而不是按注释里的乐观假设:
24+
25+
- **良性的「已存在」**(SQLite 的 `table … already exists` / `duplicate column name`
26+
Postgres 的 SQLSTATE `42P07`/`42701`/`42710`、MySQL 的 `ER_TABLE_EXISTS_ERROR` 等及其
27+
`errno`,并跟随 `cause` 链)—— 表确实已就绪,当作 no-op 静默通过,并照常执行后续的
28+
`project_id → environment_id` 迁移与 ADR-0005 索引。
29+
- **其余一切失败** —— 以 `console.error` 上报,文案同时说清**后果**(`sys_metadata` 的表/
30+
列未创建,后续每一次元数据写入都会报错、或在宽松驱动上悄悄丢列,而服务器仍报告健康)
31+
**修复动作**(修掉下面那条驱动/数据源错误后重启)。只说**一次**,不是每次写入都刷屏。
32+
- `schemaReady` **不再**在真实失败后置 `true`。启动依旧不被阻断(该方法不抛),但 loader
33+
不再声称一个它并不具备的就绪状态,下一次元数据操作会重试 —— 数据源只是还在连接这类瞬
34+
时故障因此可以自愈,恢复时补一条 `info`
35+
36+
`ensureHistorySchema()` 按同一规则对齐:良性「已存在」不再每次写入都打一条 `error`(过度
37+
使用 `error` 是镜像失败),真实失败则同样只响亮一次并保持重试。
38+
39+
无 API / schema 变更;新增内部工具 `isSchemaAlreadyExistsError()`(未从包入口导出)。
40+
`scripts/durability-degradation.baseline.json` 中指向本单的条目随之删除(该文件 shrink-only)。
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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
docs(protocol): `protocol/kernel/http-protocol` 的 API Discovery 一节拆成两段式 —— `@objectstack/rest` 服务的 `/api/v1`(与 `/api/v1/discovery`)与 dispatcher 服务的 `/.well-known/objectstack` 各给一份真实响应形状,不再共用一份混合示例。Docs-only;releases nothing.
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)